// video-tool.jsx — Dedicated video generation page
// Tabs: 参考图生成视频 | 开始/结束帧 | 文生视频
// URL: /video?tab=ref | /video?tab=frames | /video?tab=text

const VIDEO_ACCENT = '#22d3ee';
const VIDEO_ACCENT_2 = '#a78bfa';

// ── OSS thumbnail helpers ────────────────────────────────────────────────────
// Delegates to api.js's getCdnDomain() — single fetch shared across all pages.
let _vtCdnDomain = null;
const _vtFetchCdn = () => {
  if (_vtCdnDomain !== null) return Promise.resolve(_vtCdnDomain);
  const p = window.YiteAPI?.getCdnDomain?.() || Promise.resolve('');
  return p.then(d => { _vtCdnDomain = d; return d; });
};
// Returns a small thumbnail URL using OSS image processing if the URL is
// from our CDN. Falls back to the original URL for external/provider URLs.
const _vtThumbUrl = (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 _isVideoUrl = url => /\.(mp4|mov|webm|avi|mkv)(\?|$)/i.test(url);

const ResultMedia = ({ url, style: s }) => _isVideoUrl(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 VIDEO_TABS = [
  { id: 'ref',    labelKey: 'video.tab.ref',    icon: 'image' },
  { id: 'frames', labelKey: 'video.tab.frames', icon: 'video' },
  { id: 'text',   labelKey: 'video.tab.text',   icon: 'pen'   },
];

const VIDEO_TASK_FILTER_KEYS = [
  { label: '全部',     key: 'video.filter.all'    },
  { label: '参考生视频', key: 'video.filter.ref'    },
  { label: '开始结束帧', key: 'video.filter.frames' },
  { label: '文生视频',  key: 'video.filter.text'   },
];

// ─── Model selector (video only) ─────────────────────────────────────────────
// ─── Model selector — full-screen modal ──────────────────────────────────────
const _isNewModel = m => {
  if (!m.created_at) return false;
  const diff = Date.now() - new Date(m.created_at).getTime();
  return diff < 30 * 86400000; // 30 days
};

const ModelPickerModal = ({ models, value, onChange, accent, 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()}>
        {/* Header */}
        <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)' }}>Model</span>
          <button onClick={onClose} style={{ background: 'none', border: 'none', color: 'var(--fg-3)', cursor: 'pointer', fontSize: 18, padding: 4 }}>✕</button>
        </div>
        {/* Filters */}
        <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, fontWeight: 500, cursor: 'pointer',
          }}>All models</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="">All providers</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="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>
        {/* List */}
        <div style={{ flex: 1, overflowY: 'auto', padding: '0 10px 14px' }}>
          {filtered.map(m => {
            const isSelected = value === m.id;
            const isNew = _isNewModel(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: isSelected ? 'rgba(255,255,255,0.06)' : 'transparent',
                transition: 'background .1s',
              }}
                onMouseEnter={e => { if (!isSelected) e.currentTarget.style.background = 'rgba(255,255,255,0.03)'; }}
                onMouseLeave={e => { if (!isSelected) e.currentTarget.style.background = 'transparent'; }}>
                {/* Icon */}
                <div style={{ width: 42, height: 42, borderRadius: 10, flexShrink: 0, background: 'rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: accent || 'var(--accent)' }}>
                  {m.icon ? <div dangerouslySetInnerHTML={{ __html: m.icon }} /> : <span style={{ fontSize: 16, fontWeight: 700 }}>{(m.name || '?')[0]}</span>}
                </div>
                {/* Info */}
                <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)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.extra || m.platform + ' · ' + m.credits + ' ' + t('common.credits')}</div>
                </div>
                {isSelected && <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={accent || 'var(--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 VModelSelect = ({ 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: VIDEO_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 && <ModelPickerModal models={models} value={value || active?.id} onChange={onChange} accent={VIDEO_ACCENT} onClose={() => setOpen(false)} />}
    </div>
  );
};

// ─── Aspect ratio select with icons ───────────────────────────────────────────
const ASPECT_RATIOS = [
  { 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 AspectRatioIcon = ({ w, h, size = 18, color = 'currentColor' }) => {
  if (w === 0 && h === 0) {
    // Auto icon — overlapping squares
    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 AspectRatioSelect = ({ value, onChange }) => (
  <div style={{ display: 'flex', gap: 3 }}>
    {ASPECT_RATIOS.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)'; }}>
          <AspectRatioIcon w={r.w} h={r.h} size={20} color={isActive ? VIDEO_ACCENT : 'var(--fg-3)'} />
          <span style={{ fontSize: 10, fontWeight: isActive ? 600 : 400, color: isActive ? VIDEO_ACCENT : 'var(--fg-3)' }}>{r.label || r.value}</span>
        </button>
      );
    })}
  </div>
);

// ─── Info tooltip (click/hover, works on mobile) ──────────────────────────────
const InfoTip = ({ text }) => {
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    const close = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', close);
    document.addEventListener('touchstart', close);
    return () => { document.removeEventListener('mousedown', close); document.removeEventListener('touchstart', close); };
  }, [open]);
  return (
    <span ref={ref} style={{ position: 'relative', display: 'inline-flex', alignItems: 'center' }}>
      <span
        onClick={e => { e.stopPropagation(); setOpen(v => !v); }}
        onMouseEnter={() => setOpen(true)}
        onMouseLeave={() => setOpen(false)}
        style={{ fontSize: 13, color: 'var(--fg-3)', cursor: 'pointer', lineHeight: 1, userSelect: 'none' }}>ⓘ</span>
      {open && (
        <span style={{
          position: 'absolute', left: 20, top: '50%', transform: 'translateY(-50%)',
          width: 220, background: '#1e2235', border: '1px solid var(--line-2)',
          borderRadius: 8, padding: '8px 10px', fontSize: 12, color: 'var(--fg-1)',
          lineHeight: 1.5, boxShadow: '0 8px 24px rgba(0,0,0,0.5)', zIndex: 100,
          pointerEvents: 'none',
        }}>{text}</span>
      )}
    </span>
  );
};

// ─── Param row ────────────────────────────────────────────────────────────────
const VParamRow = ({ 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 && <InfoTip text={info} />}
    </div>
    <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>{children}</div>
  </div>
);

