// Library.jsx — study materials library + Generate flow modal

function LibraryScreen({ query, materials, openMaterial, openGenerate, onDeleteMaterial, toast }) {
  const [filter, setFilter] = React.useState('all');
  const [sort, setSort] = React.useState('recent');
  const [localQ, setLocalQ] = React.useState('');
  const [confirmDel, setConfirmDel] = React.useState(null); // material pending delete confirmation
  const q = (localQ || query || '').toLowerCase();
  let list = materials.filter((m) =>
  (filter === 'all' || m.type === filter) && (
  m.title.toLowerCase().includes(q) || (topicById(m.topic)?.name || '').toLowerCase().includes(q))
  );
  if (sort === 'cards') list = [...list].sort((a, b) => (b.cards || 0) - (a.cards || 0));

  const counts = { all: materials.length, summary: 0, map: 0, deck: 0 };
  materials.forEach((m) => counts[m.type]++);

  return (
    <div className="content-pad fade-up">
      <div className="page-head">
        <div>
          <p className="eyebrow">Generated from your notes</p>
          <h1>Study Materials</h1>
          <p className="sub">{materials.length} materials across {DOMAINS.length} domains</p>
        </div>
        <button className="btn blue" onClick={() => openGenerate()}><Icon name="sparkles" size={16} /> Generate new</button>
      </div>

      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 14, marginBottom: 22, flexWrap: 'wrap' }}>
        <div className="segmented">
          {[['all', 'All'], ['summary', 'Summaries'], ['map', 'Concept maps'], ['deck', 'Flashcards']].map(([id, lab]) =>
          <button key={id} className={filter === id ? 'on' : ''} onClick={() => setFilter(id)}>
              {lab} <span style={{ opacity: .6, fontFamily: 'var(--font-mono)', fontSize: 11 }}>{counts[id]}</span>
            </button>
          )}
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
          <div style={{ position: 'relative' }}>
            <Icon name="search" size={15} style={{ position: 'absolute', left: 11, top: '50%', transform: 'translateY(-50%)', color: 'var(--ink-4)', pointerEvents: 'none' }} />
            <input className="input" value={localQ} onChange={(e) => setLocalQ(e.target.value)} placeholder="Search materials…"
              style={{ paddingLeft: 32, paddingRight: localQ ? 30 : 12, height: 36, width: 210 }} />
            {localQ && <button className="icon-btn" onClick={() => setLocalQ('')} title="Clear search"
              style={{ position: 'absolute', right: 4, top: '50%', transform: 'translateY(-50%)', width: 24, height: 24, border: 'none' }}><Icon name="x" size={14} /></button>}
          </div>
          <button className="chip" onClick={() => setSort((s) => s === 'recent' ? 'cards' : 'recent')}>
            <Icon name="filter" size={14} /> Sort: {sort === 'recent' ? 'Recent' : 'Most cards'}
          </button>
        </div>
      </div>

      {list.length === 0 ?
      (q ?
      <EmptyHint icon="search" title={'No materials match “' + (localQ || query) + '”'}
      sub="Try a different keyword, or clear the search to see everything." /> :
      <EmptyHint icon="cards" title="Nothing here yet"
      sub="Generate a summary, concept map, or flashcard deck from your connected notes to get started."
      action={<button className="btn blue" onClick={() => openGenerate()}><Icon name="sparkles" size={16} /> Generate new</button>} />) :

      <div className="grid cards-4">
          {list.map((m) => <MaterialCard key={m.id} mat={m} onOpen={() => openMaterial(m.id)} onDelete={onDeleteMaterial ? setConfirmDel : undefined} />)}
        </div>
      }

      {confirmDel &&
      <div className="scrim" onMouseDown={(e) => { if (e.target === e.currentTarget) setConfirmDel(null); }}>
        <div className="modal" style={{ maxWidth: 400 }}>
          <div className="mhead">
            <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
              <span className="itile" style={{ width: 32, height: 32, borderRadius: 9, background: 'color-mix(in srgb,var(--critical) 14%, var(--paper))', color: 'var(--critical)' }}><Icon name="trash" size={17} /></span>
              <span style={{ fontWeight: 600, fontSize: 16 }}>Delete material</span>
            </div>
            <button className="icon-btn" style={{ width: 32, height: 32, border: 'none' }} onClick={() => setConfirmDel(null)}><Icon name="x" size={18} /></button>
          </div>
          <div className="mbody">
            <p className="t-body" style={{ fontSize: 14, lineHeight: 1.6, margin: 0 }}>
              Delete <strong>{confirmDel.title}</strong>? This permanently removes it from your library. This can’t be undone.
            </p>
          </div>
          <div className="mfoot">
            <button className="btn" onClick={() => setConfirmDel(null)}>Cancel</button>
            <button className="btn" style={{ background: 'var(--critical)', color: '#fff', borderColor: 'var(--critical)' }}
              onClick={() => { const nm = confirmDel.title; onDeleteMaterial(confirmDel.id); setConfirmDel(null); if (toast) toast('Deleted “' + nm + '”'); }}>
              <Icon name="trash" size={15} /> Delete material</button>
          </div>
        </div>
      </div>
      }
    </div>);

}

