// creation-tool.jsx — Image generation (参考生图 + 文生图)
// Layout matches video-tool.jsx: AppShell + tabs inside aside + results panel

const IMG_ACCENT = '#22d3ee';

// ── OSS thumbnail helpers (shared with video-tool pattern) ──────────────────
let _ctCdnDomain = null;
const _ctFetchCdn = () => {
  if (_ctCdnDomain !== null) return Promise.resolve(_ctCdnDomain);
  const p = window.YiteAPI?.getCdnDomain?.() || Promise.resolve('');
  return p.then(d => { _ctCdnDomain = d; return d; });
};
const _ctThumbUrl = (url, tag, cdnDomain) => {
  if (!url || !cdnDomain || !url.startsWith(cdnDomain) || url.includes('?')) return url;
  if (tag === 'video') return url + '?x-oss-process=video/snapshot,t_1000,f_jpg,w_200,h_200,m_fast';
  if (tag === 'image') return url + '?x-oss-process=image/resize,w_200,h_200,m_fill/format,webp';
  return url;
};

const _isVideoUrl2 = url => /\.(mp4|mov|webm|avi|mkv)(\?|$)/i.test(url);

const ResultMedia2 = ({ url, style: s }) => _isVideoUrl2(url)
  ? <video src={url} muted playsInline preload="metadata" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', ...s }} />
  : <img src={url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', ...s }} />;

const IMG_TABS = [
  { id: 'ref',      labelKey: 'creation.tab.ref',  icon: 'image' },
  { id: 'text',     labelKey: 'creation.tab.text', icon: 'pen'   },
  { id: 'template', labelKey: 'nav.template',      icon: 'layers', hot: true },
];

// ─── Reuse API helpers from video-tool ───────────────────────────────────────
const _imgToken = () => window.YiteAPI?.getToken?.() || localStorage.getItem('token') || '';
const _imgAssetAPI = {
  list: (parentID = '', tag = '') => {
    let url = '/api/v1/assets?parent_id=' + encodeURIComponent(parentID);
    if (tag) url += '&tag=' + encodeURIComponent(tag);
    return fetch(url, { headers: { Authorization: 'Bearer ' + _imgToken() } }).then(r => r.json());
  },
  upload: (file, parentID = '') => {
    const fd = new FormData(); fd.append('file', file); fd.append('parent_id', parentID);
    return fetch('/api/v1/assets/upload', { method: 'POST', headers: { Authorization: 'Bearer ' + _imgToken() }, body: fd }).then(r => r.json());
  },
};
const _imgTaskAPI = {
  list: (type = '') => {
    let url = '/api/v1/create-tasks?media_type=image';
    if (type) url += '&type=' + encodeURIComponent(type);
    return fetch(url, { headers: { Authorization: 'Bearer ' + _imgToken() } }).then(r => r.json());
  },
  publishTemplate: body => fetch('/api/v1/templates', {
    method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + _imgToken() },
    body: JSON.stringify(body),
  }).then(r => r.json()),
  unpublishTemplate: templateId => fetch('/api/v1/templates/' + templateId + '/unpublish', {
    method: 'POST', headers: { Authorization: 'Bearer ' + _imgToken() },
  }).then(r => r.json()),
  create: body => fetch('/api/v1/create-tasks', {
    method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + _imgToken() },
    body: JSON.stringify({ ...body, media_type: 'image' }),
  }).then(r => r.json()),
  remove: id => fetch('/api/v1/create-tasks/' + id, {
    method: 'DELETE', headers: { Authorization: 'Bearer ' + _imgToken() },
  }).then(r => r.json()),
  listTemplates: (page = 1) =>
    fetch('/api/v1/templates?media_type=image&category=template&page_size=50&page=' + page).then(r => r.json()),
  getTemplate: id =>
    fetch('/api/v1/templates/' + id).then(r => r.json()),
};

// ─── Asset picker modal (self-contained for image page) ──────────────────────
const _IMG_ASSET_TYPE_INFO = {
  image: { label: '图片', color: '#22d3ee', bg: 'rgba(34,211,238,0.15)' },
  video: { label: '视频', color: '#a78bfa', bg: 'rgba(167,139,250,0.15)' },
  audio: { label: '音频', color: '#34d399', bg: 'rgba(52,211,153,0.15)' },
};
const _imgAssetTypeInfo = tp => _IMG_ASSET_TYPE_INFO[tp] || { label: tp || '?', color: 'var(--fg-3)', bg: 'rgba(255,255,255,0.08)' };