// ─── Toggle ───────────────────────────────────────────────────────────────────
const VToggleRow = ({ label, sublabel, info, checked, onChange }) => (
  <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>
      {sublabel && <span style={{ fontSize: 12, color: checked ? VIDEO_ACCENT : 'var(--fg-3)', fontWeight: 600, marginLeft: 2 }}>{checked ? 'On' : 'Off'}</span>}
      {info && <InfoTip text={info} />}
    </div>
    <div onClick={() => onChange && onChange(!checked)} style={{
      width: 36, height: 20, borderRadius: 10, padding: 2,
      background: checked ? VIDEO_ACCENT : 'rgba(255,255,255,0.1)',
      cursor: 'pointer', transition: 'background .15s', display: 'flex', alignItems: 'center',
    }}>
      <div style={{ width: 16, height: 16, borderRadius: 8, background: '#fff', transition: 'transform .15s', transform: checked ? 'translateX(16px)' : 'translateX(0)' }} />
    </div>
  </div>
);

// ─── Asset picker API (reuse from asset-library) ─────────────────────────────
const _assetToken = () => window.YiteAPI?.getToken?.() || localStorage.getItem('token') || '';
const _assetAPI = {
  list: (parentID = '', tag = '') => {
    let url = '/api/v1/assets?parent_id=' + encodeURIComponent(parentID);
    if (tag) url += '&tag=' + encodeURIComponent(tag);
    return fetch(url, { headers: { Authorization: 'Bearer ' + _assetToken() } }).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 ' + _assetToken() }, body: fd }).then(r => r.json());
  },
};

// ─── Video task API ───────────────────────────────────────────────────────────
const _videoTaskAPI = {
  list: (type = '') => {
    let url = '/api/v1/create-tasks?media_type=video';
    if (type && type !== '全部') url += '&type=' + encodeURIComponent(type);
    return fetch(url, { headers: { Authorization: 'Bearer ' + _assetToken() } }).then(r => r.json());
  },
  create: body =>
    fetch('/api/v1/create-tasks', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + _assetToken() },
      body: JSON.stringify({ ...body, media_type: 'video' }),
    }).then(r => r.json()),
  remove: id =>
    fetch('/api/v1/create-tasks/' + id, {
      method: 'DELETE', headers: { Authorization: 'Bearer ' + _assetToken() },
    }).then(r => r.json()),
  publishTemplate: body =>
    fetch('/api/v1/templates', {
      method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + _assetToken() },
      body: JSON.stringify(body),
    }).then(r => r.json()),
  unpublishTemplate: templateId =>
    fetch('/api/v1/templates/' + templateId + '/unpublish', {
      method: 'POST', headers: { Authorization: 'Bearer ' + _assetToken() },
    }).then(r => r.json()),
  uploadCover: file => {
    const fd = new FormData(); fd.append('file', file);
    return fetch('/api/v1/templates/cover', { method: 'POST', headers: { Authorization: 'Bearer ' + _assetToken() }, body: fd }).then(r => r.json());
  },
};

// ─── Relative time helper — delegate to global from shared.jsx ────────────────
const _relTime = iso => (typeof relativeTime === 'function' ? relativeTime(iso) : (iso ? new Date(iso).toLocaleDateString() : ''));

// ─── Folder picker modal (for choosing save target) ──────────────────────────
const FolderPickerModal = ({ 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 _assetAPI.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)' }}>{t('video.selectFolder')}</span>
          <button onClick={onClose} style={{ background: 'none', border: 'none', color: 'var(--fg-3)', cursor: 'pointer', fontSize: 18, padding: 4 }}>✕</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)' : VIDEO_ACCENT, cursor: 'pointer', padding: '2px 3px', 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)' : VIDEO_ACCENT, cursor: 'pointer', padding: '2px 3px', 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 ' + VIDEO_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: 30, padding: '0 14px', borderRadius: 6, border: 'none', background: VIDEO_ACCENT, color: '#052028', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>{t('video.selectHere')}</button>
                  </div>
                );
              })}
            </div>
          ) : (
            <div style={{ textAlign: 'center', padding: 30, color: 'var(--fg-3)', fontSize: 13 }}>{t('video.noFolders')}</div>
          )}
        </div>
        {/* Save to current folder */}
        <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('video.rootFolder') })} style={{
            width: '100%', height: 40, borderRadius: 8, border: 'none',
            background: 'rgba(34,211,238,0.12)', color: VIDEO_ACCENT, fontSize: 14, fontWeight: 600, cursor: 'pointer',
          }}>
            {t('video.saveToFolder')}{path.length > 0 ? ` (${path[path.length - 1].name})` : ` (${t('video.rootFolder')})`}
          </button>
        </div>
      </div>
    </div>,
    document.body
  );
};

// ─── Asset picker modal ──────────────────────────────────────────────────────
const _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 _assetTypeInfo = t => _ASSET_TYPE_INFO[t] || { label: t || '?', color: 'var(--fg-3)', bg: 'rgba(255,255,255,0.08)' };