function GenerateModal({ topicId, presetType, presetName, replaceId, materials, onClose, onDone, onCommit, onAddTopic, toast }) {
  const [topic, setTopic] = React.useState(topicId || null);
  const [types, setTypes] = React.useState(() => [presetType || 'deck']); // selected material types (multi-select)
  const [phase, setPhase] = React.useState('config'); // config | generating | done
  const [step, setStep] = React.useState(0);
  const [tq, setTq] = React.useState(topicId ? '' : (presetName || '')); // topic search query (pre-seeded from a goal)
  const [file, setFile] = React.useState(null);
  const [rec, setRec] = React.useState(null); // recommended topic id from file
  const [newName, setNewName] = React.useState(null); // pending NEW topic (from an uploaded file or typed name)
  const [fileText, setFileText] = React.useState(''); // extracted text of the uploaded file, fed to the AI
  const fileRef = React.useRef(null);
  const genPromiseRef = React.useRef(null); // resolves when real AI generation finishes
  const genMatsRef = React.useRef([]); // the material(s) being generated this run
  const [genCounts, setGenCounts] = React.useState({}); // per-type count the AI actually produced
  const fixed = !!topicId; // opened from a specific topic
  const t = newName != null ? { name: (newName.trim() || 'New topic'), notes: 0, domain: (DOMAINS[0] || {}).id }
    : topic ? topicById(topic) : null;
  // Turn a file name into a clean, Title-Cased topic name.
  const niceName = (fname) => {
    const base = (fname || '').replace(/\.[^.]+$/, '').replace(/[-_]+/g, ' ').replace(/\b[0-9a-f]{6,}\b/gi, ' ').replace(/\s+/g, ' ').trim();
    return base ? base.replace(/\b\w/g, (c) => c.toUpperCase()) : 'New topic';
  };
  // Suggest 1-3 topic names from the material (a goal title or file name): the
  // full cleaned title, plus a concise key-term version. The user can pick one
  // or ignore them and type their own.
  const recommendedNames = (seed) => {
    const s = (seed || '').trim();
    if (!s) return [];
    const out = [];
    const full = niceName(s);
    if (full && full !== 'New topic') out.push(full);
    const stop = new Set(['how', 'why', 'what', 'when', 'where', 'the', 'a', 'an', 'of', 'to', 'and', 'for', 'in', 'on', 'is', 'are', 'your', 'you', 'its', 'it', 'into', 'from', 'with', 'that', 'about', 'do', 'does']);
    const core = s.replace(/[^\w\s]/g, ' ').split(/\s+/).filter((w) => w && !stop.has(w.toLowerCase()));
    if (core.length) {
      const concise = core.slice(0, 3).map((w) => w.length <= 3 ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
      if (concise && !out.some((x) => x.toLowerCase() === concise.toLowerCase())) out.push(concise);
    }
    return out.slice(0, 3);
  };
  // Gather the real source text for a topic: the user's own workspace notes
  // (typed or dictated) plus any linked notes for THIS exact topic — so the
  // generated materials reflect the content the user actually put here, not a
  // generic fallback deck.
  const gatherTopicText = (tid) => {
    if (!tid) return '';
    const parts = [];
    try { JSON.parse(localStorage.getItem('pl-topic-notes-' + tid) || '[]').forEach((n) => { if (n && n.text) parts.push(n.text); }); } catch (e) {}
    try { JSON.parse(localStorage.getItem('pl-topic-files-' + tid) || '[]').forEach((f) => { if (f && f.text && f.text.trim()) parts.push('From ' + (f.name || 'a file') + ':\n' + f.text.trim()); else if (f && f.name) parts.push('Attached resource: ' + f.name); }); } catch (e) {}
    try { const linked = (window.NOTES || {})[tid]; if (Array.isArray(linked)) linked.forEach((n) => { const s = [n.title, n.excerpt || n.body].filter(Boolean).join(': '); if (s) parts.push(s); }); } catch (e) {}
    return parts.join('\n\n').trim();
  };
  // Rank topics by how well they match a query (name + domain).
  const matches = React.useMemo(() => {
    const q = tq.trim().toLowerCase();
    if (!q) return [];
    return TOPICS.filter((tp) => tp.name.toLowerCase().includes(q) || (domainById(tp.domain) || {}).name?.toLowerCase().includes(q)).slice(0, 8);
  }, [tq]);
  // Recommended new-topic names, sourced from the material (goal title / file)
  // or, failing that, whatever the user is typing.
  const recs = React.useMemo(() => recommendedNames(presetName || (file ? niceName(file.name) : '') || tq), [presetName, file, tq]);
  // Recommend the best-matching topic for an uploaded file by token overlap.
  const recommendFor = (fname) => {
    const base = (fname || '').replace(/\.[^.]+$/, '').replace(/[-_]+/g, ' ').replace(/\b[0-9a-f]{6,}\b/gi, ' ').toLowerCase();
    const toks = base.split(/\s+/).filter((w) => w.length > 2);
    let best = null, bestScore = 0;
    TOPICS.forEach((tp) => {
      const hay = (tp.name + ' ' + (domainById(tp.domain) || {}).name).toLowerCase();
      let s = 0; toks.forEach((w) => { if (hay.includes(w)) s += 1; });
      if (s > bestScore) { bestScore = s; best = tp.id; }
    });
    return bestScore > 0 ? best : null;
  };
  const srcLabel = file ? 'your file' : 'your linked notes';
  const matLabels = types.map((ty) => MAT_TYPE[ty].label.toLowerCase());
  const matList = matLabels.length === 1 ? matLabels[0] : matLabels.length === 2 ? matLabels.join(' & ') : matLabels.slice(0, -1).join(', ') + ' & ' + matLabels[matLabels.length - 1];
  const GEN_STEPS = ['Reading ' + srcLabel + '…', 'Pulling out the key concepts & specific details…', 'Writing your ' + matList + '…', 'Finalizing — almost ready…'];

  React.useEffect(() => {
    if (phase !== 'generating') return;
    setStep(0);
    const timers = [setTimeout(() => setStep(1), 700), setTimeout(() => setStep(2), 1500), setTimeout(() => setStep(3), 3200)];
    let cancelled = false;
    // Hold on the generating screen until the REAL AI call finishes for every
    // selected material — no matter how long it takes. The last step keeps
    // spinning ("Finalizing") so the user never sees "done" before the content
    // they'll review is actually ready. A short min-wait only smooths the intro.
    const minWait = new Promise((res) => setTimeout(res, 1400));
    Promise.all([genPromiseRef.current || Promise.resolve(), minWait]).then(() => {
      if (cancelled) return;
      timers.forEach(clearTimeout);
      setStep(GEN_STEPS.length); // mark all steps complete
      // Commit the generated materials to the Topic & Library pages immediately,
      // so they persist even if the user closes this modal without opening them.
      if (onCommit) onCommit(genMatsRef.current);
      if (toast && genMatsRef.current.length) toast(genMatsRef.current.length > 1 ? genMatsRef.current.length + ' materials added to your library' : 'Material added to your library');
      setPhase('done');
    });
    return () => {cancelled = true;timers.forEach(clearTimeout);};
  }, [phase]);

  const titleForType = (ty, name) => ({
    deck: `${name} — quick recall`, summary: `${name}, summarized`, map: `${name} concept map`
  })[ty];

  const finish = () => {
    const mats = genMatsRef.current || [];
    if (mats.length) onDone(mats, mats[0]);
  };

  return (
    <div className="scrim" onMouseDown={(e) => {if (e.target === e.currentTarget && phase !== 'generating') onClose();}}>
      <div className="modal" style={{ maxWidth: 560 }}>
        <div className="mhead">
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <span className="itile" style={{ width: 32, height: 32, borderRadius: 9, background: 'var(--sky)', color: 'var(--blue-ink)' }}><Icon name="sparkles" size={17} /></span>
            <span style={{ fontWeight: 600, fontSize: 16 }}>Generate study material</span>
          </div>
          {phase !== 'generating' && <button className="icon-btn" style={{ width: 32, height: 32, border: 'none' }} onClick={onClose}><Icon name="x" size={18} /></button>}
        </div>

        {phase === 'config' &&
        <React.Fragment>
            <div className="mbody">
              {!fixed &&
              <React.Fragment>
              <label className="field-label">Topic</label>
              <div className="t-small" style={{ marginTop: -2, marginBottom: 10 }}>Upload a file and we’ll recommend the right topic — or search for one by name.</div>
              <input type="file" ref={fileRef} style={{ display: 'none' }} accept=".pdf,.doc,.docx,.txt,.md,.png,.jpg,.jpeg"
                onChange={(e) => {const f = e.target.files && e.target.files[0];if (f) {setFile(f);const r = recommendFor(f.name);setRec(r);if (r) {setTopic(r);setNewName(null);} else {setNewName(niceName(f.name));setTopic(null);}setFileText('');extractFileText(f).then((txt) => setFileText(txt || '')).catch(() => setFileText(''));}}} />
              {!file ?
              <button type="button" onClick={() => fileRef.current && fileRef.current.click()}
                style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 11, padding: '12px 14px', borderRadius: 12, border: '1.5px dashed var(--line-strong)', background: 'var(--surface-2)', cursor: 'pointer', color: 'var(--ink-3)', marginBottom: 12 }}>
                <Icon name="download" size={18} style={{ transform: 'rotate(180deg)', color: 'var(--blue-ink)', flex: 'none' }} />
                <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink-2)' }}>Upload a file to get a recommendation</span>
              </button> :
              <React.Fragment>
              <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 12px', borderRadius: 12, border: '1px solid var(--line)', background: 'var(--surface-2)', marginBottom: 10 }}>
                <span className="itile" style={{ width: 30, height: 30, borderRadius: 8, background: 'var(--paper)', color: 'var(--blue-ink)', flex: 'none' }}><Icon name="book" size={16} /></span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 13, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{file.name}</div>
                  <div className="mono" style={{ fontSize: 11, color: rec ? 'var(--positive)' : 'var(--blue-ink)' }}>{rec ? 'Recommended topic: ' + topicById(rec).name : 'Will create a new topic from this file'}</div>
                </div>
                <button className="icon-btn" style={{ width: 28, height: 28, border: 'none', flex: 'none' }} onClick={() => {setFile(null);setRec(null);setNewName(null);setFileText('');if (fileRef.current) fileRef.current.value = '';}}><Icon name="x" size={15} /></button>
              </div>
              <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 12 }}>
                <button className={'chip' + (newName != null ? ' on' : '')} onClick={() => {setNewName(niceName(file.name));setTopic(null);}}><Icon name="plus" size={13} /> New topic from file</button>
                {rec && <button className={'chip' + (topic === rec ? ' on' : '')} onClick={() => {setTopic(rec);setNewName(null);}}><span style={{ width: 7, height: 7, borderRadius: 99, background: (domainById(topicById(rec).domain) || {}).accent }} /> Use {topicById(rec).name}</button>}
              </div>
              </React.Fragment>
              }
              <div style={{ position: 'relative', marginBottom: recs.length || matches.length || tq ? 10 : 18 }}>
                <Icon name="search" size={15} style={{ position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)', color: 'var(--ink-4)' }} />
                <input className="input" value={tq} onChange={(e) => setTq(e.target.value)} placeholder="Search or type a topic name…" style={{ paddingLeft: 34 }} />
              </div>
              {recs.length > 0 && !topic &&
              <div style={{ marginBottom: 16 }}>
                <div className="mono" style={{ fontSize: 10.5, fontWeight: 600, color: 'var(--ink-3)', textTransform: 'uppercase', letterSpacing: '.05em', marginBottom: 7 }}>Recommended names</div>
                <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
                  {recs.map((nmRec) =>
                  <button key={nmRec} className={'chip' + (newName != null && newName.trim().toLowerCase() === nmRec.toLowerCase() ? ' on' : '')} onClick={() => {setNewName(nmRec);setTopic(null);}}><Icon name="sparkles" size={12} /> {nmRec}</button>
                  )}
                </div>
              </div>
              }
              {tq.trim() &&
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 18 }}>
                {matches.length ? matches.map((tp) =>
                <button key={tp.id} className={'chip' + (topic === tp.id ? ' on' : '')} onClick={() => {setTopic(tp.id);setNewName(null);}}>
                    <span style={{ width: 7, height: 7, borderRadius: 99, background: (domainById(tp.domain) || {}).accent }} /> {tp.name}
                  </button>
                ) : <button className={'chip' + (newName != null ? ' on' : '')} onClick={() => {setNewName(tq.trim());setTopic(null);}}><Icon name="plus" size={13} /> Create new topic “{tq.trim()}”</button>}
              </div>
              }
              {(topic || newName != null) &&
              <div style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '9px 12px', borderRadius: 10, background: 'color-mix(in srgb,var(--blue) 8%, var(--paper))', border: '1px solid color-mix(in srgb,var(--blue) 22%, var(--line))', marginBottom: 18 }}>
                <Icon name="check" size={15} style={{ color: 'var(--blue-ink)', flex: 'none' }} />
                <span style={{ fontSize: 13, color: 'var(--ink-2)' }}>{newName != null ? 'New topic: ' : 'Selected topic: '}<strong>{t.name}</strong></span>
                {newName != null && <span className="tag" style={{ background: 'color-mix(in srgb,var(--blue) 16%, transparent)', color: 'var(--blue-ink)', fontSize: 10.5, padding: '2px 7px' }}>NEW</span>}
                <button className="icon-btn" style={{ width: 24, height: 24, border: 'none', marginLeft: 'auto', flex: 'none' }} onClick={() => {setTopic(null);setNewName(null);}}><Icon name="x" size={14} /></button>
              </div>
              }
              </React.Fragment>
              }
              <label className="field-label">Material type <span style={{ fontWeight: 400, color: 'var(--ink-3)' }}>· pick one or more</span></label>
              <div className="grid cards-3" style={{ gap: 10 }}>
                {['summary', 'map', 'deck'].map((ty) => {
                const m = MAT_TYPE[ty];const on = types.includes(ty);
                return (
                  <button key={ty} onClick={() => setTypes((s) => s.includes(ty) ? s.filter((x) => x !== ty) : [...s, ty])} className="card tight"
                  style={{ position: 'relative', display: 'flex', flexDirection: 'column', gap: 9, alignItems: 'flex-start', cursor: 'pointer',
                    border: on ? '1.5px solid var(--blue)' : '1px solid var(--line)', boxShadow: on ? '0 0 0 3px rgba(30,143,240,.14)' : 'none' }}>
                      <MatTile type={ty} size={34} />
                      <span style={{ fontSize: 13.5, fontWeight: 600 }}>{m.label}</span>
                      <span aria-hidden="true" style={{ position: 'absolute', top: 9, right: 9, width: 18, height: 18, borderRadius: 99, display: 'flex', alignItems: 'center', justifyContent: 'center', border: on ? 'none' : '1.5px solid var(--line-strong)', background: on ? 'var(--blue)' : 'transparent', color: '#fff' }}>{on ? <Icon name="check" size={12} /> : null}</span>
                    </button>);

              })}
              </div>
              <div style={{ marginTop: 16, fontSize: 12.5, color: 'var(--ink-3)', display: 'flex', alignItems: 'center', gap: 7 }}>
                {newName != null ? <React.Fragment><Icon name="sparkles" size={14} /> Creates a new topic <strong style={{ color: 'var(--ink-2)', fontWeight: 600 }}>{t.name}</strong> {file ? 'from ' + file.name : 'and generates its first material'}</React.Fragment>
                  : t ? <React.Fragment><Icon name="link" size={14} /> Pulls from your notes &amp; resources in <strong style={{ color: 'var(--ink-2)', fontWeight: 600 }}>{t.name}</strong></React.Fragment>
                  : <React.Fragment><Icon name="search" size={14} /> Choose a topic above to generate from its notes.</React.Fragment>}
              </div>
            </div>
            <div className="mfoot">
              <button className="btn" onClick={onClose}>Cancel</button>
              <button className="btn blue" disabled={(!topic && !(newName != null && newName.trim())) || types.length === 0} onClick={() => {
                let topicId = topic;
                if (newName != null && newName.trim() && onAddTopic) { const nt = onAddTopic(newName.trim(), (DOMAINS[0] || {}).id); if (nt) { topicId = nt.id; setTopic(nt.id); } setNewName(null); }
                const nm = (t && t.name) || 'New topic';
                const stamp = Date.now();
                const sel = ['summary', 'map', 'deck'].filter((ty) => types.includes(ty));
                // Build a material for each selected type up front, with stable ids.
                // When regenerating, reuse the existing material's id for that type
                // so its content is overwritten in place (no duplicate entry).
                // Build a material for each selected type. Reuse the id of an
                // existing material of the same type ON THIS TOPIC (or the one being
                // regenerated) so a repeat generation refreshes it in place instead
                // of creating a duplicate — keeping one summary/map/deck per topic.
                const existingOf = (ty) => (materials || []).find((m) => m.topic === topicId && m.type === ty);
                const mats = sel.map((ty) => { const ex = (replaceId && ty === presetType) ? { id: replaceId } : existingOf(ty); return { id: ex ? ex.id : 'gen-' + stamp + '-' + ty, type: ty, topic: topicId, title: (ex && ex.title) ? ex.title : titleForType(ty, nm), updated: 'just now', ...(ty === 'deck' ? { cards: 10 } : {}) }; });
                genMatsRef.current = mats;
                setGenCounts({});
                const gen = window.genFromFile;
                // Source = uploaded file text, else the topic's own saved notes.
                // Re-extract the file at generate time so PDFs (async) are ready;
                // genFromFile falls back to on-topic generation when there's no text.
                const fileP = file ? Promise.resolve(fileText || '').then((t) => t.trim() ? t : (window.extractFileText ? window.extractFileText(file).catch(() => '') : '')) : Promise.resolve('');
                if (gen && window.claude && window.claude.complete) {
                  const picks = { summary: types.includes('summary'), map: types.includes('map'), deck: types.includes('deck') };
                  genPromiseRef.current = fileP.then((ft) => gen((ft && ft.trim()) ? ft : gatherTopicText(topicId), nm, picks)).then((json) => {
                    window.SUMMARIES = window.SUMMARIES || {};
                    window.CONCEPT_MAPS = window.CONCEPT_MAPS || {};
                    window.FLASHCARDS = window.FLASHCARDS || {};
                    const counts = {};
                    if (json) {
                      const sumMat = mats.find((m) => m.type === 'summary');
                      if (sumMat && json.summary) { const srcFiles = []; if (file && file.name) srcFiles.push(file.name); try { JSON.parse(localStorage.getItem('pl-topic-files-' + topicId) || '[]').forEach((f) => { if (f && f.name && !srcFiles.includes(f.name)) srcFiles.push(f.name); }); } catch (e) {} window.SUMMARIES[sumMat.id] = { head: json.summary.head || nm, intro: json.summary.intro || '', secs: (json.summary.secs || []).slice(0, 6), links: srcFiles.map((n) => ({ label: n, file: true })) }; counts.summary = (json.summary.secs || []).length; }
                      const mapMat = mats.find((m) => m.type === 'map');
                      if (mapMat && Array.isArray(json.map) && json.map.length) { window.CONCEPT_MAPS[topicId] = json.map.slice(0, 10).map((c) => ({ label: c.label || '', note: c.note || '' })); counts.map = Math.min(10, json.map.length); }
                      const deckMat = mats.find((m) => m.type === 'deck');
                      if (deckMat && Array.isArray(json.deck) && json.deck.length) { const d = json.deck.slice(0, 8).map((c) => ({ q: c.q || '', a: c.a || '', label: c.label || 'Card' })); window.FLASHCARDS[deckMat.id] = d; deckMat.cards = d.length; counts.deck = d.length; }
                    }
                    setGenCounts(counts);
                    try { window.dispatchEvent(new CustomEvent('pl-generated', { detail: { topic: topicId } })); } catch (e) {}
                  }).catch(() => {});
                } else { genPromiseRef.current = null; }
                setPhase('generating');
              }}><Icon name="sparkles" size={16} /> {types.length > 1 ? 'Generate ' + types.length + ' materials' : 'Generate ' + (types.length ? MAT_TYPE[types[0]].label.toLowerCase() : 'material')}</button>
            </div>
          </React.Fragment>
        }

        {phase === 'generating' &&
        <div className="mbody" style={{ padding: '34px 22px' }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 13, marginBottom: 22 }}>
              <span className="itile" style={{ width: 42, height: 42, borderRadius: 12, background: 'var(--sky)', color: 'var(--blue-ink)' }}><Icon name="sparkles" size={21} /></span>
              <div>
                <div style={{ fontSize: 15.5, fontWeight: 600 }}>{types.length > 1 ? 'Generating ' + types.length + ' materials…' : 'Generating ' + (types.length ? MAT_TYPE[types[0]].label.toLowerCase() : 'material') + '…'}</div>
                <div className="mono" style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 2 }}>{GEN_STEPS[Math.min(step, GEN_STEPS.length - 1)]}</div>
              </div>
            </div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 11 }}>
              {GEN_STEPS.map((label, i) =>
            <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 11 }}>
                  {i < step ? <Icon name="check" size={16} style={{ color: 'var(--positive)' }} /> :
              i === step ? <span style={{ width: 16, height: 16, borderRadius: 99, border: '2px solid var(--blue)', borderTopColor: 'transparent', animation: 'spin .7s linear infinite' }} /> :
              <span style={{ width: 16, height: 16, borderRadius: 99, border: '2px solid var(--line-strong)' }} />}
                  <div className={i <= step ? '' : 'gen-skel'} style={{ flex: 1, height: 13, borderRadius: 6,
                background: i <= step ? 'transparent' : undefined, fontSize: 12.5, color: 'var(--ink-2)' }}>{i <= step ? label : ''}</div>
                </div>
            )}
            </div>
            <div className="t-small" style={{ marginTop: 16, textAlign: 'center', color: 'var(--ink-3)' }}>{types.length > 1 ? 'Generating ' + types.length + ' materials can take a little longer — hang tight while we finish them all.' : 'This can take a moment — we’ll open it as soon as it’s ready to review.'}</div>
          </div>
        }

        {phase === 'done' &&
        <React.Fragment>
            <div className="mbody" style={{ textAlign: 'center', padding: '30px 22px' }}>
              <span className="itile" style={{ width: 54, height: 54, borderRadius: 15, background: 'rgba(46,122,58,.14)', color: 'var(--positive)', margin: '0 auto 16px' }}><Icon name="check" size={28} /></span>
              <div style={{ fontSize: 18, fontWeight: 600, letterSpacing: '-0.01em' }}>{genMatsRef.current.length > 1 ? genMatsRef.current.length + ' materials ready' : (genMatsRef.current[0] ? genMatsRef.current[0].title : 'Ready')}</div>
              <div style={{ fontSize: 12.5, color: 'var(--ink-3)', marginTop: 5 }}>in {t.name}</div>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 16, textAlign: 'left' }}>
                {genMatsRef.current.map((m) => {
                  const cnt = m.type === 'deck' ? (genCounts.deck || m.cards || 10) + ' flashcards' : m.type === 'map' ? (genCounts.map ? genCounts.map + ' concepts mapped' : 'Concept map') : (genCounts.summary ? genCounts.summary + ' sections' : 'Summary');
                  return (
                    <div key={m.id} style={{ display: 'flex', alignItems: 'center', gap: 11, padding: '10px 12px', borderRadius: 12, border: '1px solid var(--line)', background: 'var(--surface-2)' }}>
                      <MatTile type={m.type} size={30} />
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <div style={{ fontSize: 13.5, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{m.title}</div>
                        <div className="mono" style={{ fontSize: 11, color: 'var(--ink-3)' }}>{cnt}</div>
                      </div>
                      <button className="btn sm" onClick={() => onDone(genMatsRef.current, m)}>Open</button>
                    </div>);
                })}
              </div>
            </div>
            <div className="mfoot" style={{ justifyContent: 'space-between' }}>
              <button className="btn" onClick={onClose}>Close</button>
              <button className="btn blue" onClick={finish}>{genMatsRef.current.length > 1 ? 'Open first' : 'Open material'} <Icon name="arrow-right" size={16} /></button>
            </div>
          </React.Fragment>
        }
      </div>
    </div>);

}

Object.assign(window, { LibraryScreen, GenerateModal });