const ImgAssetPickerModal = ({ max = 6, existing = 0, onSelect, onClose }) => {
  useModalLock(true, onClose);
  const { t } = useI18n();
  const [folders, setFolders] = React.useState([]);
  const [allFiles, setAllFiles] = React.useState([]);
  const [path, setPath] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [selected, setSelected] = React.useState(new Set());
  const [typeFilter, setTypeFilter] = React.useState('image');
  const [viewMode, setViewMode] = React.useState('list');
  const [cdnDomain, setCdnDomain] = React.useState(() => _ctCdnDomain || window.YiteAPI?.cdnDomain || '');
  const pendingSelectRef = React.useRef(null);
  const uploadRef = React.useRef(null);
  const parentID = path.length > 0 ? path[path.length - 1].id : '';
  const remaining = max - existing;
  const files = typeFilter === 'all' ? allFiles : allFiles.filter(f => f.tag === typeFilter);

  const fetchItems = async () => {
    setLoading(true);
    const res = await _imgAssetAPI.list(parentID, '');
    const items = res.items || [];
    setFolders(items.filter(i => i.is_folder));
    const newFiles = items.filter(i => !i.is_folder);
    setAllFiles(newFiles);
    if (pendingSelectRef.current) {
      const id = pendingSelectRef.current;
      pendingSelectRef.current = null;
      if (newFiles.some(f => f.id === id)) {
        setSelected(prev => {
          const next = new Set(prev);
          if (next.size < remaining) next.add(id);
          return next;
        });
      }
    }
    setLoading(false);
  };
  React.useEffect(() => {
    fetchItems();
    if (!_ctCdnDomain) _ctFetchCdn().then(d => setCdnDomain(d));
  }, [parentID]);

  const toggleSelect = id => {
    setSelected(prev => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else if (next.size < remaining) next.add(id);
      return next;
    });
  };
  const handleUpload = async e => {
    const f = e.target.files?.[0]; e.target.value = '';
    if (!f) return;
    setLoading(true);
    const res = await _imgAssetAPI.upload(f, parentID);
    if (!res.error && res.item?.id) pendingSelectRef.current = res.item.id;
    fetchItems();
  };
  const handleConfirm = () => {
    const picked = files.filter(f => selected.has(f.id)).map(f => ({ id: f.id, url: f.url, name: f.name }));
    onSelect(picked);
    onClose();
  };

  const uploadAccept = typeFilter === 'audio' ? 'audio/*' : typeFilter === 'video' ? 'video/*' : 'image/*';
  const TYPE_FILTERS = [
    { key: 'all', label: '全部' }, { key: 'image', label: '图片' },
    { key: 'video', label: '视频' }, { key: 'audio', label: '音频' },
  ];

  const ImgFileThumb = ({ f, size = 40 }) => {
    const ti = _imgAssetTypeInfo(f.tag);
    if (f.tag === 'audio') return (
      <div style={{ width: size, height: size, background: ti.bg, borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={ti.color} strokeWidth="1.5" strokeLinecap="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
      </div>
    );
    if (!f.url) return (
      <div style={{ width: size, height: size, background: 'rgba(255,255,255,0.04)', borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
        <Icon name="image" size={16} color="var(--fg-3)" />
      </div>
    );
    const thumbSrc = _ctThumbUrl(f.url, f.tag, cdnDomain);
    return <img src={thumbSrc} alt="" loading="lazy" decoding="async"
      style={{ width: size, height: size, objectFit: 'cover', borderRadius: 6, flexShrink: 0, display: 'block', background: 'rgba(255,255,255,0.04)' }} />;
  };

  return (
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.65)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 200 }}
      onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div style={{ width: 660, maxHeight: '82vh', background: '#141722', border: '1px solid var(--line-2)', borderRadius: 14, display: 'flex', flexDirection: 'column', boxShadow: '0 24px 60px rgba(0,0,0,0.6)' }} onClick={e => e.stopPropagation()}>

        {/* Header */}
        <div style={{ padding: '16px 20px', borderBottom: '1px solid var(--line)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexShrink: 0 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <span style={{ fontSize: 16, fontWeight: 700, color: 'var(--fg-0)' }}>{t('tpl.selectImages')}</span>
            <span style={{ fontSize: 13, color: 'var(--fg-3)' }}>{t('tpl.canSelect')} {remaining - selected.size} {t('tpl.items')}</span>
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <div style={{ display: 'flex', background: 'rgba(255,255,255,0.06)', borderRadius: 7, padding: 2 }}>
              {[
                { mode: 'list', icon: <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="9" y1="6" x2="21" y2="6"/><line x1="9" y1="12" x2="21" y2="12"/><line x1="9" y1="18" x2="21" y2="18"/><circle cx="4" cy="6" r="1.5" fill="currentColor" stroke="none"/><circle cx="4" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="4" cy="18" r="1.5" fill="currentColor" stroke="none"/></svg> },
                { mode: 'grid', icon: <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg> },
              ].map(({ mode, icon }) => (
                <button key={mode} onClick={() => setViewMode(mode)} style={{
                  width: 28, height: 28, border: 'none', borderRadius: 5, cursor: 'pointer',
                  background: viewMode === mode ? 'rgba(255,255,255,0.14)' : 'transparent',
                  color: viewMode === mode ? 'var(--fg-0)' : 'var(--fg-3)',
                  display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all .12s',
                }}>{icon}</button>
              ))}
            </div>
            <button onClick={onClose} style={{ background: 'none', border: 'none', color: 'var(--fg-3)', cursor: 'pointer', fontSize: 16, padding: 4 }}>✕</button>
          </div>
        </div>

        {/* Breadcrumb + type filter */}
        <div style={{ padding: '9px 20px', borderBottom: '1px solid var(--line)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexShrink: 0 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 14, flex: 1, minWidth: 0 }}>
            <button onClick={() => setPath([])} style={{ background: 'none', border: 'none', color: path.length === 0 ? 'var(--fg-0)' : IMG_ACCENT, cursor: 'pointer', fontSize: 14, fontWeight: path.length === 0 ? 600 : 400, flexShrink: 0 }}>{t('assets.myLibrary')}</button>
            {path.map((p, i) => (
              <React.Fragment key={i}>
                <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="var(--fg-3)" strokeWidth="2.5" strokeLinecap="round"><polyline points="9 18 15 12 9 6"/></svg>
                <button onClick={() => setPath(path.slice(0, i + 1))} style={{ background: 'none', border: 'none', color: i === path.length - 1 ? 'var(--fg-0)' : IMG_ACCENT, cursor: 'pointer', fontSize: 14, fontWeight: i === path.length - 1 ? 600 : 400 }}>{p.name}</button>
              </React.Fragment>
            ))}
          </div>
          <div style={{ display: 'flex', gap: 4, flexShrink: 0 }}>
            {TYPE_FILTERS.map(tf => (
              <button key={tf.key} onClick={() => setTypeFilter(tf.key)} style={{
                height: 26, padding: '0 10px', borderRadius: 6, border: '1px solid',
                borderColor: typeFilter === tf.key ? IMG_ACCENT : 'var(--line-2)',
                background: typeFilter === tf.key ? 'rgba(34,211,238,0.1)' : 'transparent',
                color: typeFilter === tf.key ? IMG_ACCENT : 'var(--fg-3)',
                fontSize: 13, fontWeight: typeFilter === tf.key ? 600 : 400, cursor: 'pointer', transition: 'all .12s',
              }}>{tf.label}</button>
            ))}
          </div>
        </div>

        {/* Content */}
        <div style={{ flex: 1, overflowY: 'auto', padding: 14 }}>
          <input ref={uploadRef} type="file" accept={uploadAccept} style={{ display: 'none' }} onChange={handleUpload} />
          {loading ? (
            <div style={{ display: 'flex', justifyContent: 'center', padding: 48 }}><div className="yt-spinner" /></div>
          ) : viewMode === 'list' ? (
            /* ── List view ── */
            <div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
              <button onClick={() => uploadRef.current?.click()} style={{
                display: 'flex', alignItems: 'center', gap: 10, padding: '7px 8px', marginBottom: 4,
                borderRadius: 8, border: '1px dashed var(--line-2)', background: 'rgba(255,255,255,0.02)',
                cursor: 'pointer', color: 'var(--fg-3)', transition: 'all .12s', textAlign: 'left',
              }}
                onMouseEnter={e => { e.currentTarget.style.borderColor = IMG_ACCENT; e.currentTarget.style.background = 'rgba(34,211,238,0.04)'; }}
                onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--line-2)'; e.currentTarget.style.background = 'rgba(255,255,255,0.02)'; }}>
                <div style={{ width: 40, height: 40, borderRadius: 7, background: 'rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
                  <Icon name="plus" size={18} />
                </div>
                <span style={{ fontSize: 14 }}>{t('common.upload')}</span>
              </button>
              {folders.map(f => {
                const hue = (f.name.charCodeAt(0) * 37) % 360;
                return (
                  <div key={f.id} onClick={() => setPath([...path, { id: f.id, name: f.name }])} style={{
                    display: 'flex', alignItems: 'center', gap: 10, padding: '7px 8px',
                    borderRadius: 8, cursor: 'pointer', transition: 'background .1s',
                  }}
                    onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.05)'}
                    onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
                    <div style={{ width: 40, height: 40, borderRadius: 7, background: `hsl(${hue} 50% 12%)`, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
                      <svg width="18" height="18" viewBox="0 0 24 24" fill={`hsl(${hue} 70% 55%)`}><path d="M3 7a2 2 0 012-2h3.586a1 1 0 01.707.293L10.707 6.7A1 1 0 0011.414 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z"/></svg>
                    </div>
                    <span style={{ flex: 1, fontSize: 14, color: 'var(--fg-1)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.name}</span>
                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--fg-3)" strokeWidth="2" strokeLinecap="round"><polyline points="9 18 15 12 9 6"/></svg>
                  </div>
                );
              })}
              {files.map(f => {
                const isSel = selected.has(f.id);
                const ti = _imgAssetTypeInfo(f.tag);
                return (
                  <div key={f.id} onClick={() => toggleSelect(f.id)} style={{
                    display: 'flex', alignItems: 'center', gap: 10, padding: '6px 8px',
                    borderRadius: 8, border: `1px solid ${isSel ? IMG_ACCENT + '60' : 'transparent'}`,
                    background: isSel ? 'rgba(34,211,238,0.05)' : 'transparent',
                    cursor: 'pointer', transition: 'all .1s',
                  }}
                    onMouseEnter={e => { if (!isSel) e.currentTarget.style.background = 'rgba(255,255,255,0.04)'; }}
                    onMouseLeave={e => { if (!isSel) e.currentTarget.style.background = 'transparent'; }}>
                    <ImgFileThumb f={f} size={40} />
                    <span style={{ flex: 1, fontSize: 14, color: 'var(--fg-1)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.name}</span>
                    <span style={{ fontSize: 11, fontWeight: 600, padding: '2px 7px', borderRadius: 5, background: ti.bg, color: ti.color, flexShrink: 0 }}>{ti.label}</span>
                    <div style={{ width: 18, height: 18, borderRadius: 4, border: `2px solid ${isSel ? IMG_ACCENT : 'var(--line-2)'}`, background: isSel ? IMG_ACCENT : 'transparent', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all .12s' }}>
                      {isSel && <svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="#052028" strokeWidth="2.5" strokeLinecap="round"><polyline points="2,6 5,9 10,3"/></svg>}
                    </div>
                  </div>
                );
              })}
              {files.length === 0 && folders.length === 0 && (
                <div style={{ padding: '32px 0', textAlign: 'center', color: 'var(--fg-3)', fontSize: 14 }}>暂无文件</div>
              )}
            </div>
          ) : (
            /* ── Grid / Preview view ── */
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(100px, 1fr))', gap: 10 }}>
              <button onClick={() => uploadRef.current?.click()} style={{ aspectRatio: '1', borderRadius: 8, border: '1.5px dashed var(--line-2)', background: 'rgba(255,255,255,0.02)', cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 6, color: 'var(--fg-3)', transition: 'all .12s' }}
                onMouseEnter={e => { e.currentTarget.style.borderColor = IMG_ACCENT; e.currentTarget.style.background = 'rgba(34,211,238,0.05)'; }}
                onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--line-2)'; e.currentTarget.style.background = 'rgba(255,255,255,0.02)'; }}>
                <Icon name="plus" size={22} /><span style={{ fontSize: 12 }}>{t('common.upload')}</span>
              </button>
              {folders.map(f => {
                const hue = (f.name.charCodeAt(0) * 37) % 360;
                return (
                  <div key={f.id} onClick={() => setPath([...path, { id: f.id, name: f.name }])} style={{ aspectRatio: '1', borderRadius: 8, border: '1px solid var(--line-2)', background: `hsl(${hue} 50% 12%)`, cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 6, transition: 'all .12s' }}
                    onMouseEnter={e => e.currentTarget.style.borderColor = `hsl(${hue} 60% 40%)`}
                    onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--line-2)'}>
                    <svg width="28" height="28" viewBox="0 0 24 24" fill={`hsl(${hue} 70% 55%)`}><path d="M3 7a2 2 0 012-2h3.586a1 1 0 01.707.293L10.707 6.7A1 1 0 0011.414 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z"/></svg>
                    <span style={{ fontSize: 12, color: 'var(--fg-1)', fontWeight: 500, maxWidth: '90%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.name}</span>
                  </div>
                );
              })}
              {files.map(f => {
                const isSel = selected.has(f.id);
                const ti = _imgAssetTypeInfo(f.tag);
                return (
                  <div key={f.id} onClick={() => toggleSelect(f.id)} style={{ aspectRatio: '1', borderRadius: 8, overflow: 'hidden', position: 'relative', border: `2px solid ${isSel ? IMG_ACCENT : 'var(--line-2)'}`, cursor: 'pointer', transition: 'border-color .12s' }}>
                    {f.tag === 'audio' ? (
                      <div style={{ width: '100%', height: '100%', background: ti.bg, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 4, padding: 4 }}>
                        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke={ti.color} strokeWidth="1.5" strokeLinecap="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
                        <span style={{ fontSize: 10, color: 'var(--fg-3)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', width: '100%', textAlign: 'center', padding: '0 4px' }}>{f.name}</span>
                      </div>
                    ) : f.url ? (
                      <img src={_ctThumbUrl(f.url, f.tag, cdnDomain)} alt="" loading="lazy" decoding="async"
                        style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', background: 'rgba(255,255,255,0.04)' }} />
                    ) : (
                      <div style={{ width: '100%', height: '100%', background: 'rgba(255,255,255,0.04)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}><Icon name="image" size={20} color="var(--fg-3)" /></div>
                    )}
                    <div style={{ position: 'absolute', bottom: 4, left: 4, fontSize: 10, fontWeight: 600, padding: '1px 5px', borderRadius: 4, background: 'rgba(0,0,0,0.55)', color: ti.color, backdropFilter: 'blur(4px)' }}>{ti.label}</div>
                    {isSel && <div style={{ position: 'absolute', top: 4, right: 4, width: 20, height: 20, borderRadius: '50%', background: IMG_ACCENT, display: 'flex', alignItems: 'center', justifyContent: 'center' }}><svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="#052028" strokeWidth="2.5" strokeLinecap="round"><polyline points="2,6 5,9 10,3"/></svg></div>}
                  </div>
                );
              })}
            </div>
          )}
        </div>

        <div style={{ padding: '13px 20px', borderTop: '1px solid var(--line)', display: 'flex', justifyContent: 'flex-end', gap: 8, flexShrink: 0 }}>
          <button onClick={onClose} style={{ height: 38, padding: '0 18px', borderRadius: 8, border: '1px solid var(--line-2)', background: 'transparent', color: 'var(--fg-1)', fontSize: 14, cursor: 'pointer' }}>{t('common.cancel')}</button>
          <button onClick={handleConfirm} disabled={selected.size === 0} style={{ height: 38, padding: '0 22px', borderRadius: 8, border: 'none', background: selected.size > 0 ? IMG_ACCENT : 'rgba(255,255,255,0.06)', color: selected.size > 0 ? '#052028' : 'var(--fg-3)', fontSize: 14, fontWeight: 600, cursor: selected.size > 0 ? 'pointer' : 'not-allowed' }}>
            {t('tpl.confirmSelect')} {selected.size > 0 ? `(${selected.size})` : ''}
          </button>
        </div>
      </div>
    </div>
  );
};

// ─── Ref image strip (with labels 图1, 图2 ...) ──────────────────────────────
const RefImageStrip = ({ images, onAdd, onRemove, size = 88 }) => {
  const { t } = useI18n();
  const [pickerOpen, setPickerOpen] = React.useState(false);
  const handlePicked = picked => { picked.forEach(p => onAdd({ id: p.id, url: p.url, name: p.name })); };
  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'flex-start' }}>
        {images.map((img, idx) => (
          <div key={img.id} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
            <div style={{ position: 'relative', width: size, height: size, borderRadius: 10, overflow: 'hidden', border: '1px solid rgba(255,255,255,0.15)' }}>
              <img src={img.url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
              <button onClick={() => onRemove(img.id)} style={{
                position: 'absolute', top: 2, right: 2, width: 16, height: 16, borderRadius: '50%',
                background: 'rgba(0,0,0,0.72)', border: 'none', color: '#fff', fontSize: 10,
                cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0,
              }}>✕</button>
            </div>
            <span style={{ fontSize: 10, color: 'var(--fg-3)' }}>{t('creation.image')}{idx + 1}</span>
          </div>
        ))}
        {/* + 图片 button */}
        {images.length < 6 && (
          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
            <button onClick={() => setPickerOpen(true)} style={{
              width: size, height: size, borderRadius: 10, border: '1.5px dashed var(--line-2)',
              background: 'rgba(255,255,255,0.02)', cursor: 'pointer',
              display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 4,
              color: 'var(--fg-3)', transition: 'all .12s',
            }}
              onMouseEnter={e => { e.currentTarget.style.borderColor = IMG_ACCENT; e.currentTarget.style.background = 'rgba(34,211,238,0.05)'; }}
              onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--line-2)'; e.currentTarget.style.background = 'rgba(255,255,255,0.02)'; }}>
              <Icon name="plus" size={18} />
              <span style={{ fontSize: 10 }}>{t('creation.image')}</span>
            </button>
          </div>
        )}
      </div>
      {pickerOpen && ReactDOM.createPortal(
        <ImgAssetPickerModal max={6} existing={images.length} onSelect={handlePicked} onClose={() => setPickerOpen(false)} />,
        document.body
      )}
    </div>
  );
};

// ─── Prompt with @图N inline highlights ──────────────────────────────────────
const ImgPromptTextarea = ({ value, onChange, images, placeholder, minHeight = 120 }) => {
  const [showAt, setShowAt] = React.useState(false);
  const [atPos, setAtPos] = React.useState({ top: 0, left: 0 });
  const [caretIdx, setCaretIdx] = React.useState(0);
  const [atFilter, setAtFilter] = React.useState('');
  const [hlIdx, setHlIdx] = React.useState(0);
  const taRef = React.useRef(null);

  const getCaretCoords = (ta, pos) => {
    const div = document.createElement('div');
    const style = getComputedStyle(ta);
    ['font','fontSize','fontFamily','fontWeight','lineHeight','letterSpacing','wordSpacing',
     'padding','border','boxSizing','whiteSpace','wordWrap','overflowWrap','width'].forEach(p => div.style[p] = style[p]);
    div.style.position = 'absolute'; div.style.visibility = 'hidden'; div.style.height = 'auto'; div.style.overflow = 'hidden';
    div.textContent = ta.value.substring(0, pos);
    const span = document.createElement('span');
    span.textContent = ta.value.substring(pos) || '.';
    div.appendChild(span);
    document.body.appendChild(div);
    const taRect = ta.getBoundingClientRect();
    const top = taRect.top + (span.offsetTop - ta.scrollTop) + span.getBoundingClientRect().height + 4;
    document.body.removeChild(div);
    return { top: Math.min(top, taRect.bottom + 4), left: taRect.left };
  };

  const filteredImgs = images.filter((img, i) => !atFilter || ('图' + (i + 1)).includes(atFilter) || img.name.toLowerCase().includes(atFilter));

  const handleInput = e => {
    const v = e.target.value;
    const pos = e.target.selectionStart;
    onChange(v);
    const before = v.slice(0, pos);
    const atMatch = before.match(/@([^\s@]*)$/);
    if (atMatch && images.length > 0) {
      setAtFilter(atMatch[1].toLowerCase());
      setCaretIdx(pos);
      setHlIdx(0);
      const ta = taRef.current;
      if (ta) setAtPos(getCaretCoords(ta, pos));
      setShowAt(true);
    } else { setShowAt(false); }
  };

  const insertMention = (img, idx) => {
    const before = value.slice(0, caretIdx);
    const atStart = before.lastIndexOf('@');
    const after = value.slice(caretIdx);
    const tag = '图' + (idx + 1);
    const newVal = before.slice(0, atStart) + '@' + tag + ' ' + after;
    onChange(newVal);
    setShowAt(false);
  };

  const handleKeyDown = e => {
    if (!showAt || filteredImgs.length === 0) return;
    if (e.key === 'ArrowDown') { e.preventDefault(); setHlIdx(i => (i + 1) % filteredImgs.length); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); setHlIdx(i => (i - 1 + filteredImgs.length) % filteredImgs.length); }
    else if (e.key === 'Enter') { e.preventDefault(); const img = filteredImgs[hlIdx]; insertMention(img, images.indexOf(img)); }
    else if (e.key === 'Escape') { e.preventDefault(); setShowAt(false); }
  };

  return (
    <div style={{ position: 'relative' }}>
      <textarea ref={taRef} value={value} onChange={handleInput} onKeyDown={handleKeyDown}
        placeholder={placeholder}
        style={{ width: '100%', minHeight, resize: 'vertical', background: 'rgba(255,255,255,0.03)', border: '1px solid var(--line-2)', borderRadius: 12, padding: 16, fontSize: 15, lineHeight: 1.6, color: 'var(--fg-0)', fontFamily: 'var(--font-sans)', outline: 'none' }} />
      {showAt && filteredImgs.length > 0 && ReactDOM.createPortal(
        <div style={{ position: 'fixed', left: atPos.left, top: atPos.top, zIndex: 300, width: 260, maxHeight: 220, overflowY: 'auto', background: '#1a1e2e', border: '1px solid var(--line-2)', borderRadius: 10, padding: 4, boxShadow: '0 12px 40px rgba(0,0,0,0.6)' }}>
          <div style={{ padding: '6px 10px', fontSize: 11, color: 'var(--fg-3)', fontWeight: 600 }}>引用图片 ↑↓ 选择 ↵ 确认</div>
          {filteredImgs.map((img, i) => {
            const origIdx = images.indexOf(img);
            const isHl = i === hlIdx;
            return (
              <button key={img.id} onClick={() => insertMention(img, origIdx)}
                onMouseEnter={() => setHlIdx(i)}
                style={{
                  width: '100%', padding: '8px 10px', borderRadius: 6, border: 'none',
                  background: isHl ? 'rgba(34,211,238,0.1)' : 'transparent',
                  color: 'var(--fg-1)', fontSize: 13, cursor: 'pointer',
                  display: 'flex', alignItems: 'center', gap: 10, textAlign: 'left',
                }}>
                <div style={{ width: 32, height: 32, borderRadius: 6, overflow: 'hidden', flexShrink: 0, background: '#0a0e1a' }}>
                  {img.url && <img src={img.url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />}
                </div>
                <span style={{ fontSize: 14, fontWeight: 700, color: IMG_ACCENT }}>图{origIdx + 1}</span>
                <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: 'var(--fg-2)', fontSize: 13 }}>{img.name}</span>
              </button>
            );
          })}
        </div>,
        document.body
      )}
    </div>
  );
};

// ─── Build final prompt ──────────────────────────────────────────────────────
const buildImgPrompt = (rawPrompt, imageList) => {
  let result = rawPrompt;
  imageList.forEach((img, idx) => {
    result = result.replace(new RegExp('@图' + (idx + 1), 'g'), 'image ' + (idx + 1));
  });
  return result;
};

// ─── Model selector modal (same as video page) ──────────────────────────────
const _imgIsNew = m => {
  if (!m.created_at) return false;
  return (Date.now() - new Date(m.created_at).getTime()) < 30 * 86400000;
};

const ImgModelPickerModal = ({ models, value, onChange, onClose }) => {
  useModalLock(true, onClose);
  const { t } = useI18n();
  const [search, setSearch] = React.useState('');
  const [platform, setPlatform] = React.useState('');
  const platforms = [...new Set(models.map(m => m.platform).filter(Boolean))];
  const filtered = models.filter(m =>
    (!search || m.name.toLowerCase().includes(search.toLowerCase())) &&
    (!platform || m.platform === platform)
  );
  return ReactDOM.createPortal(
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 200 }}
      onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div style={{ width: 520, maxHeight: '80vh', background: '#16192a', border: '1px solid var(--line-2)', borderRadius: 16, display: 'flex', flexDirection: 'column', boxShadow: '0 24px 80px rgba(0,0,0,0.7)' }} onClick={e => e.stopPropagation()}>
        <div style={{ padding: '18px 22px 14px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexShrink: 0 }}>
          <span style={{ fontSize: 17, fontWeight: 700, color: 'var(--fg-0)' }}>{t('creation.selectModel')}</span>
          <button onClick={onClose} style={{ background: 'none', border: 'none', color: 'var(--fg-3)', cursor: 'pointer', fontSize: 18 }}>✕</button>
        </div>
        <div style={{ padding: '0 22px 14px', display: 'flex', alignItems: 'center', gap: 10, flexShrink: 0 }}>
          <button onClick={() => setPlatform('')} style={{ height: 34, padding: '0 16px', borderRadius: 999, border: platform === '' ? '1px solid var(--fg-1)' : '1px solid var(--line-2)', background: platform === '' ? 'rgba(255,255,255,0.08)' : 'transparent', color: platform === '' ? 'var(--fg-0)' : 'var(--fg-2)', fontSize: 14, cursor: 'pointer' }}>{t('creation.allModels')}</button>
          <div style={{ flex: 1 }} />
          {platforms.length > 1 && (
            <select value={platform} onChange={e => setPlatform(e.target.value)} style={{ height: 34, padding: '0 10px', borderRadius: 8, border: '1px solid var(--line-2)', background: 'rgba(255,255,255,0.03)', color: 'var(--fg-1)', fontSize: 14, cursor: 'pointer' }}>
              <option value="">{t('creation.allProviders')}</option>
              {platforms.map(p => <option key={p} value={p}>{p}</option>)}
            </select>
          )}
          <div style={{ position: 'relative' }}>
            <input value={search} onChange={e => setSearch(e.target.value)} placeholder={t('creation.search')} style={{ height: 34, paddingLeft: 28, paddingRight: 10, width: 150, borderRadius: 8, border: '1px solid var(--line-2)', background: 'rgba(255,255,255,0.03)', color: 'var(--fg-0)', fontSize: 14, outline: 'none' }} />
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--fg-3)" strokeWidth="2" strokeLinecap="round" style={{ position: 'absolute', left: 8, top: 8 }}><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
          </div>
        </div>
        <div style={{ flex: 1, overflowY: 'auto', padding: '0 10px 14px' }}>
          {filtered.map(m => {
            const isSel = value === m.id;
            const isNew = _imgIsNew(m);
            return (
              <button key={m.id} onClick={() => { onChange(m.id); onClose(); }} style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 14, padding: '12px 14px', borderRadius: 10, border: 'none', cursor: 'pointer', textAlign: 'left', background: isSel ? 'rgba(255,255,255,0.06)' : 'transparent', transition: 'background .1s' }}
                onMouseEnter={e => { if (!isSel) e.currentTarget.style.background = 'rgba(255,255,255,0.03)'; }}
                onMouseLeave={e => { if (!isSel) e.currentTarget.style.background = 'transparent'; }}>
                <div style={{ width: 42, height: 42, borderRadius: 10, flexShrink: 0, background: 'rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: IMG_ACCENT }}>
                  {m.icon ? <div dangerouslySetInnerHTML={{ __html: m.icon }} /> : <span style={{ fontSize: 16, fontWeight: 700 }}>{(m.name || '?')[0]}</span>}
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 2 }}>
                    <span style={{ fontSize: 14, fontWeight: 600, color: 'var(--fg-0)' }}>{m.name}</span>
                    {isNew && <span style={{ fontSize: 10, fontWeight: 700, padding: '1px 6px', borderRadius: 4, background: '#22c55e', color: '#052e16' }}>New</span>}
                    {m.is_pro && <span style={{ fontSize: 10, fontWeight: 700, padding: '1px 6px', borderRadius: 4, background: 'linear-gradient(90deg,#fbbf24,#f97316)', color: '#1c1207' }}>PRO</span>}
                  </div>
                  <div style={{ fontSize: 13, color: 'var(--fg-3)' }}>{m.extra || m.platform + ' · ' + m.credits + ' ' + t('common.credits')}</div>
                </div>
                {isSel && <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={IMG_ACCENT} strokeWidth="3" strokeLinecap="round"><polyline points="4,12 9,17 20,6"/></svg>}
              </button>
            );
          })}
          {filtered.length === 0 && <div style={{ textAlign: 'center', padding: 40, color: 'var(--fg-3)', fontSize: 13 }}>{t('creation.noModels')}</div>}
        </div>
      </div>
    </div>,
    document.body
  );
};

const ImgModelSelect = ({ value, onChange, models }) => {
  const { t } = useI18n();
  const active = models.find(m => m.id === value) || models[0];
  const [open, setOpen] = React.useState(false);
  return (
    <div>
      <button onClick={() => setOpen(true)} style={{
        display: 'flex', alignItems: 'center', gap: 8,
        padding: '6px 12px', borderRadius: 8,
        background: 'rgba(255,255,255,0.04)', border: '1px solid var(--line-2)',
        color: 'var(--fg-0)', fontSize: 13, cursor: 'pointer',
      }}>
        {active?.icon && <div style={{ width: 20, height: 20, display: 'flex', alignItems: 'center', justifyContent: 'center', color: IMG_ACCENT }} dangerouslySetInnerHTML={{ __html: active.icon }} />}
        <span style={{ fontWeight: 600 }}>{active?.name || t('creation.selectModel')}</span>
        {active?.is_pro && <span style={{ fontSize: 9, fontWeight: 700, padding: '1px 5px', borderRadius: 3, background: 'linear-gradient(90deg,#fbbf24,#f97316)', color: '#1c1207' }}>PRO</span>}
        <svg width="10" height="10" viewBox="0 0 10 10" fill="var(--fg-3)"><path d="M2 3.5l3 3 3-3"/></svg>
      </button>
      {open && <ImgModelPickerModal models={models} value={value || active?.id} onChange={onChange} onClose={() => setOpen(false)} />}
    </div>
  );
};

// ─── Aspect ratio select ─────────────────────────────────────────────────────
const IMG_ASPECTS = [
  { value: '1:1',  w: 1, h: 1 },
  { value: '16:9', w: 16, h: 9 },
  { value: '9:16', w: 9, h: 16 },
  { value: '4:3',  w: 4, h: 3 },
  { value: '3:4',  w: 3, h: 4 },
];
const ImgAspectIcon = ({ w, h, size = 18, color = 'currentColor' }) => {
  if (w === 0 && h === 0) {
    return (
      <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={1.5} strokeLinecap="round">
        <rect x="3" y="3" width="12" height="12" rx="2" /><rect x="9" y="9" width="12" height="12" rx="2" />
      </svg>
    );
  }
  const maxDim = size - 4;
  const scale = maxDim / Math.max(w, h);
  const rw = Math.round(w * scale);
  const rh = Math.round(h * scale);
  const x = Math.round((size - rw) / 2);
  const y = Math.round((size - rh) / 2);
  return (
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
      <rect x={x} y={y} width={rw} height={rh} rx={2} fill="none" stroke={color} strokeWidth={1.5} />
    </svg>
  );
};
const ImgAspectSelect = ({ value, onChange }) => (
  <div style={{ display: 'flex', gap: 3 }}>
    {IMG_ASPECTS.map(r => {
      const isActive = r.value === value;
      return (
        <button key={r.value} onClick={() => onChange(r.value)} style={{
          flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4,
          padding: '8px 2px', borderRadius: 8, border: 'none', cursor: 'pointer',
          background: isActive ? 'rgba(34,211,238,0.12)' : 'rgba(255,255,255,0.03)',
          outline: isActive ? '1.5px solid rgba(34,211,238,0.5)' : '1px solid transparent',
          transition: 'all .15s',
        }}
          onMouseEnter={e => { if (!isActive) e.currentTarget.style.background = 'rgba(255,255,255,0.06)'; }}
          onMouseLeave={e => { if (!isActive) e.currentTarget.style.background = isActive ? 'rgba(34,211,238,0.12)' : 'rgba(255,255,255,0.03)'; }}>
          <ImgAspectIcon w={r.w} h={r.h} size={20} color={isActive ? IMG_ACCENT : 'var(--fg-3)'} />
          <span style={{ fontSize: 10, fontWeight: isActive ? 600 : 400, color: isActive ? IMG_ACCENT : 'var(--fg-3)' }}>{r.label || r.value}</span>
        </button>
      );
    })}
  </div>
);

// ─── Param row ───────────────────────────────────────────────────────────────
const ImgParamRow = ({ label, children, info }) => (
  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', minHeight: 42, padding: '6px 0', borderBottom: '1px solid var(--line)' }}>
    <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
      <span style={{ fontSize: 14, color: 'var(--fg-0)', fontWeight: 500 }}>{label}</span>
      {info && <span style={{ fontSize: 11, color: 'var(--fg-3)', cursor: 'help' }} title={info}>ⓘ</span>}
    </div>
    <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>{children}</div>
  </div>
);

// ─── Task card ───────────────────────────────────────────────────────────────
const _downloadFile = async (url, filename) => {
  const token = window.YiteAPI?.getToken?.() || localStorage.getItem('token') || '';
  try {
    const res = await fetch('/api/v1/download?url=' + encodeURIComponent(url) + '&filename=' + encodeURIComponent(filename), {
      headers: { Authorization: 'Bearer ' + token },
    });
    if (!res.ok) throw new Error('proxy ' + res.status);
    const blob = await res.blob();
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(a.href);
  } catch (e) {
    console.error('download failed', e);
  }
};

// ─── Media preview modal (image/video fullscreen) ────────────────────────────
const MediaPreviewModal = ({ url, type, onClose }) => ReactDOM.createPortal(
  <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.85)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 300, cursor: 'pointer' }}
    onClick={onClose}>
    <button onClick={onClose} style={{ position: 'absolute', top: 16, right: 16, background: 'rgba(255,255,255,0.1)', border: 'none', color: '#fff', width: 36, height: 36, borderRadius: '50%', fontSize: 18, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>✕</button>
    {type === 'video' ? (
      <video src={url} controls autoPlay style={{ maxWidth: '90vw', maxHeight: '90vh', borderRadius: 8 }} onClick={e => e.stopPropagation()} />
    ) : (
      <img src={url} alt="" style={{ maxWidth: '90vw', maxHeight: '90vh', borderRadius: 8, objectFit: 'contain' }} onClick={e => e.stopPropagation()} />
    )}
  </div>,
  document.body
);

// ─── Task card — feed-style with large result preview ────────────────────────
const ImgTaskCard = ({ task, onDelete, onPublish, onUnpublish, onRepeat, onEdit }) => {
  const { t } = useI18n();
  const [preview, setPreview] = React.useState(null);
  const [publishing, setPublishing] = React.useState(false);
  const isRunning = task.status === 'running' || task.status === 'pending';
  const isDone = task.status === 'done';
  const isPublished = task.published;
  const statusLabel = task.status === 'pending' ? t('status.queued') : task.status === 'running' ? t('status.generating') : isDone ? t('status.completed') : t('status.failed');
  const statusColor = task.status === 'error' ? '#f87171' : isRunning ? IMG_ACCENT : '#4ade80';
  const results = task.results || [];
  const mp = task.model_param || {};
  const metaChips = [mp.aspect_ratio, mp.resolution].filter(Boolean);

  return (
    <div style={{ borderRadius: 12, background: 'rgba(255,255,255,0.02)', border: '1px solid var(--line)', transition: 'border-color .12s', overflow: 'hidden' }}
      onMouseEnter={e => e.currentTarget.style.borderColor = 'rgba(34,211,238,0.25)'}
      onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--line)'}>

      {/* Header */}
      <div style={{ padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
        <span style={{ fontSize: 11, padding: '2px 8px', borderRadius: 4, background: isRunning ? 'rgba(34,211,238,0.15)' : task.status === 'error' ? 'rgba(248,113,113,0.15)' : 'rgba(74,222,128,0.12)', color: statusColor, fontWeight: 600 }}>{task.type}</span>
        <span style={{ fontSize: 12, color: 'var(--fg-3)' }}>{relativeTime(task.created_at)}</span>
        {metaChips.length > 0 && (
          <span style={{ fontSize: 11, color: 'var(--fg-3)', fontFamily: 'var(--font-mono)' }}>{metaChips.join(' · ')}</span>
        )}
        <span style={{ fontSize: 11, color: statusColor, fontWeight: 500, marginLeft: 'auto' }}>{statusLabel}</span>
      </div>

      {/* Prompt */}
      {task.prompt && (
        <div style={{ padding: '0 16px 10px', fontSize: 13, color: 'var(--fg-1)', lineHeight: 1.5, display: '-webkit-box', WebkitLineClamp: 3, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
          {task.prompt}
        </div>
      )}

      {/* Result images — grid for multiple, single large for one */}
      {results.length > 0 && (
        <div style={{ padding: '0 16px 12px', display: 'flex', gap: 6, flexWrap: 'wrap' }}>
          {results.map((url, i) => (
            <div key={i} onClick={() => setPreview(url)} style={{
              borderRadius: 10, overflow: 'hidden', cursor: 'pointer', background: '#06080f',
              flex: results.length === 1 ? '1 1 100%' : '1 1 calc(50% - 3px)',
              maxWidth: results.length === 1 ? '100%' : 'calc(50% - 3px)',
            }}>
              <img src={url} alt="" loading="lazy" style={{ width: '100%', display: 'block', maxHeight: results.length === 1 ? 400 : 240, objectFit: 'contain' }} />
            </div>
          ))}
        </div>
      )}

      {/* Running placeholder */}
      {isRunning && results.length === 0 && (
        <div style={{ margin: '0 16px 12px', borderRadius: 10, aspectRatio: '4/3', background: 'rgba(255,255,255,0.02)', border: '1px solid var(--line)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 10 }}>
          <div style={{ width: 32, height: 32, border: '3px solid var(--line-2)', borderTop: '3px solid ' + IMG_ACCENT, borderRadius: '50%', animation: 'spin 0.6s linear infinite' }} />
          <span style={{ fontSize: 12, color: 'var(--fg-3)' }}>{statusLabel}…</span>
        </div>
      )}

      {/* Error */}
      {task.error_msg && (
        <div style={{ margin: '0 16px 12px', padding: '8px 12px', borderRadius: 8, background: 'rgba(248,113,113,0.08)', border: '1px solid rgba(248,113,113,0.2)', fontSize: 12, color: '#fca5a5', wordBreak: 'break-all' }}>{task.error_msg}</div>
      )}

      {/* Actions */}
      <div style={{ padding: '8px 16px 12px', display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
        {/* Repeat */}
        <button onClick={() => onRepeat(task)} style={{
          height: 30, padding: '0 12px', borderRadius: 8, border: '1px solid var(--line-2)',
          background: 'rgba(255,255,255,0.04)', color: 'var(--fg-1)', fontSize: 12, fontWeight: 500, cursor: 'pointer',
          display: 'flex', alignItems: 'center', gap: 5,
        }}>
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 014-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 01-4 4H3"/></svg>
          {t('common.repeat')}
        </button>
        {/* Edit */}
        <button onClick={() => onEdit(task)} style={{
          height: 30, padding: '0 12px', borderRadius: 8, border: '1px solid var(--line-2)',
          background: 'rgba(255,255,255,0.04)', color: 'var(--fg-1)', fontSize: 12, fontWeight: 500, cursor: 'pointer',
          display: 'flex', alignItems: 'center', gap: 5,
        }}>
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
          {t('common.edit')}
        </button>
        {/* Download */}
        {isDone && results.length > 0 && (
          <button onClick={() => results.forEach((url, i) => _downloadFile(url, 'image_' + task.id + '_' + (i + 1) + '.png'))} style={{
            height: 30, padding: '0 12px', borderRadius: 8, border: '1px solid var(--line-2)',
            background: 'rgba(255,255,255,0.04)', color: 'var(--fg-1)', fontSize: 12, fontWeight: 500, cursor: 'pointer',
            display: 'flex', alignItems: 'center', gap: 5,
          }}>
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
            {t('common.download')}
          </button>
        )}
        {/* Publish */}
        {isDone && (
          <button disabled={publishing} onClick={async () => {
            setPublishing(true);
            if (isPublished) { await onUnpublish(task); } else { await onPublish(task); }
            setPublishing(false);
          }} style={{
            height: 30, padding: '0 12px', borderRadius: 8,
            border: isPublished ? '1px solid rgba(74,222,128,0.3)' : '1px solid var(--line-2)',
            background: isPublished ? 'rgba(74,222,128,0.08)' : 'rgba(255,255,255,0.04)',
            color: isPublished ? '#4ade80' : 'var(--fg-1)',
            fontSize: 12, fontWeight: 500, cursor: 'pointer',
            display: 'flex', alignItems: 'center', gap: 5, opacity: publishing ? 0.6 : 1,
          }}>
            {isPublished ? (
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><polyline points="4,12 9,17 20,6"/></svg>
            ) : (
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M22 2L11 13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
            )}
            {publishing ? t('common.processing') : isPublished ? t('status.submitted') : t('common.submit')}
          </button>
        )}
        <div style={{ flex: 1 }} />
        <button onClick={async () => { if (await ytConfirm(t('confirm.deleteTask'), { danger: true })) onDelete(task.id); }} aria-label={t('common.delete') || '删除'} style={{
          height: 30, padding: '0 10px', borderRadius: 8, border: 'none',
          background: 'transparent', color: 'var(--fg-3)', fontSize: 12, cursor: 'pointer',
          display: 'flex', alignItems: 'center', gap: 4,
        }}
          onMouseEnter={e => { e.currentTarget.style.color = '#f87171'; }}
          onMouseLeave={e => { e.currentTarget.style.color = 'var(--fg-3)'; }}>
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
          {t('common.delete')}
        </button>
      </div>

      {/* Preview modal */}
      {preview && <MediaPreviewModal url={preview} type={task.media_type} onClose={() => setPreview(null)} />}
    </div>
  );
};

// ─── Folder picker (same as video-tool, but with IMG_ACCENT) ─────────────────
const ImgFolderPickerModal = ({ onSelect, onClose }) => {
  useModalLock(true, onClose);
  const { t } = useI18n();
  const [folders, setFolders] = React.useState([]);
  const [path, setPath] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const parentID = path.length > 0 ? path[path.length - 1].id : '';
  const fetchFolders = async () => { setLoading(true); const res = await _imgAssetAPI.list(parentID, ''); setFolders((res.items || []).filter(i => i.is_folder)); setLoading(false); };
  React.useEffect(() => { fetchFolders(); }, [parentID]);
  return ReactDOM.createPortal(
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.65)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 200 }}
      onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div style={{ width: 420, maxHeight: '60vh', background: '#141722', border: '1px solid var(--line-2)', borderRadius: 14, display: 'flex', flexDirection: 'column', boxShadow: '0 24px 60px rgba(0,0,0,0.6)' }} onClick={e => e.stopPropagation()}>
        <div style={{ padding: '18px 22px', borderBottom: '1px solid var(--line)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexShrink: 0 }}>
          <span style={{ fontSize: 17, fontWeight: 700, color: 'var(--fg-0)' }}>选择保存文件夹</span>
          <button onClick={onClose} style={{ background: 'none', border: 'none', color: 'var(--fg-3)', cursor: 'pointer', fontSize: 18 }}>✕</button>
        </div>
        <div style={{ padding: '10px 22px', borderBottom: '1px solid var(--line)', display: 'flex', alignItems: 'center', gap: 5, fontSize: 14, flexShrink: 0 }}>
          <button onClick={() => setPath([])} style={{ background: 'none', border: 'none', color: path.length === 0 ? 'var(--fg-0)' : IMG_ACCENT, cursor: 'pointer', fontSize: 14, fontWeight: path.length === 0 ? 600 : 400 }}>{t('assets.myLibrary')}</button>
          {path.map((p, i) => (
            <React.Fragment key={i}>
              <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="var(--fg-3)" strokeWidth="2.5" strokeLinecap="round"><polyline points="9 18 15 12 9 6"/></svg>
              <button onClick={() => setPath(path.slice(0, i + 1))} style={{ background: 'none', border: 'none', color: i === path.length - 1 ? 'var(--fg-0)' : IMG_ACCENT, cursor: 'pointer', fontSize: 14, fontWeight: i === path.length - 1 ? 600 : 400 }}>{p.name}</button>
            </React.Fragment>
          ))}
        </div>
        <div style={{ flex: 1, overflowY: 'auto', padding: 16 }}>
          {loading ? (
            <div style={{ display: 'flex', justifyContent: 'center', padding: 30 }}><div style={{ width: 24, height: 24, border: '3px solid var(--line-2)', borderTop: '3px solid ' + IMG_ACCENT, borderRadius: '50%', animation: 'spin 0.6s linear infinite' }} /></div>
          ) : folders.length > 0 ? (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
              {folders.map(f => {
                const hue = (f.name.charCodeAt(0) * 37) % 360;
                return (
                  <div key={f.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 12px', borderRadius: 8, cursor: 'pointer', border: '1px solid transparent', transition: 'all .12s' }}
                    onMouseEnter={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.04)'; e.currentTarget.style.borderColor = 'var(--line-2)'; }}
                    onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.borderColor = 'transparent'; }}>
                    <div style={{ width: 32, height: 32, borderRadius: 7, background: `hsl(${hue} 60% 20%)`, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
                      <svg width="16" height="16" viewBox="0 0 24 24" fill={`hsl(${hue} 70% 60%)`}><path d="M3 7a2 2 0 012-2h3.586a1 1 0 01.707.293L10.707 6.7A1 1 0 0011.414 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z"/></svg>
                    </div>
                    <span onClick={e => { e.stopPropagation(); setPath([...path, { id: f.id, name: f.name }]); }} style={{ flex: 1, fontSize: 14, fontWeight: 500, color: 'var(--fg-0)', cursor: 'pointer' }}>{f.name}</span>
                    <button onClick={() => onSelect({ id: f.id, name: f.name })} style={{ height: 36, padding: '0 16px', borderRadius: 8, border: 'none', background: IMG_ACCENT, color: '#052028', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>选择</button>
                  </div>
                );
              })}
            </div>
          ) : (
            <div style={{ textAlign: 'center', padding: 30, color: 'var(--fg-3)', fontSize: 13 }}>当前目录没有文件夹</div>
          )}
        </div>
        <div style={{ padding: '14px 22px', borderTop: '1px solid var(--line)', flexShrink: 0 }}>
          <button onClick={() => onSelect({ id: parentID, name: path.length > 0 ? path[path.length - 1].name : t('assets.rootFolder') })} style={{
            width: '100%', height: 40, borderRadius: 8, border: 'none',
            background: 'rgba(34,211,238,0.12)', color: IMG_ACCENT, fontSize: 14, fontWeight: 600, cursor: 'pointer',
          }}>{t('video.saveToFolder')}{path.length > 0 ? ` (${path[path.length - 1].name})` : ` (${t('assets.rootFolder')})`}</button>
        </div>
      </div>
    </div>,
    document.body
  );
};

// ─── Template picker modal (grid of image templates) ────────────────────────
const ImgTemplatePickerModal = ({ onSelect, onClose }) => {
  useModalLock(true, onClose);
  const { t } = useI18n();
  const [templates, setTemplates] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  React.useEffect(() => {
    _imgTaskAPI.listTemplates().then(d => { setTemplates(d.items || []); setLoading(false); }).catch(() => setLoading(false));
  }, []);
  return ReactDOM.createPortal(
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.65)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 300, padding: '0 16px' }}
      onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div style={{ width: '100%', maxWidth: 720, maxHeight: '80vh', background: '#141722', border: '1px solid var(--line-2)', borderRadius: 14, display: 'flex', flexDirection: 'column', boxShadow: '0 24px 60px rgba(0,0,0,0.6)' }} onClick={e => e.stopPropagation()}>
        <div style={{ padding: '18px 22px', borderBottom: '1px solid var(--line)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
          <span style={{ fontSize: 17, fontWeight: 700, color: 'var(--fg-0)' }}>{t('nav.template')}</span>
          <button onClick={onClose} style={{ background: 'none', border: 'none', color: 'var(--fg-3)', cursor: 'pointer', fontSize: 18 }}>✕</button>
        </div>
        <div style={{ flex: 1, overflowY: 'auto', padding: 16 }}>
          {loading ? (
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 40 }}><div className="yt-spinner" /></div>
          ) : templates.length === 0 ? (
            <div style={{ textAlign: 'center', padding: 40, color: 'var(--fg-3)', fontSize: 14 }}>{t('tpl.noTemplates')}</div>
          ) : (
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: 12 }}>
              {templates.map(tpl => (
                <div key={tpl.id} onClick={() => { onSelect(tpl); onClose(); }} style={{
                  borderRadius: 10, overflow: 'hidden', cursor: 'pointer',
                  border: '1px solid var(--line-2)', transition: 'border-color .15s, transform .15s',
                  background: 'rgba(255,255,255,0.02)',
                }}
                  onMouseEnter={e => { e.currentTarget.style.borderColor = 'rgba(34,211,238,0.4)'; e.currentTarget.style.transform = 'translateY(-2px)'; }}
                  onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--line-2)'; e.currentTarget.style.transform = 'none'; }}>
                  <div style={{ aspectRatio: '4/3', background: '#0a0e1a' }}>
                    {(tpl.cover_url || tpl.result_url) && <img src={tpl.cover_url || tpl.result_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />}
                  </div>
                  <div style={{ padding: '8px 10px' }}>
                    <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--fg-0)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{tpl.title || t('tpl.untitled')}</div>
                    <div style={{ fontSize: 11, color: 'var(--fg-3)', marginTop: 2 }}>{tpl.use_count || 0} {t('tpl.usageCount')}</div>
                  </div>
                </div>
              ))}
            </div>
          )}
        </div>
      </div>
    </div>,
    document.body
  );
};

// ─── Template image slots (for template tab) ────────────────────────────────
const TplImageSlots = ({ slots, onPickSlot, size = 88 }) => {
  const { t } = useI18n();
  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'flex-start' }}>
        {slots.map((slot, idx) => (
          <div key={idx} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
            <div onClick={() => onPickSlot(idx)} style={{
              position: 'relative', width: size, height: size, borderRadius: 10, overflow: 'hidden',
              border: slot.user_url ? '1.5px solid rgba(34,211,238,0.4)' : '1.5px dashed var(--line-2)',
              cursor: 'pointer', background: '#0a0e1a',
            }}>
              {slot.user_url ? (
                <img src={slot.user_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
              ) : slot.original_url ? (
                <img src={slot.original_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', opacity: 0.3 }} />
              ) : (
                <div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 4, color: 'var(--fg-3)' }}>
                  <Icon name="image" size={20} />
                </div>
              )}
              {!slot.user_url && (
                <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                  <span style={{ fontSize: 11, color: '#fff', background: 'rgba(0,0,0,0.6)', padding: '2px 8px', borderRadius: 4 }}>Replace</span>
                </div>
              )}
            </div>
            <span style={{ fontSize: 10, color: 'var(--fg-3)' }}>{slot.label}</span>
          </div>
        ))}
      </div>
    </div>
  );
};

// ═══════ MAIN SCREEN ═══════
const CreationToolScreen = ({ initialTab = 'ref' }) => {
  const { t } = useI18n();
  const { isMobile, isTablet, w } = useBreakpoint();

  React.useEffect(() => {
    if (!window.YiteAPI?.isLoggedIn?.()) {
      window.showLoginModal?.().then(ok => {
        if (ok) window.location.reload();
        else window.location.href = '/';
      });
    }
  }, []);
  const isCompact = isMobile || isTablet;
  const sidebarW = w >= 3840 ? '600px' : w >= 2560 ? '540px' : '480px';
  const thumbSize = w >= 2560 ? 110 : w >= 1440 ? 96 : 88;
  const promptH = w >= 2560 ? 200 : w >= 1440 ? 160 : 120;
  const [mobileView, setMobileView] = React.useState('config');
  const [tab, setTabState] = React.useState(initialTab);
  const setTab = id => {
    setTabState(id);
    const url = new URL(window.location.href);
    url.searchParams.set('tab', id);
    window.history.pushState({}, '', url.toString());
  };
  // ── Template tab state ──
  const [selectedTpl, setSelectedTpl] = React.useState(null);
  const [tplSlots, setTplSlots] = React.useState([]);
  const [tplPickerOpen, setTplPickerOpen] = React.useState(false);
  const [tplSlotPicking, setTplSlotPicking] = React.useState(null); // index of slot being picked

  const handleSelectTemplate = tpl => {
    // Fetch full template data with task details
    _imgTaskAPI.getTemplate(tpl.id).then(data => {
      if (!data || data.error) return;
      setSelectedTpl(data);
      const task = data.task;
      const mp = task?.model_param || data.model_param || {};
      // Prefill prompt (editable)
      setPrompt(task?.prompt || data.prompt || '');
      // Prefill model
      const m = task?.model || data.model_id;
      if (m) { tplModelRef.current = m; setModel(m); }
      // Prefill params
      if (mp.resolution) setResolution(String(mp.resolution));
      if (mp.aspect_ratio) setAspect(String(mp.aspect_ratio));
      // Setup image slots
      const imgList = data.image_list || task?.image_list || [];
      if (imgList.length > 0) {
        setTplSlots(imgList.map((img, i) => ({
          role: img.role || 'reference_image',
          original_url: img.image_url,
          user_url: null,
          label: img.role === 'first_frame' ? 'Start' : img.role === 'last_frame' ? 'End' : (i + 1).toString(),
        })));
      } else {
        const cnt = data.image_count || 1;
        setTplSlots(Array.from({ length: cnt }, (_, i) => ({
          role: 'reference_image', original_url: null, user_url: null, label: (i + 1).toString(),
        })));
      }
    });
  };

  const handleTplSlotPicked = picked => {
    if (tplSlotPicking == null || picked.length === 0) return;
    setTplSlots(prev => prev.map((s, i) => i === tplSlotPicking ? { ...s, user_url: picked[0].url } : s));
    setTplSlotPicking(null);
  };

  // Fetch models from API
  const [imgModels, setImgModels] = React.useState([]);
  const [model, setModel] = usePersistentState('yt_img_model', '');
  const tplModelRef = React.useRef('');

  React.useEffect(() => {
    fetch('/api/v1/ai-models?media_type=image').then(r => r.json()).then(d => {
      if (d.items && d.items.length > 0) {
        setImgModels(d.items);
        const prefill = tplModelRef.current;
        if (prefill && d.items.some(m => m.id === prefill)) {
          setModel(prefill);
          tplModelRef.current = '';
        } else if (!model && d.items.length > 0) {
          setModel(d.items[0].id);
        }
      }
    });
  }, []);

  const [refImages, setRefImages] = usePersistentState('yt_img_refs', []);
  const [prompt, setPrompt] = usePersistentState('yt_img_prompt', '');
  const [resolution, setResolution] = usePersistentState('yt_img_resolution', '1080p');
  const [aspect, setAspect] = usePersistentState('yt_img_aspect', '16:9');
  const [count, setCount] = usePersistentState('yt_img_count', 1);
  const [generating, setGenerating] = React.useState(false);
  const [genError, setGenError] = React.useState('');
  const [tasks, setTasks] = React.useState([]);
  const [taskFilter, setTaskFilter] = React.useState('all');
  const [taskFilterOpen, setTaskFilterOpen] = React.useState(false);
  const [folderPickerOpen, setFolderPickerOpen] = React.useState(false);

  // Task filter options: [filterKey, labelKey]
  const IMG_TASK_FILTERS = [
    ['all',  'creation.filter.all'],
    ['ref',  'creation.filter.ref'],
    ['text', 'creation.filter.text'],
  ];
  // Map filter key to API type value
  const filterToType = key => {
    if (key === 'ref')  return '参考生图';
    if (key === 'text') return '文生图';
    return '';
  };

  const addRefImage = img => setRefImages(prev => [...prev, img]);
  const removeRefImage = id => setRefImages(prev => prev.filter(i => i.id !== id));

  // Load template params if from_template in URL
  React.useEffect(() => {
    const p = new URLSearchParams(window.location.search);
    const tplId = p.get('from_template');
    if (!tplId) return;
    console.log('[from_template] loading template:', tplId);
    fetch('/api/v1/templates/' + tplId).then(r => r.json()).then(data => {
      if (!data || data.error) return;
      const task = data.task;
      const prompt_ = task?.prompt || data.prompt;
      const model_ = task?.model || data.model_id;
      const mp = task?.model_param || data.model_param || {};
      const imgList = task?.image_list || [];

      console.log('[from_template] task:', task, 'model_param:', mp);

      if (prompt_) setPrompt(prompt_);
      if (model_) { tplModelRef.current = model_; setModel(model_); }
      if (mp.resolution) setResolution(String(mp.resolution));
      if (mp.aspect_ratio) setAspect(String(mp.aspect_ratio));
      // Prefill reference images
      if (imgList.length > 0) {
        setRefImages(imgList.map((img, i) => ({ id: 'tpl_img_' + i, url: img.image_url, name: img.role || ('图' + (i + 1)) })));
      }
      const url = new URL(window.location.href);
      url.searchParams.delete('from_template');
      window.history.replaceState({}, '', url.toString());
    }).catch(e => console.error('[from_template] error:', e));
  }, []);

  // Fetch tasks
  const fetchTasks = React.useCallback(() => {
    const typeParam = filterToType(taskFilter);
    _imgTaskAPI.list(typeParam).then(res => { if (res.items) setTasks(res.items); });
  }, [taskFilter]);
  React.useEffect(() => { fetchTasks(); }, [fetchTasks]);
  React.useEffect(() => {
    const hasRunning = tasks.some(t => t.status === 'pending' || t.status === 'running');
    if (!hasRunning) return;
    const id = setInterval(fetchTasks, 5000);
    return () => clearInterval(id);
  }, [tasks, fetchTasks]);

  const activeModel = imgModels.find(m => m.id === model) || imgModels[0] || { credits: 4 };
  const credits = activeModel.credits * count;

  const handleGenerateClick = () => {
    if (!prompt.trim()) { setGenError(t('creation.enterPrompt')); return; }
    if (tab === 'ref' && refImages.length === 0) { setGenError(t('creation.addRefImage')); return; }
    if (tab === 'template' && tplSlots.some(s => !s.user_url)) { setGenError(t('tpl.missingImages') + tplSlots.filter(s => !s.user_url).length + ')'); return; }
    if (!window.YiteAPI?.isLoggedIn()) { showLoginModal(); return; }
    setGenError('');
    setFolderPickerOpen(true);
  };

  const handleFolderSelected = async folder => {
    setFolderPickerOpen(false);
    setGenerating(true); setGenError('');

    if (repeatImgTask) {
      try {
        const res = await _imgTaskAPI.create({ ...repeatImgTask, id: undefined, folder_id: folder.id });
        if (res.error) { setGenError(res.error); } else { fetchTasks(); if (isCompact) setMobileView('results'); }
      } catch (e) { setGenError(e.message || t('video.createFailed')); }
      finally { setGenerating(false); setRepeatImgTask(null); }
      return;
    }

    let typeLabel, imageList, finalPrompt;
    if (tab === 'template') {
      typeLabel = selectedTpl?.task_type || '参考生图';
      imageList = tplSlots.map(s => ({ image_url: s.user_url, role: s.role }));
      finalPrompt = prompt.trim();
    } else {
      typeLabel = tab === 'ref' ? '参考生图' : '文生图';
      imageList = refImages.filter(img => img.url).map(img => ({ image_url: img.url, role: 'reference_image' }));
      finalPrompt = buildImgPrompt(prompt.trim(), refImages);
    }
    try {
      const body = {
        type: typeLabel, prompt: finalPrompt, model: model || activeModel?.id || '',
        model_param: { resolution, aspect_ratio: aspect, count },
        image_list: imageList, folder_id: folder.id,
      };
      if (tab === 'template' && selectedTpl) body.template_id = selectedTpl.id;
      const res = await _imgTaskAPI.create(body);
      if (res.error) { setGenError(res.error); } else { fetchTasks(); if (isCompact) setMobileView('results'); }
    } catch (e) { setGenError(e.message || t('video.createFailed')); }
    finally { setGenerating(false); }
  };

  const handleDeleteTask = async id => {
    await _imgTaskAPI.remove(id);
    fetchTasks();
  };

  const [repeatImgTask, setRepeatImgTask] = React.useState(null);
  const handleRepeatTask = task => {
    setRepeatImgTask(task);
    setFolderPickerOpen(true);
  };

  const handleEditTask = task => {
    const mp = task.model_param || {};
    const imgList = task.image_list || [];
    if (task.prompt) setPrompt(task.prompt);
    if (task.model) setModel(task.model);
    if (mp.aspect_ratio) setAspect(String(mp.aspect_ratio));
    if (mp.resolution) setResolution(String(mp.resolution));
    if (mp.count) setCount(Number(mp.count));
    const refs = imgList.filter(img => img.role === 'reference_image' || !img.role);
    if (refs.length > 0) {
      setTab('ref');
      setRefImages(refs.map((img, i) => ({ id: 'edit_' + i, url: img.image_url, name: t('creation.image') + (i + 1) })));
    } else {
      setTab('text');
    }
    if (isCompact) setMobileView('config');
    window.scrollTo({ top: 0, behavior: 'smooth' });
  };

  const [publishingTask, setPublishingTask] = React.useState(null);

  const handlePublishTask = task => {
    setPublishingTask(task);
  };

  const handlePublishConfirm = async (title, isTemplate, customCoverFile) => {
    const task = publishingTask;
    setPublishingTask(null);
    if (!task) return;
    let coverOssKey = '';
    if (customCoverFile) {
      const fd = new FormData(); fd.append('file', customCoverFile);
      const coverRes = await fetch('/api/v1/templates/cover', { method: 'POST', headers: { Authorization: 'Bearer ' + _imgToken() }, body: fd }).then(r => r.json());
      if (coverRes.oss_key) coverOssKey = coverRes.oss_key;
    }
    const res = await _imgTaskAPI.publishTemplate({ task_id: task.id, title, is_public: true, is_template: !!isTemplate, cover_oss_key: coverOssKey });
    if (res.error) { ytAlert(res.error); return; }
    fetchTasks();
  };

  const handleUnpublishTask = async task => {
    if (!task.template_id) return;
    const res = await _imgTaskAPI.unpublishTemplate(task.template_id);
    if (res.error) { ytAlert(res.error); return; }
    fetchTasks();
  };

  // Current filter label for display
  const currentFilterEntry = IMG_TASK_FILTERS.find(([k]) => k === taskFilter) || IMG_TASK_FILTERS[0];

  return (
    <AppShell active="image">
      <div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>

        <div style={{
          flex: 1,
          display: isCompact ? 'flex' : 'grid',
          flexDirection: isCompact ? 'column' : undefined,
          gridTemplateColumns: isCompact ? undefined : (sidebarW + ' 1fr'),
          overflow: isCompact ? 'auto' : 'hidden',
        }}>

          {/* ── LEFT (mobile: top): tabs + params ── */}
          <aside style={{ borderRight: isCompact ? 'none' : '1px solid var(--line)', borderBottom: isCompact ? '1px solid var(--line)' : 'none', display: 'flex', flexDirection: 'column', overflow: isCompact ? 'visible' : 'hidden', flexShrink: 0 }}>

            {/* Tab switcher */}
            <div style={{ display: 'flex', borderBottom: '1px solid var(--line)', flexShrink: 0, alignItems: 'stretch', position: isCompact ? 'sticky' : 'static', top: 0, background: 'rgba(8,11,20,0.95)', backdropFilter: isCompact ? 'blur(10px)' : 'none', zIndex: 5 }}>
              {IMG_TABS.map(tb => {
                const isActive = tab === tb.id;
                return (
                  <button key={tb.id} onClick={() => setTab(tb.id)} style={{
                    flex: 1, minWidth: 0, height: isCompact ? 48 : 40, border: 'none',
                    borderBottom: isActive ? '2px solid ' + IMG_ACCENT : '2px solid transparent',
                    background: isActive ? 'rgba(34,211,238,0.04)' : 'transparent',
                    color: isActive ? 'var(--fg-0)' : 'var(--fg-3)',
                    fontSize: isCompact ? 13 : 14, fontWeight: isActive ? 600 : 500, cursor: 'pointer',
                    display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
                    whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                    padding: '0 8px',
                    transition: 'color .12s, background .12s',
                  }}>
                    <Icon name={tb.icon} size={15} color={isActive ? IMG_ACCENT : 'var(--fg-3)'} />
                    <span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{t(tb.labelKey)}</span>
                    {tb.hot && <span style={{ fontSize: 9, fontWeight: 700, padding: '1px 5px', borderRadius: 3, background: 'linear-gradient(90deg,#fbbf24,#f97316)', color: '#1c1207', lineHeight: 1.2, flexShrink: 0 }}>HOT</span>}
                  </button>
                );
              })}
            </div>

            {/* Scrollable content (mobile: 父级滚动) */}
            <div style={{ flex: 1, overflowY: isCompact ? 'visible' : 'auto', padding: isCompact ? '14px 16px' : '16px 20px' }}>

              {/* ── Template tab ── */}
              {tab === 'template' && (
                <div>
                  {/* Template selector */}
                  <div onClick={() => setTplPickerOpen(true)} style={{
                    display: 'flex', alignItems: 'center', gap: 12, padding: '10px 14px', borderRadius: 10,
                    border: '1px solid var(--line-2)', cursor: 'pointer', marginBottom: 16,
                    background: 'rgba(255,255,255,0.02)', transition: 'border-color .12s',
                  }}
                    onMouseEnter={e => e.currentTarget.style.borderColor = 'rgba(34,211,238,0.3)'}
                    onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--line-2)'}>
                    {selectedTpl ? (
                      <>
                        <div style={{ width: 44, height: 44, borderRadius: 8, overflow: 'hidden', background: '#0a0e1a', flexShrink: 0 }}>
                          {(selectedTpl.cover_url || selectedTpl.result_url) && <img src={selectedTpl.cover_url || selectedTpl.result_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />}
                        </div>
                        <div style={{ flex: 1, minWidth: 0 }}>
                          <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--fg-0)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{selectedTpl.title || t('tpl.untitled')}</div>
                          <div style={{ fontSize: 12, color: 'var(--fg-3)' }}>{selectedTpl.image_count || tplSlots.length} {t('creation.image')} · {selectedTpl.use_count || 0} {t('tpl.usageCount')}</div>
                        </div>
                      </>
                    ) : (
                      <div style={{ flex: 1, textAlign: 'center', color: 'var(--fg-3)', fontSize: 14 }}>{t('tpl.selectTemplate')}</div>
                    )}
                    <Icon name="chevron" size={14} color="var(--fg-3)" />
                  </div>
                  {/* Image slots */}
                  {tplSlots.length > 0 && <TplImageSlots slots={tplSlots} onPickSlot={idx => setTplSlotPicking(idx)} size={thumbSize} />}
                </div>
              )}

              {/* ── Ref tab images ── */}
              {tab === 'ref' && <RefImageStrip images={refImages} onAdd={addRefImage} onRemove={removeRefImage} size={thumbSize} />}

              {/* Prompt (shared across all tabs) */}
              <div style={{ marginBottom: 16 }}>
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
                  <span style={{ fontSize: 14, color: 'var(--fg-0)', fontWeight: 500 }}>Prompt</span>
                  <button aria-label="AI 优化提示词" style={{ background: 'none', border: 'none', color: 'var(--fg-3)', cursor: 'pointer', width: 32, height: 32, borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center' }}><Icon name="sparkle" size={14} /></button>
                </div>
                {tab === 'ref' && refImages.length > 0 ? (
                  <ImgPromptTextarea value={prompt} onChange={setPrompt} images={refImages} placeholder={t('creation.promptPlaceholder.ref')} minHeight={promptH} />
                ) : (
                  <textarea value={prompt} onChange={e => setPrompt(e.target.value)}
                    placeholder={tab === 'template' ? (selectedTpl ? t('creation.promptPlaceholder.ref') : t('tpl.selectTemplate')) : t('creation.promptPlaceholder.text')}
                    style={{ width: '100%', minHeight: promptH, resize: 'vertical', background: 'rgba(255,255,255,0.03)', border: '1px solid var(--line-2)', borderRadius: 12, padding: 16, fontSize: 15, lineHeight: 1.6, color: 'var(--fg-0)', fontFamily: 'var(--font-sans)', outline: 'none' }} />
                )}
              </div>

              {/* Divider before params */}
              <div style={{ height: 1, background: 'var(--line)', margin: '0 0 8px' }} />

              {/* Params */}
              <ImgParamRow label={t('creation.selectModel')}>
                <ImgModelSelect value={model || activeModel?.id} onChange={setModel} models={imgModels} />
              </ImgParamRow>

              <ImgParamRow label={t('creation.resolution')}>
                <select value={resolution} onChange={e => setResolution(e.target.value)} style={{ padding: '5px 10px', borderRadius: 8, background: 'rgba(255,255,255,0.04)', border: '1px solid var(--line-2)', color: 'var(--fg-0)', fontSize: 14 }}>
                  <option value="720p">720p</option>
                  <option value="1080p">1080p</option>
                  <option value="4K">4K</option>
                </select>
              </ImgParamRow>

              <div style={{ padding: '8px 0 6px', borderBottom: '1px solid var(--line)', flexShrink: 0 }}>
                <div style={{ fontSize: 14, color: 'var(--fg-0)', fontWeight: 500, marginBottom: 8 }}>{t('creation.aspectRatio')}</div>
                <ImgAspectSelect value={aspect} onChange={setAspect} />
              </div>

            </div>

            {/* Create button */}
            <div style={{ padding: '12px 20px 16px', borderTop: '1px solid var(--line)', flexShrink: 0 }}>
              {genError && (
                <div style={{ marginBottom: 8, padding: '7px 10px', borderRadius: 8, background: 'rgba(248,113,113,0.1)', border: '1px solid rgba(248,113,113,0.3)', fontSize: 12, color: '#fca5a5' }}>{genError}</div>
              )}
              <button disabled={generating} onClick={handleGenerateClick} style={{
                width: '100%', height: 46, borderRadius: 10, border: 'none',
                cursor: generating ? 'not-allowed' : 'pointer',
                fontSize: 15, fontWeight: 700,
                display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
                background: 'linear-gradient(135deg, var(--accent), var(--accent-2))',
                color: '#052028', opacity: generating ? 0.7 : 1,
                boxShadow: '0 1px 0 rgba(255,255,255,.3) inset, 0 6px 16px rgba(34,211,238,.2)',
              }}>
                {generating ? <span style={{ fontSize: 13 }}>{t('creation.inProgress')}…</span> : <>{t('creation.create')} <span style={{ opacity: 0.7, fontSize: 12, fontWeight: 500 }}>· {credits} {t('common.credits')}</span></>}
              </button>
            </div>
          </aside>

          {/* ── RIGHT (mobile: 紧接参数下方) ── */}
          <main style={{ overflowY: isCompact ? 'visible' : 'auto', padding: isCompact ? '20px 16px 24px' : '24px 32px', display: 'block' }}>
            <div style={{ width: '100%', maxWidth: isCompact ? '100%' : (w >= 2560 ? 960 : 720), margin: '0 auto', display: 'flex', flexDirection: 'column' }}>
            {/* Mobile section header */}
            {isCompact && (
              <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 14, paddingBottom: 10, borderBottom: '1px solid var(--line)' }}>
                <span style={{ fontSize: 16, fontWeight: 600, color: 'var(--fg-0)' }}>执行历史</span>
                {tasks.length > 0 && <span style={{ fontSize: 12, color: 'var(--fg-3)' }}>{tasks.length} 项</span>}
              </div>
            )}
            {/* Filter bar */}
            <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 18, flexShrink: 0 }}>
              <button style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '5px 12px', borderRadius: 8, background: 'rgba(255,255,255,0.04)', border: '1px solid var(--line-2)', color: 'var(--fg-1)', fontSize: 13, cursor: 'pointer' }}>
                <span style={{ width: 7, height: 7, borderRadius: 999, background: '#4ade80' }} />
                {t('creation.inProgress')} <span style={{ fontFamily: 'var(--font-mono)', color: 'var(--fg-3)' }}>{tasks.filter(t => t.status === 'pending' || t.status === 'running').length}</span>
              </button>
              <div style={{ position: 'relative' }}>
                <button onClick={() => setTaskFilterOpen(!taskFilterOpen)} style={{ display: 'flex', alignItems: 'center', gap: 5, padding: '5px 12px', borderRadius: 8, background: 'rgba(255,255,255,0.04)', border: '1px solid var(--line-2)', color: 'var(--fg-1)', fontSize: 13, cursor: 'pointer' }}>
                  {taskFilter === 'all' ? t('creation.taskType') : t(currentFilterEntry[1])}
                  <svg width="9" height="9" viewBox="0 0 10 10" fill="var(--fg-3)"><path d="M2 3.5l3 3 3-3"/></svg>
                </button>
                {taskFilterOpen && (
                  <div style={{ position: 'absolute', top: '100%', left: 0, marginTop: 4, width: 155, background: '#141722', border: '1px solid var(--line-2)', borderRadius: 10, padding: 4, zIndex: 50, boxShadow: '0 12px 40px rgba(0,0,0,0.5)' }}>
                    {IMG_TASK_FILTERS.map(([key, labelKey]) => (
                      <button key={key} onClick={() => { setTaskFilter(key); setTaskFilterOpen(false); }} style={{ width: '100%', textAlign: 'left', padding: '7px 11px', borderRadius: 6, border: 'none', background: taskFilter === key ? 'rgba(34,211,238,0.1)' : 'transparent', color: 'var(--fg-1)', fontSize: 13, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                        {t(labelKey)} {taskFilter === key && <span style={{ color: IMG_ACCENT }}>✓</span>}
                      </button>
                    ))}
                  </div>
                )}
              </div>
            </div>

            {/* Task list */}
            {tasks.length > 0 ? (
              <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
                {tasks.map(tk => <ImgTaskCard key={tk.id} task={tk} onDelete={handleDeleteTask} onPublish={handlePublishTask} onUnpublish={handleUnpublishTask} onRepeat={handleRepeatTask} onEdit={handleEditTask} />)}
              </div>
            ) : (
              <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 14 }}>
                <div style={{ width: 80, height: 80, borderRadius: '50%', background: 'rgba(255,255,255,0.02)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                  <Icon name="image" size={36} color="var(--fg-3)" />
                </div>
                <div style={{ fontSize: 13, color: 'var(--fg-3)' }}>{t('creation.emptyFirst')}</div>
              </div>
            )}
            </div>{/* end maxWidth wrapper */}
          </main>
        </div>
      </div>

      {folderPickerOpen && <ImgFolderPickerModal onSelect={handleFolderSelected} onClose={() => setFolderPickerOpen(false)} />}
      {publishingTask && <PublishModal onConfirm={handlePublishConfirm} onCancel={() => setPublishingTask(null)} />}
      {tplPickerOpen && <ImgTemplatePickerModal onSelect={handleSelectTemplate} onClose={() => setTplPickerOpen(false)} />}
      {tplSlotPicking != null && <ImgAssetPickerModal max={1} existing={0} onSelect={handleTplSlotPicked} onClose={() => setTplSlotPicking(null)} />}
    </AppShell>
  );
};

Object.assign(window, { CreationToolScreen, CREATION_MODES: IMG_TABS });