const AssetPickerModal = ({ tag = 'image', 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(tag);
  const [viewMode, setViewMode] = React.useState('list');
  const [cdnDomain, setCdnDomain] = React.useState(() => _vtCdnDomain || 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 _assetAPI.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 (!_vtCdnDomain) _vtFetchCdn().then(d => setCdnDomain(d));
  }, [parentID]);  // eslint-disable-line react-hooks/exhaustive-deps

  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 _assetAPI.upload(f, parentID);
    if (!res.error) {
      if (res.item?.id) pendingSelectRef.current = res.item.id;
      fetchItems();
    } else {
      setLoading(false);
    }
  };

  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/*' : typeFilter === 'image' ? 'image/*' : '*/*';
  const TYPE_FILTERS = [
    { key: 'all', label: '全部' }, { key: 'image', label: '图片' },
    { key: 'video', label: '视频' }, { key: 'audio', label: '音频' },
  ];

  // All thumbnails use OSS-processed small URLs when CDN domain is known.
  // Videos use a static JPEG snapshot instead of a <video> element — avoids
  // the heavy metadata fetch that makes the picker feel slow.
  const FileThumb = ({ f, size = 40 }) => {
    const ti = _assetTypeInfo(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 = _vtThumbUrl(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', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <span style={{ fontSize: 16, fontWeight: 700, color: 'var(--fg-0)' }}>{tag === 'audio' ? t('video.selectAudio') : tag === 'image' ? t('video.selectImage') : 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 }}>
            {/* View mode toggle */}
            <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, lineHeight: 1 }}>✕</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)' : VIDEO_ACCENT, cursor: 'pointer', padding: '2px 3px', 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)' : VIDEO_ACCENT, cursor: 'pointer', padding: '2px 3px', fontSize: 14, fontWeight: i === path.length - 1 ? 600 : 400 }}>{p.name}</button>
              </React.Fragment>
            ))}
          </div>
          {/* Type filter pills */}
          <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 ? VIDEO_ACCENT : 'var(--line-2)',
                background: typeFilter === tf.key ? 'rgba(34,211,238,0.1)' : 'transparent',
                color: typeFilter === tf.key ? VIDEO_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', alignItems: 'center', justifyContent: 'center', padding: 48 }}>
              <div className="yt-spinner" />
            </div>
          ) : viewMode === 'list' ? (
            /* ── List view ── */
            <div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
              {/* Upload row */}
              <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 = VIDEO_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 */}
              {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 */}
              {files.map(f => {
                const isSel = selected.has(f.id);
                const ti = _assetTypeInfo(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 ? VIDEO_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'; }}>
                    <FileThumb 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 ? VIDEO_ACCENT : 'var(--line-2)'}`, background: isSel ? VIDEO_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 = VIDEO_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, fontWeight: 500 }}>{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 = _assetTypeInfo(f.tag);
                return (
                  <div key={f.id} onClick={() => toggleSelect(f.id)} style={{
                    aspectRatio: '1', borderRadius: 8, overflow: 'hidden', position: 'relative',
                    border: `2px solid ${isSel ? VIDEO_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={_vtThumbUrl(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>
                    )}
                    {/* Type badge */}
                    <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: VIDEO_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>

        {/* Footer */}
        <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 ? VIDEO_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>
  );
};

// ─── Image upload strip (参考图生成视频) ──────────────────────────────────────
const ImageUploadStrip = ({ 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 => (
          <div key={img.id} style={{ position: 'relative', width: size, height: size, borderRadius: 10, overflow: 'hidden', border: '1px solid rgba(255,255,255,0.12)', flexShrink: 0 }}>
            <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', lineHeight: 1, padding: 0,
            }}>✕</button>
          </div>
        ))}
        {images.length < 6 && (
          <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: 'border-color .12s, background .12s',
          }}
            onMouseEnter={e => { e.currentTarget.style.borderColor = VIDEO_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={20} />
            <span style={{ fontSize: 11, color: 'var(--fg-3)' }}>{images.length}/6</span>
          </button>
        )}
      </div>
      {pickerOpen && ReactDOM.createPortal(
        <AssetPickerModal tag="image" max={6} existing={images.length} onSelect={handlePicked} onClose={() => setPickerOpen(false)} />,
        document.body
      )}
    </div>
  );
};

// ─── Prompt textarea with @ mention ───────────────────────────────────────────
const PromptTextarea = ({ value, onChange, images, placeholder, minHeight = 90 }) => {
  const { t } = useI18n();
  const [showAt, setShowAt] = React.useState(false);
  const [atFilter, setAtFilter] = React.useState('');
  const [atPos, setAtPos] = React.useState({ top: 0, left: 0 });
  const [caretIdx, setCaretIdx] = React.useState(0);
  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 => !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) {
      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 => {
    const before = value.slice(0, caretIdx);
    const atStart = before.lastIndexOf('@');
    const after = value.slice(caretIdx);
    const newVal = before.slice(0, atStart) + '@' + img.name + ' ' + after;
    onChange(newVal);
    setShowAt(false);
    setTimeout(() => {
      const ta = taRef.current;
      if (ta) { const p = atStart + 1 + img.name.length + 1; ta.selectionStart = ta.selectionEnd = p; ta.focus(); }
    }, 0);
  };

  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(); insertMention(filteredImgs[hlIdx]); }
    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 }}>{t('video.refGuide')}</div>
          {filteredImgs.map((img, i) => {
            const origIdx = images.indexOf(img);
            const isHl = i === hlIdx;
            return (
              <button key={img.id || i} onClick={() => insertMention(img)}
                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: VIDEO_ACCENT }}>{t('creation.image')}{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: replace @imageName with "image N" ────────────────────
const buildFinalPrompt = (rawPrompt, imageList) => {
  let result = rawPrompt;
  imageList.forEach((img, idx) => {
    const pattern = new RegExp('@' + img.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
    result = result.replace(pattern, 'image ' + (idx + 1));
  });
  return result;
};

// ─── Frame picker (开始/结束帧) ───────────────────────────────────────────────
const FramePicker = ({ label, frame, onSet, onClear }) => {
  const { t } = useI18n();
  const [pickerOpen, setPickerOpen] = React.useState(false);
  const handlePicked = picked => {
    if (picked.length > 0) {
      onSet({ url: picked[0].url, name: picked[0].name });
    }
  };
  return (
    <div style={{ flex: 1 }}>
      <div style={{ fontSize: 13, color: 'var(--fg-2)', fontWeight: 500, marginBottom: 10 }}>{label}</div>
      {frame ? (
        <div style={{ position: 'relative', borderRadius: 10, overflow: 'hidden', aspectRatio: '3/4', border: '1px solid rgba(34,211,238,0.3)', background: '#06080f' }}>
          <img src={frame.url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
          <button onClick={onClear} style={{
            position: 'absolute', top: 6, right: 6, width: 20, height: 20, borderRadius: '50%',
            background: 'rgba(0,0,0,0.75)', border: 'none', color: '#fff', fontSize: 10,
            cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>✕</button>
          <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, textAlign: 'center', fontSize: 10, color: '#fff', background: 'rgba(34,211,238,0.75)', padding: '3px 0', fontWeight: 600 }}>{label}</div>
        </div>
      ) : (
        <div onClick={() => setPickerOpen(true)} style={{
          borderRadius: 10, aspectRatio: '3/4', border: '1.5px dashed var(--line-2)',
          background: 'rgba(255,255,255,0.015)', display: 'flex', flexDirection: 'column',
          alignItems: 'center', justifyContent: 'center', gap: 8, cursor: 'pointer',
          transition: 'border-color .12s, background .12s',
        }}
          onMouseEnter={e => { e.currentTarget.style.borderColor = VIDEO_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.015)'; }}>
          <div style={{ width: 36, height: 36, borderRadius: 8, background: 'rgba(34,211,238,0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            <Icon name="image" size={16} color={VIDEO_ACCENT} />
          </div>
          <span style={{ fontSize: 13, color: 'var(--fg-2)', fontWeight: 500 }}>{t('video.selectImage')}</span>
        </div>
      )}
      {pickerOpen && ReactDOM.createPortal(
        <AssetPickerModal tag="image" max={1} existing={0} onSelect={handlePicked} onClose={() => setPickerOpen(false)} />,
        document.body
      )}
    </div>
  );
};

// ─── Task card ────────────────────────────────────────────────────────────────
const VIDEO_TYPE_I18N = {
  '参考生视频': 'video.filter.ref',
  '开始结束帧': 'video.filter.frames',
  '文生视频':   'video.filter.text',
};
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);
  }
};

const VideoTaskCard = ({ 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 statusColor = task.status === 'error' ? '#f87171' : isRunning ? VIDEO_ACCENT : '#4ade80';
  const statusLabel = task.status === 'pending' ? t('status.queued') : task.status === 'running' ? t('status.generating') : isDone ? t('status.completed') : t('status.failed');
  const resultUrl = task.results && task.results.length > 0 ? task.results[0] : null;
  const mp = task.model_param || {};
  const metaChips = [mp.aspect_ratio, mp.resolution, mp.duration ? mp.duration + 's' : null].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: type badge + model + status + time */}
      <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 }}>{VIDEO_TYPE_I18N[task.type] ? t(VIDEO_TYPE_I18N[task.type]) : task.type}</span>
        <span style={{ fontSize: 12, color: 'var(--fg-3)' }}>{_relTime(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 text */}
      {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>
      )}

      {/* Hero result — large preview */}
      {resultUrl && (
        <div onClick={() => setPreview(resultUrl)} style={{ margin: '0 16px 12px', borderRadius: 10, overflow: 'hidden', cursor: 'pointer', position: 'relative', background: '#06080f' }}>
          {_isVideoUrl(resultUrl) ? (
            <video src={resultUrl} muted playsInline preload="metadata"
              onMouseEnter={e => e.target.play().catch(() => {})}
              onMouseLeave={e => { e.target.pause(); e.target.currentTime = 0; }}
              style={{ width: '100%', display: 'block', maxHeight: 400, objectFit: 'contain' }} />
          ) : (
            <img src={resultUrl} alt="" style={{ width: '100%', display: 'block', maxHeight: 400, objectFit: 'contain' }} />
          )}
          {/* Multiple results indicator */}
          {task.results && task.results.length > 1 && (
            <div style={{ position: 'absolute', top: 8, right: 8, padding: '2px 8px', borderRadius: 4, background: 'rgba(0,0,0,0.7)', fontSize: 11, color: '#fff', fontWeight: 600 }}>
              1/{task.results.length}
            </div>
          )}
        </div>
      )}

      {/* Running placeholder */}
      {isRunning && !resultUrl && (
        <div style={{ margin: '0 16px 12px', borderRadius: 10, aspectRatio: '16/9', background: 'rgba(255,255,255,0.02)', border: '1px solid var(--line)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 10 }}>
          <div className="yt-spinner" style={{ width: 32, height: 32 }} />
          <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 bar */}
      <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 && resultUrl && (
          <button onClick={() => _downloadFile(resultUrl, 'video_' + task.id + (resultUrl.includes('.mp4') ? '.mp4' : '.mov'))} 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>
        )}
        {isDone && task.results && task.results.length > 1 && (
          <button onClick={() => setPreview(resultUrl)} 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,
          }}>
            <Icon name="grid" size={12} /> {t('video.viewAll')}
          </button>
        )}
        <div style={{ flex: 1 }} />
        <button onClick={async () => { if (await ytConfirm(t('confirm.deleteTask'), { danger: true })) onDelete(task.id); }} 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>

      {/* Fullscreen preview modal */}
      {preview && 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={() => setPreview(null)}>
          <button onClick={() => setPreview(null)} 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>
          {_isVideoUrl(preview)
            ? <video src={preview} controls autoPlay style={{ maxWidth: '90vw', maxHeight: '90vh', borderRadius: 8 }} onClick={e => e.stopPropagation()} />
            : <img src={preview} alt="" style={{ maxWidth: '90vw', maxHeight: '90vh', borderRadius: 8, objectFit: 'contain' }} onClick={e => e.stopPropagation()} />
          }
        </div>,
        document.body
      )}
    </div>
  );
};

// ─── Video params (shared across all tabs) ────────────────────────────────────
const VideoRefsRow = ({ refs, onAdd, onRemove }) => {
  const { t } = useI18n();
  const MAX = 3;
  const [cdnDomain, setCdnDomain] = React.useState(() => _vtCdnDomain || window.YiteAPI?.cdnDomain || '');
  const [previewUrl, setPreviewUrl] = React.useState(null);

  React.useEffect(() => {
    if (_vtCdnDomain) return;
    _vtFetchCdn().then(d => setCdnDomain(d));
  }, []);

  return (
    <div style={{ padding: '10px 0 8px', borderBottom: '1px solid var(--line)' }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: refs.length > 0 ? 8 : 0 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <span style={{ fontSize: 14, color: 'var(--fg-0)', fontWeight: 500 }}>{t('video.videoRef')}</span>
          <InfoTip text={t('video.videoRefHint')} />
        </div>
        {refs.length < MAX && (
          <button onClick={onAdd} style={{ height: 26, padding: '0 10px', borderRadius: 6, border: '1px solid var(--line-2)', background: 'transparent', color: VIDEO_ACCENT, fontSize: 12, cursor: 'pointer', fontWeight: 500 }}>
            + {t('video.videoRefAdd')}
          </button>
        )}
      </div>
      {refs.length > 0 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
          {refs.map((ref, i) => {
            const snapUrl = _vtThumbUrl(ref.url, 'video', cdnDomain);
            return (
              <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 8px', borderRadius: 6, background: 'rgba(255,255,255,0.03)', border: '1px solid var(--line-2)' }}>
                {/* Thumbnail — click to preview */}
                <div onClick={() => setPreviewUrl(ref.url)} style={{
                  width: 52, height: 34, borderRadius: 5, overflow: 'hidden', flexShrink: 0,
                  background: 'rgba(167,139,250,0.1)', cursor: 'pointer', position: 'relative',
                }}>
                  {snapUrl ? (
                    <img src={snapUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
                  ) : (
                    <div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                      <svg width="12" height="12" viewBox="0 0 24 24" fill="var(--accent-2)"><polygon points="5 3 19 12 5 21 5 3"/></svg>
                    </div>
                  )}
                  {/* Play icon overlay */}
                  <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.25)' }}>
                    <svg width="10" height="10" viewBox="0 0 24 24" fill="#fff"><polygon points="5 3 19 12 5 21 5 3"/></svg>
                  </div>
                </div>
                <span style={{ flex: 1, fontSize: 12, color: 'var(--fg-1)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{ref.name}</span>
                <button onClick={() => onRemove(i)} style={{ width: 18, height: 18, borderRadius: 4, border: 'none', background: 'transparent', color: 'var(--fg-3)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13, lineHeight: 1 }}>✕</button>
              </div>
            );
          })}
        </div>
      )}

      {/* Video preview modal */}
      {previewUrl && ReactDOM.createPortal(
        <div onClick={() => setPreviewUrl(null)} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.82)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 300 }}>
          <div onClick={e => e.stopPropagation()} style={{ position: 'relative', maxWidth: '80vw', maxHeight: '80vh' }}>
            <video src={previewUrl} controls autoPlay muted playsInline
              style={{ maxWidth: '80vw', maxHeight: '80vh', borderRadius: 10, display: 'block', background: '#000' }} />
            <button onClick={() => setPreviewUrl(null)} style={{
              position: 'absolute', top: -13, right: -13, width: 26, height: 26,
              borderRadius: '50%', background: 'rgba(255,255,255,0.15)', border: '1px solid rgba(255,255,255,0.2)',
              color: '#fff', cursor: 'pointer', fontSize: 13, display: 'flex', alignItems: 'center', justifyContent: 'center',
            }}>✕</button>
          </div>
        </div>,
        document.body
      )}
    </div>
  );
};

const AudioRefsRow = ({ refs, onAdd, onRemove }) => {
  const { t } = useI18n();
  const MAX = 3;
  return (
    <div style={{ padding: '10px 0 8px', borderBottom: '1px solid var(--line)' }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: refs.length > 0 ? 8 : 0 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <span style={{ fontSize: 14, color: 'var(--fg-0)', fontWeight: 500 }}>{t('video.audioRef')}</span>
          <InfoTip text={t('video.audioRefHint')} />
        </div>
        {refs.length < MAX && (
          <button onClick={onAdd} style={{ height: 26, padding: '0 10px', borderRadius: 6, border: '1px solid var(--line-2)', background: 'transparent', color: VIDEO_ACCENT, fontSize: 12, cursor: 'pointer', fontWeight: 500 }}>
            + {t('video.audioRefAdd')}
          </button>
        )}
      </div>
      {refs.length > 0 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
          {refs.map((ref, i) => (
            <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 8px', borderRadius: 6, background: 'rgba(255,255,255,0.03)', border: '1px solid var(--line-2)' }}>
              <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="1.8" strokeLinecap="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
              <span style={{ flex: 1, fontSize: 12, color: 'var(--fg-1)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{ref.name}</span>
              <button onClick={() => onRemove(i)} style={{ width: 18, height: 18, borderRadius: 4, border: 'none', background: 'transparent', color: 'var(--fg-3)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13, lineHeight: 1 }}>✕</button>
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

const VideoParams = ({ videoModel, setVideoModel, videoModels, activeModel, duration, setDuration, resolution, setResolution, aspect, setAspect, count, setCount, audioOn, setAudioOn, audioRefs, onAddAudioRef, onRemoveAudioRef, credits }) => {
  const { t } = useI18n();
  return (
    <>
      <VParamRow label={t('video.model')}>
        <VModelSelect value={videoModel || activeModel?.id} onChange={setVideoModel} models={videoModels} />
      </VParamRow>
      <VParamRow label={t('video.duration')}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
          <input type="range" min={3} max={15} step={1} value={duration} onChange={e => setDuration(Number(e.target.value))}
            style={{ width: 120, accentColor: VIDEO_ACCENT, cursor: 'pointer' }} />
          <span style={{ fontSize: 14, fontWeight: 700, color: VIDEO_ACCENT, fontFamily: 'var(--font-mono)', minWidth: 28, textAlign: 'right' }}>{duration}s</span>
        </div>
      </VParamRow>
      <VParamRow label={t('video.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="480p">480p</option>
          <option value="720p">720p</option>
          <option value="1080p">1080p</option>
        </select>
      </VParamRow>
      <div style={{ padding: '10px 0 8px', borderBottom: '1px solid var(--line)' }}>
        <div style={{ fontSize: 14, color: 'var(--fg-0)', fontWeight: 500, marginBottom: 10 }}>{t('creation.aspectRatio')}</div>
        <AspectRatioSelect value={aspect} onChange={setAspect} />
      </div>
      <VToggleRow label={t('video.audio')} sublabel info={t('video.audioHint')} checked={audioOn} onChange={setAudioOn} />
      <AudioRefsRow refs={audioRefs} onAdd={onAddAudioRef} onRemove={onRemoveAudioRef} />
    </>
  );
};

// 归一化参考音视频列表（兼容纯 URL 字符串与 {id,url,name} 对象）
const normalizeMediaRefs = (refs, prefix) => {
  if (!Array.isArray(refs)) return [];
  return refs.map((ref, i) => {
    if (typeof ref === 'string') return { id: prefix + i, url: ref, name: prefix + ' ' + (i + 1) };
    const url = ref?.url || '';
    return { id: ref?.id || prefix + i, url, name: ref?.name || prefix + ' ' + (i + 1) };
  }).filter(r => r.url);
};

const taskTypeToTab = taskType => {
  if (!taskType) return null;
  if (taskType.includes('文生')) return 'text';
  if (taskType.includes('开始') || taskType.includes('结束')) return 'frames';
  return 'ref';
};

// ====== MAIN SCREEN ======
const VideoToolScreen = ({ 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');

  // Read tab from URL on mount and when hash/search changes
  const getTabFromURL = () => {
    const p = new URLSearchParams(window.location.search);
    return p.get('tab') || initialTab;
  };
  // If from_template is in URL, we'll determine the correct tab after loading
  const _initTab = getTabFromURL();
  const [tab, setTabState] = React.useState(_initTab);

  const setTab = (id) => {
    setTabState(id);
    const url = new URL(window.location.href);
    url.searchParams.set('tab', id);
    window.history.pushState({}, '', url.toString());
  };

  React.useEffect(() => {
    const handler = () => setTabState(getTabFromURL());
    window.addEventListener('popstate', handler);
    return () => window.removeEventListener('popstate', handler);
  }, []);

  // Fetch models from API — re-fetch when tab changes (different mode)
  const tabToMode = { ref: 'r2v', frames: 'i2v', text: 't2v' };
  const [videoModels, setVideoModels] = React.useState([]);
  const [videoModel, setVideoModel] = usePersistentState('yt_vid_model', '');
  const tplModelRef = React.useRef(''); // model from template prefill, preserved across model fetch

  React.useEffect(() => {
    const mode = tabToMode[tab] || '';
    fetch('/api/v1/ai-models?media_type=video' + (mode ? '&mode=' + mode : '')).then(r => r.json()).then(d => {
      if (d.items && d.items.length > 0) {
        setVideoModels(d.items);
        const prefill = tplModelRef.current;
        if (prefill) {
          if (d.items.some(m => m.id === prefill)) {
            setVideoModel(prefill);
            tplModelRef.current = '';
          } else {
            // Model not in this mode's list — fetch all models to find it
            fetch('/api/v1/ai-models?media_type=video').then(r => r.json()).then(all => {
              const allItems = all.items || [];
              const merged = [...d.items];
              const existIds = new Set(d.items.map(m => m.id));
              allItems.forEach(m => { if (!existIds.has(m.id)) merged.push(m); });
              setVideoModels(merged);
              if (allItems.some(m => m.id === prefill)) {
                setVideoModel(prefill);
              } else if (d.items.length > 0) {
                setVideoModel(d.items[0].id);
              }
              tplModelRef.current = '';
            });
          }
        } else if (d.items.length > 0) {
          // 只有当前未选或选的型号不在此 tab 的列表里才设默认值
          setVideoModel(prev => prev && d.items.some(m => m.id === prev) ? prev : d.items[0].id);
        }
      } else {
        setVideoModels([]);
        setVideoModel('');
      }
    });
  }, [tab]);

  // Shared params
  const [duration, setDuration] = usePersistentState('yt_vid_duration', 7);
  const [resolution, setResolution] = usePersistentState('yt_vid_resolution', '1080p');
  const [aspect, setAspect] = usePersistentState('yt_vid_aspect', '16:9');
  const [count, setCount] = usePersistentState('yt_vid_count', 1);
  const [audioOn, setAudioOn] = usePersistentState('yt_vid_audio_on', true);
  const [audioRefs, setAudioRefs] = usePersistentState('yt_vid_audio_refs', []);
  const [videoRefs, setVideoRefs] = usePersistentState('yt_vid_video_refs', []);
  const [audioPickerOpen, setAudioPickerOpen] = React.useState(false);
  const [videoRefPickerOpen, setVideoRefPickerOpen] = React.useState(false);
  const [generating, setGenerating] = React.useState(false);
  const [genError, setGenError] = React.useState('');
  const [tasks, setTasks] = React.useState([]);
  const [taskFilter, setTaskFilter] = React.useState('全部');
  const [taskFilterOpen, setTaskFilterOpen] = React.useState(false);
  const [folderPickerOpen, setFolderPickerOpen] = React.useState(false);

  // Tab-specific state
  const [refImages, setRefImages] = usePersistentState('yt_vid_refs', []);
  const [startFrame, setStartFrame] = usePersistentState('yt_vid_start_frame', null);
  const [endFrame, setEndFrame] = usePersistentState('yt_vid_end_frame', null);
  const [prompt, setPrompt] = usePersistentState('yt_vid_prompt', '');

  // 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;
    fetch('/api/v1/templates/' + tplId).then(r => r.json()).then(data => {
      if (!data || data.error) return;
      // Use task data if available (complete), fallback to template fields
      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 || data.image_list || [];
      const taskType = task?.type || data.task_type || '';

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

      // 覆盖 localStorage 草稿，避免旧数据残留
      setPrompt(prompt_);
      if (model_) { tplModelRef.current = model_; setVideoModel(model_); }
      if (mp.duration) setDuration(Number(mp.duration));
      if (mp.resolution) setResolution(String(mp.resolution));
      if (mp.aspect_ratio) setAspect(String(mp.aspect_ratio));
      if (mp.count) setCount(Number(mp.count));
      if (mp.audio !== undefined) setAudioOn(!!mp.audio);
      setAudioRefs(normalizeMediaRefs(mp.audio_refs, 'audio'));
      setVideoRefs(normalizeMediaRefs(mp.video_refs, 'video'));

      const urlTab = new URLSearchParams(window.location.search).get('tab') || 'ref';
      const effectiveTab = taskTypeToTab(taskType) || urlTab;
      if (effectiveTab !== urlTab) setTab(effectiveTab);

      if (effectiveTab === 'ref') {
        const refs = imgList.filter(img => img.role === 'reference_image' || !img.role);
        setRefImages(refs.length > 0
          ? refs.map((img, i) => ({ id: 'tpl_img_' + i, url: img.image_url, name: img.role || (t('creation.image') + (i + 1)) }))
          : []);
      } else if (effectiveTab === 'frames') {
        const first = imgList.find(img => img.role === 'first_frame');
        const last = imgList.find(img => img.role === 'last_frame');
        setStartFrame(first ? { url: first.image_url, name: t('video.startFrame') } : null);
        setEndFrame(last ? { url: last.image_url, name: t('video.endFrame') } : null);
      }

      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 real tasks
  const fetchTasks = React.useCallback(() => {
    const typeParam = taskFilter === '全部' ? '' : taskFilter;
    _videoTaskAPI.list(typeParam).then(res => { if (res.items) setTasks(res.items); });
  }, [taskFilter]);
  React.useEffect(() => { fetchTasks(); }, [fetchTasks]);
  // Poll running tasks every 5s
  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 addRefImage = img => setRefImages(prev => [...prev, img]);
  const removeRefImage = id => setRefImages(prev => prev.filter(i => i.id !== id));

  const activeModel = videoModels.find(m => m.id === videoModel) || videoModels[0];
  const credits = Math.round((activeModel?.credits || 12) * (duration / 4)) * count;

  const handleGenerateClick = () => {
    if (tab !== 'frames' && !prompt.trim()) { setGenError(t('video.enterPrompt')); return; }
    if (tab === 'ref' && refImages.length === 0 && videoRefs.length === 0) { setGenError(t('video.addRefImage')); return; }
    if (tab === 'frames' && !startFrame) { setGenError(t('video.uploadStartFrame')); return; }
    if (!window.YiteAPI || !window.YiteAPI.isLoggedIn()) { showLoginModal(); return; }
    setGenError('');
    setFolderPickerOpen(true);
  };

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

    let body;
    if (repeatTask) {
      // Repeat: clone task params directly
      body = {
        type: repeatTask.type,
        prompt: repeatTask.prompt,
        model: repeatTask.model,
        model_param: repeatTask.model_param,
        image_list: repeatTask.image_list || [],
        folder_id: folder.id,
      };
      setRepeatTask(null);
    } else {
      // Normal generate from form
      const typeLabel = tab === 'ref' ? '参考生视频' : tab === 'frames' ? '开始结束帧' : '文生视频';
      let imageList = [];
      let currentImages = [];
      if (tab === 'ref') {
        imageList = refImages.filter(img => img.url).map(img => ({ image_url: img.url, role: 'reference_image' }));
        currentImages = refImages;
      } else if (tab === 'frames') {
        if (startFrame?.url) imageList.push({ image_url: startFrame.url, role: 'first_frame' });
        if (endFrame?.url) imageList.push({ image_url: endFrame.url, role: 'last_frame' });
        currentImages = [startFrame, endFrame].filter(Boolean).map((f, i) => ({ id: 'frame' + i, name: i === 0 ? t('video.startFrame') : t('video.endFrame'), url: f.url }));
      }
      const finalPrompt = buildFinalPrompt(prompt.trim() || (tab === 'frames' ? t('video.defaultTransition') : ''), currentImages);
      body = {
        type: typeLabel,
        prompt: finalPrompt,
        model: videoModel || activeModel?.id || '',
        model_param: { duration, resolution, aspect_ratio: aspect, audio: audioOn, count, audio_refs: audioRefs.length > 0 ? audioRefs : undefined, video_refs: videoRefs.length > 0 ? videoRefs : undefined },
        image_list: imageList,
        folder_id: folder.id,
      };
    }

    try {
      const res = await _videoTaskAPI.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 _videoTaskAPI.remove(id);
    fetchTasks();
  };

  // Repeat: open folder picker with task pre-loaded
  const [repeatTask, setRepeatTask] = React.useState(null);
  const handleRepeatTask = task => {
    setRepeatTask(task);
    setFolderPickerOpen(true);
  };

  // Edit: pre-fill all form state from task
  const handleEditTask = task => {
    const mp = task.model_param || {};
    const imgList = task.image_list || [];
    const taskType = task.type || '';

    if (task.prompt) setPrompt(task.prompt);
    if (task.model) setVideoModel(task.model);
    if (mp.duration) setDuration(Number(mp.duration));
    if (mp.resolution) setResolution(String(mp.resolution));
    if (mp.aspect_ratio) setAspect(String(mp.aspect_ratio));
    if (mp.audio !== undefined) setAudioOn(!!mp.audio);
    if (Array.isArray(mp.audio_refs)) setAudioRefs(mp.audio_refs);
    if (Array.isArray(mp.video_refs)) setVideoRefs(mp.video_refs);

    if (taskType === '开始结束帧') {
      setTab('frames');
      const first = imgList.find(img => img.role === 'first_frame');
      const last = imgList.find(img => img.role === 'last_frame');
      if (first) setStartFrame({ url: first.image_url, name: t('video.startFrame') });
      if (last) setEndFrame({ url: last.image_url, name: t('video.endFrame') });
    } else if (taskType === '文生视频') {
      setTab('text');
    } else {
      setTab('ref');
      const refs = imgList.filter(img => img.role === 'reference_image' || !img.role);
      if (refs.length > 0) setRefImages(refs.map((img, i) => ({ id: 'edit_' + i, url: img.image_url, name: t('creation.image') + (i + 1) })));
    }

    if (isCompact) setMobileView('config');
    window.scrollTo({ top: 0, behavior: 'smooth' });
  };

  // Publish flow: show modal → get title → extract cover → publish
  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 = '';
    // Cover priority: 1) custom upload 2) first_frame image from task 3) extract video first frame
    if (customCoverFile) {
      const coverRes = await _videoTaskAPI.uploadCover(customCoverFile);
      if (coverRes.oss_key) coverOssKey = coverRes.oss_key;
    } else if (task.media_type === 'video') {
      // Check if task has a first_frame image — use it directly as cover (no extraction needed)
      const firstFrame = (task.image_list || []).find(img => img.role === 'first_frame');
      if (firstFrame && firstFrame.image_url) {
        // first_frame is already an image URL, upload it as cover
        try {
          const resp = await fetch(firstFrame.image_url);
          const blob = await resp.blob();
          const coverRes = await _videoTaskAPI.uploadCover(new File([blob], 'cover.png', { type: blob.type || 'image/png' }));
          if (coverRes.oss_key) coverOssKey = coverRes.oss_key;
        } catch (e) { console.warn('use first_frame as cover failed:', e); }
      } else if (task.results && task.results[0]) {
        // Extract first frame from video
        try {
          const blob = await new Promise((resolve, reject) => {
            const video = document.createElement('video');
            video.crossOrigin = 'anonymous';
            video.src = task.results[0];
            video.onloadeddata = () => { video.currentTime = 0.1; };
            video.onseeked = () => {
              const canvas = document.createElement('canvas');
              canvas.width = video.videoWidth; canvas.height = video.videoHeight;
              canvas.getContext('2d').drawImage(video, 0, 0);
              canvas.toBlob(b => b ? resolve(b) : reject('toBlob failed'), 'image/png');
            };
            video.onerror = () => reject('video load failed');
          });
          const coverRes = await _videoTaskAPI.uploadCover(new File([blob], 'cover.png', { type: 'image/png' }));
          if (coverRes.oss_key) coverOssKey = coverRes.oss_key;
        } catch (e) { console.warn('extract first frame failed:', e); }
      }
    }
    const res = await _videoTaskAPI.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 _videoTaskAPI.unpublishTemplate(task.template_id);
    if (res.error) { ytAlert(res.error); return; }
    fetchTasks();
  };

  const handleAddAudioRef = () => setAudioPickerOpen(true);
  const handleRemoveAudioRef = i => setAudioRefs(prev => prev.filter((_, idx) => idx !== i));
  const paramsProps = { videoModel, setVideoModel, videoModels, activeModel, duration, setDuration, resolution, setResolution, aspect, setAspect, count, setCount, audioOn, setAudioOn, audioRefs, onAddAudioRef: handleAddAudioRef, onRemoveAudioRef: handleRemoveAudioRef, credits };



  const runningCount = tasks.filter(t => t.status === 'pending' || t.status === 'running').length;

  return (
    <AppShell active="video">
        {/* Work area */}
        <div style={{
          height: '100%',
          display: isCompact ? 'flex' : 'grid',
          flexDirection: isCompact ? 'column' : undefined,
          gridTemplateColumns: isCompact ? undefined : (sidebarW + ' 1fr'),
          overflow: isCompact ? 'auto' : 'hidden',
        }}>

          {/* ② Operation area */}
          <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 }}>
              {VIDEO_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 ' + VIDEO_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 ? VIDEO_ACCENT : 'var(--fg-3)'} />
                    <span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{t(tb.labelKey)}</span>
                  </button>
                );
              })}
            </div>

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

              {tab === 'ref' && (
                <div>
                  <ImageUploadStrip images={refImages} onAdd={addRefImage} onRemove={removeRefImage} size={thumbSize} />
                  <VideoRefsRow refs={videoRefs} onAdd={() => setVideoRefPickerOpen(true)} onRemove={i => setVideoRefs(prev => prev.filter((_, idx) => idx !== i))} />
                  <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>
                    <PromptTextarea value={prompt} onChange={setPrompt} images={refImages} placeholder={t('video.promptPlaceholder')} minHeight={promptH} />
                  </div>
                </div>
              )}

              {tab === 'frames' && (
                <div>
                  <div style={{ display: 'flex', gap: 12, marginBottom: 16 }}>
                    <FramePicker label={t('video.startFrame')} frame={startFrame} onSet={setStartFrame} onClear={() => setStartFrame(null)} />
                    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
                      <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3 }}>
                        <div style={{ width: 28, height: 28, borderRadius: '50%', background: 'rgba(34,211,238,0.12)', border: '1px solid rgba(34,211,238,0.25)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                          <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={VIDEO_ACCENT} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 15 12 9 6" /></svg>
                        </div>
                        <span style={{ fontSize: 8, color: VIDEO_ACCENT, fontWeight: 600 }}>AI</span>
                      </div>
                    </div>
                    <FramePicker label={t('video.endFrame')} frame={endFrame} onSet={setEndFrame} onClear={() => setEndFrame(null)} />
                  </div>
                  <div style={{ marginBottom: 16 }}>
                    <div style={{ fontSize: 14, color: 'var(--fg-0)', fontWeight: 500, marginBottom: 8 }}>Prompt ({t('video.promptOptional').toLowerCase()})</div>
                    <PromptTextarea value={prompt} onChange={setPrompt}
                      images={[startFrame, endFrame].filter(Boolean).map((f, i) => ({ id: 'frame' + i, name: i === 0 ? t('video.startFrame') : t('video.endFrame'), url: f.url }))}
                      placeholder={t('video.transitionPlaceholder')} minHeight={promptH} />
                  </div>
                </div>
              )}

              {tab === 'text' && (
                <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>
                  <textarea value={prompt} onChange={e => setPrompt(e.target.value)} placeholder={t('video.enterPrompt')}
                    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' }} />
              <VideoParams {...paramsProps} />
            </div>

            {/* Create button — pinned at bottom */}
            <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('status.generating')}</span>
                  : <>{t('creation.create')} <span style={{ opacity: 0.7, fontSize: 12, fontWeight: 500 }}>· {credits} {t('common.credits')}</span></>}
              </button>
            </div>
          </aside>

          {/* ④ Results area */}
          <main style={{ overflowY: isCompact ? 'visible' : 'auto', padding: isCompact ? '20px 16px 24px' : '24px 32px', display: 'block' }}>
            <div style={{ display: 'flex', flexDirection: 'column', width: '100%', maxWidth: isCompact ? '100%' : (w >= 2560 ? 960 : 720), margin: '0 auto' }}>
            {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>
            )}
            <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)' }}>{runningCount}</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' }}>
                  {t('creation.taskType')}
                  <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)' }}>
                    {VIDEO_TASK_FILTER_KEYS.map(f => (
                      <button key={f.label} onClick={() => { setTaskFilter(f.label); setTaskFilterOpen(false); }} style={{ width: '100%', textAlign: 'left', padding: '7px 11px', borderRadius: 6, border: 'none', background: taskFilter === f.label ? VIDEO_ACCENT + '18' : 'transparent', color: 'var(--fg-1)', fontSize: 13, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                        {t(f.key)} {taskFilter === f.label && <span style={{ color: VIDEO_ACCENT }}>✓</span>}
                      </button>
                    ))}
                  </div>
                )}
              </div>
            </div>

            {tasks.length > 0 ? (
              <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
                {tasks.map(t => (
                  <VideoTaskCard key={t.id} task={t} 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, paddingTop: 60 }}>
                <div style={{ width: 80, height: 80, borderRadius: '50%', background: 'rgba(255,255,255,0.02)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                  <Icon name="video" size={36} color="var(--fg-3)" />
                </div>
                <div style={{ fontSize: 13, color: 'var(--fg-3)' }}>{t('video.emptyFirst')}</div>
              </div>
            )}
            </div>
          </main>

        </div>{/* end work area grid */}
      {folderPickerOpen && <FolderPickerModal onSelect={handleFolderSelected} onClose={() => setFolderPickerOpen(false)} />}
      {publishingTask && <PublishModal onConfirm={handlePublishConfirm} onCancel={() => setPublishingTask(null)} />}
      {audioPickerOpen && ReactDOM.createPortal(
        <AssetPickerModal tag="audio" max={3} existing={audioRefs.length}
          onSelect={picked => setAudioRefs(prev => [...prev, ...picked].slice(0, 3))}
          onClose={() => setAudioPickerOpen(false)} />,
        document.body
      )}
      {videoRefPickerOpen && ReactDOM.createPortal(
        <AssetPickerModal tag="video" max={3} existing={videoRefs.length}
          onSelect={picked => setVideoRefs(prev => [...prev, ...picked].slice(0, 3))}
          onClose={() => setVideoRefPickerOpen(false)} />,
        document.body
      )}
    </AppShell>
  );
};

Object.assign(window, { VideoToolScreen });
