const { useState, useEffect, useRef, useCallback } = React;

const API = (location.hostname === "localhost" || location.hostname === "127.0.0.1")
  ? "http://localhost:8787"
  : "https://api.kachina.app";

const NAV = [
  ["chat", "Explorer"],
  ["library", "Bibliothèque"],
  ["favorites", "Favoris"],
  ["insights", "Insights"],
  ["activity", "Agents"],
  ["themes", "Thématiques"],
  ["tasks", "Tâches"],
  ["processes", "Processus"],
  ["keys", "Clés API"],
  ["connectors", "Connecteurs"],
  ["esprit", "Esprit"],
];

const ICONS = {
  chat: <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/><path d="M8 12h8M12 8v8"/></svg>,
  library: <svg viewBox="0 0 24 24"><path d="M4 19V5h6v14H4zM14 19V8h6v11h-6z"/></svg>,
  favorites: <svg viewBox="0 0 24 24"><path d="M12 20s-7-4.4-7-10a4 4 0 0 1 7-2 4 4 0 0 1 7 2c0 5.6-7 10-7 10z"/></svg>,
  insights: <svg viewBox="0 0 24 24"><circle cx="7" cy="12" r="2.5"/><circle cx="17" cy="8" r="2.5"/><circle cx="16" cy="17" r="2.5"/><path d="M9.2 11.2l5.2-2.4M9.4 13l5 3"/></svg>,
  activity: <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/><path d="M12 8v5l3 2"/></svg>,
  themes: <svg viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h10"/></svg>,
  tasks: <svg viewBox="0 0 24 24"><path d="M9 6h11M9 12h11M9 18h11"/><path d="M5 6l.8.8L7.6 5M5 12l.8.8L7.6 11M5 18l.8.8L7.6 17"/></svg>,
  processes: <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/><path d="M12 8v5l3 2"/></svg>,
  keys: <svg viewBox="0 0 24 24"><circle cx="8" cy="12" r="3"/><path d="M11 12h9v3"/></svg>,
  connectors: <svg viewBox="0 0 24 24"><path d="M8 8h8v8H8zM4 12h4M16 12h4"/></svg>,
  esprit: <svg viewBox="0 0 24 24"><circle cx="12" cy="9" r="4"/><path d="M6 20c1.4-3 4-4 6-4s4.6 1 6 4"/></svg>,
};

const SUGGESTS = [
  ["clock", "Synthétiser", "Résume ce que le cerveau sait déjà sur un sujet."],
  ["bulb", "Injecter un fait", "Range une info durable dans la bibliothèque."],
  ["image", "Lire un fichier", "Dépose une photo, un PDF ou un Word."],
];

const SUGGEST_ICONS = {
  clock: <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/><path d="M12 8v5l3 2"/></svg>,
  bulb: <svg viewBox="0 0 24 24"><path d="M9 18h6M10 21h4M8 10a4 4 0 1 1 8 0c0 2-1.2 3-2 4H10c-.8-1-2-2-2-4z"/></svg>,
  image: <svg viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="14" rx="2"/><circle cx="9" cy="10" r="1.5"/><path d="M7 16l4-4 3 3 3-2 3 3"/></svg>,
};

function sessionToken() {
  return localStorage.getItem("kachina_token") || "";
}

async function api(path, opts = {}) {
  const headers = { ...(opts.headers || {}) };
  if (!(opts.body instanceof FormData)) headers["content-type"] = "application/json";
  const tok = sessionToken();
  if (tok) headers.authorization = "Bearer " + tok;
  const res = await fetch(API + path, {
    credentials: "include",
    headers,
    ...opts,
    body: opts.body && typeof opts.body !== "string" && !(opts.body instanceof FormData)
      ? JSON.stringify(opts.body)
      : opts.body,
  });
  const ct = res.headers.get("content-type") || "";
  const data = ct.includes("json") ? await res.json() : await res.text();
  if (!res.ok) throw Object.assign(new Error((data && data.error) || res.statusText), { data, status: res.status });
  return data;
}

function rel(d) {
  if (!d) return "-";
  const t = new Date(String(d).includes("T") ? d : d.replace(" ", "T") + "Z").getTime();
  const s = (Date.now() - t) / 1000;
  if (s < 60) return "à l’instant";
  if (s < 3600) return `il y a ${Math.floor(s / 60)} min`;
  if (s < 86400) return `il y a ${Math.floor(s / 3600)} h`;
  return `il y a ${Math.floor(s / 86400)} j`;
}

function Login({ onIn }) {
  const [email, setEmail] = useState(localStorage.getItem("kachina_email") || "");
  const [sent, setSent] = useState(false);
  const [err, setErr] = useState("");
  const [busy, setBusy] = useState(false);
  async function submit(e) {
    e.preventDefault();
    setBusy(true); setErr("");
    try {
      await api("/v1/auth/magic", { method: "POST", body: { email: email.trim() } });
      localStorage.setItem("kachina_email", email.trim());
      setSent(true);
    } catch (ex) {
      setErr("Impossible d’envoyer le lien. Réessaie.");
    } finally { setBusy(false); }
  }
  return (
    <div className="login">
      <form className="login-card" onSubmit={submit}>
        <img className="brand-mark" src="assets/img/mark.png" alt="" />
        <h1>Kachina</h1>
        <p>Ta mémoire. Claude, ChatGPT et Cursor y puisent.</p>
        {sent ? (
          <p className="muted">Lien envoyé à <strong>{email}</strong>. Ouvre-le pour entrer.</p>
        ) : (
          <label className="field">
            <span>Email</span>
            <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} autoFocus placeholder="toi@email.ch" />
          </label>
        )}
        {err && <div className="err">{err}</div>}
        {!sent && <button className="btn" disabled={busy || !email.includes("@")}>{busy ? "Envoi…" : "Recevoir le lien"}</button>}
        {sent && <button type="button" className="btn btn-ghost" onClick={() => setSent(false)}>Autre email</button>}
      </form>
    </div>
  );
}

function isDocFile(file) {
  const name = (file.name || "").toLowerCase();
  const type = (file.type || "").toLowerCase();
  return (
    name.endsWith(".pdf") || name.endsWith(".doc") || name.endsWith(".docx") || name.endsWith(".odt") ||
    name.endsWith(".txt") || name.endsWith(".md") ||
    type.includes("pdf") || type.includes("msword") || type.includes("wordprocessing") || type.includes("opendocument.text")
  );
}

function fileError(code) {
  if (code === "file_too_large") {
    return "Fichier trop lourd. Les photos sont compressées automatiquement - réessaie. Un PDF doit faire moins de 32 Mo.";
  }
  if (code === "unsupported_type") return "Ce format n’est pas lisible. Envoie un PDF, un .docx ou une image.";
  return code || "fichier";
}

async function shrinkImage(file) {
  const isImage = (file.type || "").startsWith("image/") || /\.(jpe?g|png|heic|heif|webp|gif)$/i.test(file.name || "");
  if (!isImage) return file;
  try {
    const bitmap = await createImageBitmap(file);
    const maxEdge = 1600;
    const longest = Math.max(bitmap.width, bitmap.height);
    const scale = longest > maxEdge ? maxEdge / longest : 1;
    const canvas = document.createElement("canvas");
    canvas.width = Math.max(1, Math.round(bitmap.width * scale));
    canvas.height = Math.max(1, Math.round(bitmap.height * scale));
    canvas.getContext("2d").drawImage(bitmap, 0, 0, canvas.width, canvas.height);
    bitmap.close();
    let quality = 0.82;
    let blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg", quality));
    while (blob && blob.size > 2_400_000 && quality > 0.32) {
      quality -= 0.1;
      blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg", quality));
    }
    if (!blob) return file;
    const base = (file.name || "photo").replace(/\.[^.]+$/, "");
    return new File([blob], `${base}.jpg`, { type: "image/jpeg" });
  } catch {
    return file;
  }
}

function fileFromPaste(e) {
  const bag = e.clipboardData;
  if (!bag) return null;
  const fromItems = [...(bag.items || [])]
    .filter((item) => item.kind === "file")
    .map((item) => item.getAsFile())
    .filter(Boolean);
  const fromFiles = [...(bag.files || [])];
  const file = [...fromItems, ...fromFiles].find((f) => (f.type || "").startsWith("image/") || isDocFile(f));
  if (!file) return null;
  if (file.name && file.name !== "image.png") return file;
  const ext = (file.type || "image/png").split("/")[1] || "png";
  return new File([file], `collage.${ext}`, { type: file.type || "image/png" });
}

function AskBox({ text, setText, busy, onSend, onPickFile, compact }) {
  const fileRef = useRef(null);
  const boxRef = useRef(null);
  useEffect(() => {
    function down(e) {
      if (!boxRef.current || boxRef.current.contains(e.target)) return;
      const active = document.activeElement;
      if (active && boxRef.current.contains(active)) active.blur();
    }
    document.addEventListener("pointerdown", down);
    return () => document.removeEventListener("pointerdown", down);
  }, []);
  return (
    <form className="ask" ref={boxRef} onSubmit={onSend}>
      <textarea
        value={text}
        placeholder="Ask me anything…"
        onChange={(e) => setText(e.target.value)}
        onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) onSend(e); }}
        onPaste={(e) => {
          const file = fileFromPaste(e);
          if (!file) return;
          e.preventDefault();
          onPickFile(file);
        }}
      />
      <div className="ask-tools">
        <button type="button" className="btn btn-lavender" onClick={() => fileRef.current && fileRef.current.click()}>Joindre un fichier</button>
        <button type="button" className="icon-btn" title="Joindre" onClick={() => fileRef.current && fileRef.current.click()}>
          <svg viewBox="0 0 24 24"><path d="M8 12l6-6a3 3 0 1 1 4 4l-8 8a4 4 0 0 1-6-6l8-8"/></svg>
        </button>
        <input
          ref={fileRef}
          type="file"
          accept="image/*,.pdf,.doc,.docx,.odt,.txt,.md,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
          hidden
          onChange={(e) => { const f = e.target.files && e.target.files[0]; if (f) onPickFile(f); e.target.value = ""; }}
        />
        <button className="mic" disabled={busy || !text.trim()} title="Envoyer">{busy ? "…" : "↑"}</button>
      </div>
      {!compact && (
        <div className="ask-foot">
          <span>+ Prompts sauvés</span>
          <button type="button" className="ask-attach" onClick={() => fileRef.current && fileRef.current.click()}>Joindre PDF / Word</button>
        </div>
      )}
    </form>
  );
}

function TodayBrief({ brief, onToggle, hint }) {
  if (!brief) return null;
  const today = [...(brief.today || []), ...(brief.undated || [])];
  const later = brief.later || [];
  const insights = brief.insights || [];
  const tensions = brief.contradicts || [];
  if (!today.length && !later.length && !insights.length && !tensions.length && !brief.summary) return null;
  return (
    <div className="today">
      <div className="today-head">
        <strong>{brief.headline || "Aujourd’hui"}</strong>
        <span>{brief.summary}</span>
      </div>
      {hint ? <p className="today-hint">{hint}</p> : null}
      {today.length ? (
        <ul>
          {today.map((t) => (
            <li key={t.id}>
              <button type="button" onClick={() => onToggle(t)}>{t.status === "done" ? "☑" : "☐"}</button>
              <span>{t.title}{t.due_label ? <small> · {t.due_label}</small> : null}</span>
            </li>
          ))}
        </ul>
      ) : null}
      {insights.length ? (
        <div className="today-brief">
          <em>Brief du matin</em>
          {insights.map((item) => <p key={item.id}>{item.title}</p>)}
        </div>
      ) : null}
      {tensions.length ? tensions.map((l) => (
        <p key={l.id} className="today-tension">Tension · {l.from_title} / {l.to_title}</p>
      )) : null}
    </div>
  );
}

function ChatView({ onDropReady, seed, spirit, conversationId, onConversation }) {
  const spiritName = spirit || "Kachina";
  const [messages, setMessages] = useState([]);
  const [text, setText] = useState(seed || "");
  const [cid, setCid] = useState(conversationId || "");
  const [busy, setBusy] = useState(false);
  const [pending, setPending] = useState(false);
  const [vision, setVision] = useState(null);
  const [brief, setBrief] = useState(null);
  const [capture, setCapture] = useState("");
  const [captureHint, setCaptureHint] = useState("");
  const box = useRef(null);
  const empty = messages.length === 0 && !vision && !pending;

  useEffect(() => { box.current && (box.current.scrollTop = box.current.scrollHeight); }, [messages, pending]);

  const hydrate = useCallback(async (id) => {
    if (!id) return;
    const data = await api("/v1/conversations/" + id);
    setCid(data.conversation.id);
    setMessages((data.items || []).map((m) => ({ role: m.role, content: m.content })));
    const run = data.conversation.status === "running";
    setPending(run);
    setBusy(run);
  }, []);

  useEffect(() => {
    if (conversationId) hydrate(conversationId).catch(() => {});
  }, [conversationId, hydrate]);

  useEffect(() => {
    if (!pending || !cid) return;
    const t = setInterval(() => { hydrate(cid).catch(() => {}); }, 1600);
    return () => clearInterval(t);
  }, [pending, cid, hydrate]);

  const ingestFile = useCallback(async (file) => {
    const doc = isDocFile(file);
    const path = doc ? "/v1/ingest/document" : "/v1/ingest/image";
    setVision(doc ? "Lecture du document…" : "Lecture de l’image…");
    try {
      const payload = doc ? file : await shrinkImage(file);
      const fd = new FormData();
      fd.append("file", payload);
      const tok = sessionToken();
      const res = await fetch(API + path, {
        method: "POST",
        credentials: "include",
        headers: tok ? { authorization: "Bearer " + tok } : {},
        body: fd,
      });
      const data = await res.json();
      if (!res.ok) throw new Error(fileError(data.error));
      setMessages((m) => [...m, {
        role: "assistant",
        file: {
          name: file.name || data.memory.title,
          title: data.memory.title,
          theme: data.memory.theme,
          kind: doc ? "document" : "image",
        },
      }]);
    } catch (ex) {
      setMessages((m) => [...m, { role: "assistant", content: "Je n’ai pas pu lire le fichier. " + ((ex && ex.message) || "") }]);
    } finally {
      setVision(null);
    }
  }, []);

  useEffect(() => { onDropReady(ingestFile); }, [ingestFile, onDropReady]);

  async function loadBrief() {
    try { setBrief(await api("/v1/brief")); } catch { setBrief(null); }
  }
  useEffect(() => { loadBrief(); }, []);

  async function sendCapture(e) {
    e.preventDefault();
    const raw = capture.trim();
    if (!raw) return;
    setCapture("");
    try {
      const out = await api("/v1/capture", { method: "POST", body: { text: raw } });
      setCaptureHint(out.hint || (out.kind === "task" ? "Tâche ajoutée" : "Fait rangé"));
      await loadBrief();
    } catch (ex) {
      setCaptureHint(ex.message || "Impossible de ranger.");
    }
  }

  async function toggleToday(task) {
    try {
      await api("/v1/tasks/" + task.id, { method: "PATCH", body: { status: task.status === "done" ? "open" : "done" } });
      await loadBrief();
    } catch { /* ignore */ }
  }

  async function send(e) {
    e.preventDefault();
    const message = text.trim();
    if (!message || busy) return;
    setText("");
    setMessages((m) => [...m, { role: "user", content: message }]);
    setBusy(true);
    try {
      const data = await api("/v1/chat", { method: "POST", body: { message, conversation_id: cid || undefined, background: true } });
      setCid(data.conversation_id);
      if (onConversation) onConversation(data.conversation_id);
      if (data.pending) setPending(true);
      else {
        setMessages((m) => [...m, { role: "assistant", content: data.reply }]);
        setBusy(false);
      }
    } catch (ex) {
      setMessages((m) => [...m, { role: "assistant", content: "Je n’ai pas pu répondre. " + (ex.message || "") }]);
      setBusy(false);
    }
  }

  return (
    <div className="chat">
      {empty ? (
        <div className="hero">
          <div className="hello">Bonjour, <em>Yannick</em></div>
          <h1>How can I assist you today?</h1>
          <TodayBrief brief={brief} onToggle={toggleToday} hint={captureHint} />
          <form className="capture" onSubmit={sendCapture}>
            <input
              value={capture}
              onChange={(e) => setCapture(e.target.value)}
              placeholder="Un fait, une tâche, ou un lien Instagram…"
            />
            <button type="submit" disabled={!capture.trim()}>Ranger</button>
          </form>
          <AskBox text={text} setText={setText} busy={busy} onSend={send} onPickFile={ingestFile} />
          <div className="suggests">
            {SUGGESTS.map(([icon, title, desc]) => (
              <button key={title} className="suggest" type="button" onClick={() => setText(title + " - ")}>
                <i className="suggest-ico">{SUGGEST_ICONS[icon]}</i>
                <strong>{title}</strong>
                <span>{desc}</span>
              </button>
            ))}
          </div>
        </div>
      ) : (
        <>
          <div className="msgs" ref={box}>
            {messages.map((m, i) => (
              <div key={i} className={"bubble " + m.role + (m.file ? " file" : "")}>
                <div className="who">{m.role === "user" ? "Toi" : spiritName}</div>
                {m.file ? (
                  <div className="file-block">
                    <span className="file-ico" aria-hidden>{m.file.kind === "image" ? "▣" : "▤"}</span>
                    <div>
                      <strong>{m.file.name}</strong>
                      <small>Rangé{m.file.theme ? ` · ${m.file.theme}` : ""}</small>
                    </div>
                  </div>
                ) : m.content}
              </div>
            ))}
            {pending && <div className="bubble assistant"><div className="who">{spiritName}</div>Je réfléchis en sous-main. Tu peux fermer, la réponse sera ici.</div>}
            {vision && <div className="bubble assistant"><div className="who">Vision</div>{vision}</div>}
          </div>
          <div className="composer-wrap">
            <AskBox text={text} setText={setText} busy={busy} onSend={send} onPickFile={ingestFile} compact />
          </div>
        </>
      )}
    </div>
  );
}

function Heart({ on, onClick }) {
  return (
    <button
      className={"heart" + (on ? " on" : "")}
      type="button"
      title={on ? "Retirer des favoris" : "Mettre en favori"}
      onClick={(e) => { e.stopPropagation(); onClick(e); }}
      aria-label="Favori"
    >
      {on ? "♥" : "♡"}
    </button>
  );
}

function realSpeech(text) {
  const t = String(text || "").trim();
  if (!t || /parole indisponible/i.test(t) || /écoute de la vidéo/i.test(t)) return "";
  return t;
}

function reelBits(content) {
  const bits = { legend: "", speech: "", screen: "", summary: "", facts: "", url: "", mp4: "" };
  bits.url = ((content || "").match(/https?:\/\/\S*instagram\S*/i) || [])[0] || "";
  for (const chunk of String(content || "").split(/\n\n+/)) {
    const nl = chunk.indexOf("\n");
    const head = (nl === -1 ? chunk : chunk.slice(0, nl)).trim();
    const body = (nl === -1 ? "" : chunk.slice(nl + 1)).trim();
    if (head === "Légende") bits.legend = body;
    else if (head === "Parole") bits.speech = body;
    else if (/^À l['’]écran$/.test(head)) bits.screen = body;
    else if (head === "Synthèse" || head === "Synthese") bits.summary = body;
    else if (head === "Faits") bits.facts = body;
    else if (head === "MP4") bits.mp4 = body;
  }
  return bits;
}

function mp4Info(proc, bits, meta) {
  const status = (proc && proc.mp4) || (meta && meta.mp4) || (/^récupéré/i.test((bits && bits.mp4) || "") ? "got" : (bits && bits.mp4) ? "blocked" : "");
  const note = (bits && bits.mp4) || "";
  if (status === "got") return { kind: "ok", text: note ? `MP4 ${note}` : "MP4 récupéré" };
  if (status === "blocked") return { kind: "no", text: note ? `MP4 ${note}` : "MP4 non récupéré · fichier bloqué" };
  if (status === "absent") return { kind: "no", text: note ? `MP4 ${note}` : "MP4 non récupéré · aucune URL vidéo" };
  if (proc && proc.step === "mp4") return { kind: "wait", text: "MP4 · téléchargement…" };
  return null;
}

function Mp4Pill({ info }) {
  if (!info) return null;
  return <span className={"mp4-pill " + info.kind}>{info.text}</span>;
}

function MemorySheet({ id, onClose, onChanged }) {
  const [data, setData] = useState(null);
  const [img, setImg] = useState(null);
  const [err, setErr] = useState("");
  const [edit, setEdit] = useState(false);
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [saving, setSaving] = useState(false);
  useEffect(() => {
    let dead = false;
    let url = "";
    (async () => {
      try {
        const d = await api("/v1/memories/" + id);
        if (dead) return;
        setData(d);
        setTitle(d.memory.title || "");
        setContent(d.memory.content || "");
        if (d.asset && String(d.asset.mime || "").startsWith("image/")) {
          const tok = sessionToken();
          const res = await fetch(API + "/v1/assets/" + d.asset.id, {
            credentials: "include",
            headers: tok ? { authorization: "Bearer " + tok } : {},
          });
          if (res.ok) {
            url = URL.createObjectURL(await res.blob());
            if (!dead) setImg(url);
          }
        }
      } catch (ex) {
        if (!dead) setErr(ex.message || "Impossible d’ouvrir la fiche.");
      }
    })();
    function esc(e) { if (e.key === "Escape") onClose(); }
    window.addEventListener("keydown", esc);
    return () => {
      dead = true;
      window.removeEventListener("keydown", esc);
      if (url) URL.revokeObjectURL(url);
    };
  }, [id, onClose]);
  if (err) {
    return (
      <div className="sheet-bg" onClick={onClose}>
        <div className="sheet" onClick={(e) => e.stopPropagation()}><p className="muted">{err}</p></div>
      </div>
    );
  }
  if (!data) {
    return (
      <div className="sheet-bg" onClick={onClose}>
        <div className="sheet" onClick={(e) => e.stopPropagation()}><p className="muted">Ouverture…</p></div>
      </div>
    );
  }
  const m = data.memory;
  const bits = reelBits(m.content || "");
  const proc = data.process || {};
  const speech = realSpeech(proc.transcript || bits.speech);
  const legend = bits.legend || proc.caption || "";
  const summary = bits.summary || proc.summary || "";
  const reelUrl = bits.url || proc.source || "";
  const isReel = m.kind === "reel" || /instagram/i.test(m.content || "");
  const mp4 = mp4Info(proc, bits, m.metadata || {});
  async function save() {
    setSaving(true);
    try {
      const next = await api("/v1/memories/" + m.id, { method: "PATCH", body: { title, content } });
      setData({ ...data, memory: next.memory });
      setEdit(false);
      onChanged(next.memory);
    } finally { setSaving(false); }
  }
  return (
    <div className="sheet-bg" onClick={onClose}>
      <div className="sheet" onClick={(e) => e.stopPropagation()}>
        <div className="sheet-top">
          <div>
            <div className="muted">{m.theme} · {m.kind}</div>
            {edit ? (
              <input className="sheet-title" value={title} onChange={(e) => setTitle(e.target.value)} />
            ) : (
              <h2>{m.title}</h2>
            )}
          </div>
          <button className="btn btn-ghost" type="button" onClick={onClose}>Fermer</button>
        </div>
        {img && <img className="sheet-img" src={img} alt="" />}
        {data.asset && data.asset.filename && <p className="muted">{data.asset.filename}</p>}
        {edit ? (
          <textarea className="sheet-edit" value={content} onChange={(e) => setContent(e.target.value)} rows={10} />
        ) : isReel ? (
          <div className="reel-read">
            <div className="reel-info">
              Reel Instagram{proc.title ? ` · ${proc.title}` : ""}
              {reelUrl ? <> · <a href={reelUrl} target="_blank" rel="noreferrer">ouvrir</a></> : null}
            </div>
            <Mp4Pill info={mp4} />
            {speech ? (
              <div className="reel-transcript">
                <em>Transcription complète</em>
                <p>{speech}</p>
              </div>
            ) : (
              <div className="reel-info muted">Pas de parole extraite pour cette vidéo.</div>
            )}
            {legend ? <div className="reel-bit"><em>Légende</em><p>{legend}</p></div> : null}
            {bits.screen ? <div className="reel-bit"><em>À l’écran</em><p>{bits.screen}</p></div> : null}
            {summary ? <div className="reel-bit"><em>Synthèse</em><p>{summary}</p></div> : null}
            {bits.facts ? <div className="reel-bit"><em>Faits</em><p>{bits.facts}</p></div> : null}
          </div>
        ) : (
          <div className="sheet-body">{m.content}</div>
        )}
        {!!(data.links && data.links.length) && (
          <div className="sheet-links">
            <strong>Connexions</strong>
            {data.links.map((l) => (
              <p key={l.id} className="muted">{l.kind} · {l.reason}</p>
            ))}
          </div>
        )}
        <div className="row">
          <small className="muted">{rel(m.updated_at)}{m.source ? ` · ${m.source}` : ""}</small>
          <div className="row" style={{ gap: "0.5rem" }}>
            {edit ? (
              <>
                <button className="btn btn-ghost" type="button" onClick={() => { setEdit(false); setTitle(m.title); setContent(m.content); }}>Annuler</button>
                <button className="btn" type="button" disabled={saving} onClick={save}>{saving ? "…" : "Enregistrer"}</button>
              </>
            ) : (
              <button className="btn btn-ghost" type="button" onClick={() => setEdit(true)}>Corriger</button>
            )}
            <Heart on={!!m.favorite} onClick={async () => {
              const next = await api("/v1/memories/" + m.id + "/favorite", { method: "POST", body: { favorite: !m.favorite } });
              setData({ ...data, memory: next.memory });
              onChanged(next.memory);
            }} />
            <button className="btn btn-ghost" type="button" onClick={async () => {
              if (!confirm("Oublier cette fiche ?")) return;
              await api("/v1/memories/" + m.id, { method: "DELETE" });
              onChanged(null);
              onClose();
            }}>Oublier</button>
          </div>
        </div>
      </div>
    </div>
  );
}


function LibraryView({ favoritesOnly, startPane = "cards" }) {
  const [items, setItems] = useState([]);
  const [q, setQ] = useState("");
  const [theme, setTheme] = useState("");
  const [open, setOpen] = useState(null);
  const [pane, setPane] = useState(startPane);
  const [themes, setThemes] = useState([]);
  const [picked, setPicked] = useState("");
  const [tasks, setTasks] = useState([]);
  const [taskTitle, setTaskTitle] = useState("");
  async function load() {
    const qs = new URLSearchParams();
    if (q) qs.set("q", q);
    if (theme) qs.set("theme", theme);
    if (favoritesOnly) qs.set("favorite", "1");
    qs.set("limit", "80");
    const data = await api("/v1/memories?" + qs.toString());
    setItems(data.items || []);
  }
  useEffect(() => { load().catch(() => {}); }, [theme, favoritesOnly]);
  async function loadTasks() {
    const data = await api("/v1/tasks?status=all&limit=80");
    setTasks(data.items || []);
  }
  useEffect(() => {
    api("/v1/themes").then((d) => setThemes(d.themes || [])).catch(() => {});
    loadTasks().catch(() => {});
  }, []);
  async function addTask() {
    const title = taskTitle.trim();
    if (!title) return;
    setTaskTitle("");
    const data = await api("/v1/tasks", { method: "POST", body: { title } });
    setTasks((cur) => [data.task, ...cur]);
  }
  async function toggleTask(t) {
    const data = await api("/v1/tasks/" + t.id, { method: "PATCH", body: { status: t.status === "done" ? "open" : "done" } });
    setTasks((cur) => cur.map((x) => (x.id === data.task.id ? data.task : x)));
  }
  async function dropTask(t) {
    if (!confirm("Supprimer cette tâche ?")) return;
    await api("/v1/tasks/" + t.id, { method: "DELETE" });
    setTasks((cur) => cur.filter((x) => x.id !== t.id));
  }
  async function toggleFav(m, e) {
    if (e) e.stopPropagation();
    const data = await api("/v1/memories/" + m.id + "/favorite", { method: "POST", body: { favorite: !m.favorite } });
    setItems((cur) => {
      const next = cur.map((x) => (x.id === m.id ? data.memory : x));
      return favoritesOnly ? next.filter((x) => x.favorite) : next;
    });
  }
  const maxW = Math.max(1, ...themes.map((t) => t.count || 0));
  return (
    <div className="grid">
      <div className="pane-switch">
        <button type="button" className={pane === "cards" ? "on" : ""} onClick={() => setPane("cards")}>Cartes</button>
        <button type="button" className={pane === "themes" ? "on" : ""} onClick={() => setPane("themes")}>Thématiques</button>
        <button type="button" className={pane === "tasks" ? "on" : ""} onClick={() => setPane("tasks")}>Tâches</button>
      </div>
      {pane === "tasks" && (
        <div className="task-box">
          <div className="row">
            <input placeholder="Ex. Relancer le devis demain" value={taskTitle} onChange={(e) => setTaskTitle(e.target.value)} onKeyDown={(e) => e.key === "Enter" && addTask()} style={{ flex: 1, background: "var(--side)", border: "1px solid var(--line)", borderRadius: 12, padding: "0.7rem 0.9rem" }} />
            <button className="btn" type="button" onClick={addTask}>Ajouter</button>
          </div>
          <div className="task-list">
            {tasks.filter((t) => t.status !== "done").map((t) => (
              <button key={t.id} type="button" className="task" onClick={() => toggleTask(t)}>
                <i className="box" />
                <span>{t.title}{t.due_label ? <small className="due">Échéance · {t.due_label}</small> : null}</span>
                <em onClick={(e) => { e.stopPropagation(); dropTask(t); }}>supprimer</em>
              </button>
            ))}
            {!tasks.some((t) => t.status !== "done") && <p className="muted">Rien à faire pour l’instant.</p>}
          </div>
          {tasks.some((t) => t.status === "done") && (
            <>
              <p className="muted">Faites</p>
              <div className="task-list">
                {tasks.filter((t) => t.status === "done").map((t) => (
                  <button key={t.id} type="button" className="task done" onClick={() => toggleTask(t)}>
                    <i className="box on" />
                    <span>{t.title}{t.due_label ? <small className="due">Échéance · {t.due_label}</small> : null}</span>
                    <em onClick={(e) => { e.stopPropagation(); dropTask(t); }}>supprimer</em>
                  </button>
                ))}
              </div>
            </>
          )}
        </div>
      )}
      {pane === "themes" && (
        <div className="theme-weights">
          {themes.map((t) => (
            <button
              key={t.slug}
              type="button"
              className={"weight" + (picked === t.slug ? " on" : "")}
              onClick={() => { setPicked(t.slug === picked ? "" : t.slug); setTheme(t.slug); setPane("cards"); }}
            >
              <div className="row">
                <strong><i className="dot" style={{ background: t.color }} />{t.name}</strong>
                <b>{t.count || 0}</b>
              </div>
              <span className="bar"><i style={{ width: ((t.count || 0) / maxW * 100) + "%", background: t.color }} /></span>
              <small className="muted">{t.description}</small>
            </button>
          ))}
        </div>
      )}
      {pane === "cards" && (
      <>
      <div className="row">
        <input placeholder={favoritesOnly ? "Chercher dans les favoris" : "Chercher dans la bibliothèque"} value={q} onChange={(e) => setQ(e.target.value)} onKeyDown={(e) => e.key === "Enter" && load()} style={{ flex: 1, background: "var(--side)", border: "1px solid var(--line)", borderRadius: 12, padding: "0.7rem 0.9rem" }} />
        <button className="btn" onClick={load}>Chercher</button>
      </div>
      {theme && (
        <div className="row">
          <span className="chip">Thème · {theme}</span>
          <button className="btn btn-ghost" type="button" onClick={() => { setTheme(""); setPicked(""); }}>Tout voir</button>
        </div>
      )}
      <div className="cards">
        {items.map((m) => {
          const speech = m.kind === "reel" ? realSpeech(reelBits(m.content).speech) : "";
          const preview = speech || m.content;
          return (
          <article key={m.id} className="card card-open" onClick={() => setOpen(m.id)}>
            <div className="card-top">
              <div className="theme-bar" style={{ flex: 1 }} />
              <Heart on={!!m.favorite} onClick={(e) => toggleFav(m, e)} />
            </div>
            <div className="muted">{m.theme} · {m.kind === "reel" ? "reel · Instagram" : m.kind}</div>
            <h3>{m.title}</h3>
            <p className={speech ? "card-speech" : ""}>{preview.slice(0, 180)}{preview.length > 180 ? "…" : ""}</p>
            <div className="row">
              <small className="muted">{rel(m.updated_at)}</small>
              <button className="btn btn-ghost" onClick={async (e) => { e.stopPropagation(); if (confirm("Oublier cette fiche ?")) { await api("/v1/memories/" + m.id, { method: "DELETE" }); load(); } }}>Oublier</button>
            </div>
          </article>
          );
        })}
        {!items.length && <p className="muted">{favoritesOnly ? "Aucun favori pour l’instant. Clique le cœur sur une fiche." : "La bibliothèque est vide. Parle au cerveau ou dépose une image."}</p>}
      </div>
      </>
      )}
      {open && (
        <MemorySheet
          id={open}
          onClose={() => setOpen(null)}
          onChanged={(next) => {
            if (!next) { load(); return; }
            setItems((cur) => cur.map((x) => (x.id === next.id ? next : x)));
          }}
        />
      )}
    </div>
  );
}

function ThemesView({ themes, onOpen }) {
  return (
    <div className="grid">
      <div className="cards">
        {(themes || []).map((t) => (
          <article key={t.slug} className="card" onClick={() => onOpen(t.slug)} style={{ cursor: "pointer" }}>
            <div className="theme-bar" style={{ background: t.color }} />
            <h3>{t.name}</h3>
            <p>{t.description}</p>
            <div className="row">
              <strong>{t.count} fiche{t.count > 1 ? "s" : ""}</strong>
              <small className="muted">{t.last_at ? rel(t.last_at) : "vierge"}</small>
            </div>
          </article>
        ))}
      </div>
    </div>
  );
}

const API_PUBLIC = "https://api.kachina.app";
const MCP_PUBLIC = API_PUBLIC + "/mcp";

function agentPrompt(spirit, token, agent) {
  const key = token || "kch_TA_CLE";
  const who = (agent || "Claude").trim() || "Claude";
  const label = (spirit || "Kachina").trim() || "Kachina";
  return `Tu as accès à ${label}, mon esprit / mémoire personnelle partagée.

Base URL : ${API_PUBLIC}
MCP : ${MCP_PUBLIC}
Clé : ${key}

Headers à envoyer à chaque appel :
Authorization: Bearer ${key}
X-Agent-Name: ${who}
Content-Type: application/json

Endpoints :
- GET ${API_PUBLIC}/v1/search?q=… - cherche dans ma mémoire avant d’inventer
- POST ${API_PUBLIC}/v1/chat  {"message":"…"} - pose une question à ${label}
- POST ${API_PUBLIC}/v1/ingest  {"title":"…","content":"…","theme":"business"} - range un fait durable
- GET ${API_PUBLIC}/v1/memories - liste les fiches
- GET ${API_PUBLIC}/v1/themes - aperçu par thématique
- GET ${API_PUBLIC}/v1/tasks - tâches à faire
- POST ${API_PUBLIC}/v1/tasks  {"title":"…"} - ajouter une tâche
- PATCH ${API_PUBLIC}/v1/tasks/:id  {"status":"done"} - cocher

MCP (Cursor, Claude, tout client MCP HTTP) :
{
  "mcpServers": {
    "kachina": {
      "url": "${MCP_PUBLIC}",
      "headers": {
        "Authorization": "Bearer ${key}",
        "X-Agent-Name": "${who}"
      }
    }
  }
}

Outils MCP : brain_search, brain_remember, brain_library, brain_themes, brain_tasks, brain_task_add, brain_task_done, brain_activity, brain_who, brain_connectors.

Règles :
1. Quand je parle d’un projet, d’une personne ou d’un business, cherche d’abord dans ${label}.
2. Ne pas inventer un fait s’il est dans le cerveau - cite-le.
3. Si tu apprends un fait durable (décision, chiffre, contact), range-le avec brain_remember ou POST /v1/ingest.
4. Une clé = un agent. N’utilise que celle-ci.`;
}

function mcpJSON(token, agent) {
  const key = token || "kch_TA_CLE";
  const who = (agent || "Cursor").trim() || "Cursor";
  return `{
  "mcpServers": {
    "kachina": {
      "url": "${MCP_PUBLIC}",
      "headers": {
        "Authorization": "Bearer ${key}",
        "X-Agent-Name": "${who}"
      }
    }
  }
}`;
}

function copyText(text) {
  navigator.clipboard.writeText(text).catch(() => {});
}

function KeysView({ spirit }) {
  const [items, setItems] = useState([]);
  const [fresh, setFresh] = useState(null);
  const [open, setOpen] = useState(null);
  const [copied, setCopied] = useState("");
  async function load() { setItems((await api("/v1/keys")).items || []); }
  useEffect(() => { load().catch(() => {}); }, []);
  function mark(label, text) {
    copyText(text);
    setCopied(label);
    setTimeout(() => setCopied((cur) => (cur === label ? "" : cur)), 1600);
  }
  const notices = { clé: "Clé copiée", url: "URL copiée", mcp: "MCP copié", prompt: "Prompt copié", json: "JSON MCP copié" };
  return (
    <div className="grid">
      <p className="muted">Crée une clé, copie le pack, puis ouvre-la pour voir qui s’y connecte.</p>
      <button className="btn" onClick={async () => {
        const data = await api("/v1/keys", { method: "POST", body: {} });
        setFresh(data.key);
        load();
      }}>Créer une clé</button>
      {fresh && (
        <div className="pack">
          <p className="muted">Copie maintenant. Le secret ne réapparaîtra plus.</p>
          <div className="pack-row"><span>Clé</span><code>{fresh.token}</code><button className="btn btn-ghost" type="button" onClick={() => mark("clé", fresh.token)}>Copier</button></div>
          <div className="pack-row"><span>URL</span><code>{API_PUBLIC}</code><button className="btn btn-ghost" type="button" onClick={() => mark("url", API_PUBLIC)}>Copier</button></div>
          <div className="pack-row"><span>MCP</span><code>{MCP_PUBLIC}</code><button className="btn btn-ghost" type="button" onClick={() => mark("mcp", MCP_PUBLIC)}>Copier</button></div>
          <div className="row" style={{ gap: "0.5rem", flexWrap: "wrap" }}>
            <button className="btn" type="button" onClick={() => mark("prompt", agentPrompt(spirit, fresh.token, "Claude"))}>Copier le prompt pour l’agent</button>
            <button className="btn btn-ghost" type="button" onClick={() => mark("json", mcpJSON(fresh.token, "Cursor"))}>Copier le JSON MCP</button>
          </div>
          <pre className="secret">{agentPrompt(spirit, fresh.token, "Claude")}</pre>
        </div>
      )}
      {copied && <div className="toast">{notices[copied] || "Copié"}</div>}
      <div className="cards">
        {items.map((k) => (
          <article key={k.id} className="card card-open" onClick={() => setOpen(k.id)}>
            <h3>{(k.prefix || "kch_")}…</h3>
            <p>{k.connection_count || 0} connexion{(k.connection_count || 0) > 1 ? "s" : ""}{k.last_agent ? ` · ${k.last_agent}` : ""}</p>
            <small className="muted">{k.last_used_at ? rel(k.last_used_at) : "Jamais utilisée"}</small>
          </article>
        ))}
        {!items.length && <p className="muted">Aucune clé pour l’instant.</p>}
      </div>
      {open && <KeySheet id={open} onClose={() => setOpen(null)} onRevoke={() => { setOpen(null); if (fresh && fresh.id === open) setFresh(null); load(); }} />}
    </div>
  );
}

function KeySheet({ id, onClose, onRevoke }) {
  const [data, setData] = useState(null);
  const [err, setErr] = useState("");
  useEffect(() => {
    api("/v1/keys/" + id).then(setData).catch((ex) => setErr(ex.message || "Impossible d’ouvrir la clé."));
    function esc(e) { if (e.key === "Escape") onClose(); }
    window.addEventListener("keydown", esc);
    return () => window.removeEventListener("keydown", esc);
  }, [id, onClose]);
  if (err) {
    return (
      <div className="sheet-bg" onClick={onClose}>
        <div className="sheet" onClick={(e) => e.stopPropagation()}><p className="muted">{err}</p></div>
      </div>
    );
  }
  if (!data) {
    return (
      <div className="sheet-bg" onClick={onClose}>
        <div className="sheet" onClick={(e) => e.stopPropagation()}><p className="muted">Ouverture…</p></div>
      </div>
    );
  }
  const stats = data.stats || {};
  return (
    <div className="sheet-bg" onClick={onClose}>
      <div className="sheet" onClick={(e) => e.stopPropagation()}>
        <div className="sheet-top">
          <div>
            <div className="muted">Clé</div>
            <h2>{(data.key.prefix || "kch_")}…</h2>
          </div>
          <button className="btn btn-ghost" type="button" onClick={onClose}>Fermer</button>
        </div>
        <div className="cards">
          <article className="card"><h3>{stats.connections || 0}</h3><p>Connexions</p></article>
          <article className="card"><h3>{stats.agents || 0}</h3><p>Agents distincts</p></article>
        </div>
        <h3 style={{ marginTop: "1rem" }}>Qui s’y connecte</h3>
        {(data.agents || []).length ? (
          <table className="table">
            <thead><tr><th>Agent</th><th>Connexions</th><th>Vu</th></tr></thead>
            <tbody>
              {data.agents.map((a) => (
                <tr key={a.agent_name}><td>{a.agent_name}</td><td>{a.n}</td><td>{rel(a.last_at)}</td></tr>
              ))}
            </tbody>
          </table>
        ) : <p className="muted">Personne pour l’instant.</p>}
        <h3 style={{ marginTop: "1rem" }}>Dernières connexions</h3>
        {(data.connections || []).length ? (
          <table className="table">
            <thead><tr><th>Quand</th><th>Agent</th><th>Appel</th></tr></thead>
            <tbody>
              {data.connections.map((c) => (
                <tr key={c.id}><td>{rel(c.created_at)}</td><td>{c.agent_name || "-"}</td><td>{c.method} {c.path}</td></tr>
              ))}
            </tbody>
          </table>
        ) : <p className="muted">Aucune connexion enregistrée.</p>}
        <div className="row" style={{ marginTop: "1rem" }}>
          <small className="muted">{stats.last_at ? "Dernière " + rel(stats.last_at) : ""}</small>
          <button className="btn btn-ghost" type="button" onClick={async () => {
            if (!confirm("Révoquer cette clé ?")) return;
            await api("/v1/keys/" + id, { method: "DELETE" });
            onRevoke();
          }}>Révoquer</button>
        </div>
      </div>
    </div>
  );
}

function InsightsView({ onOpen }) {
  const [insights, setInsights] = useState([]);
  const [links, setLinks] = useState([]);
  useEffect(() => {
    api("/v1/insights").then((d) => setInsights(d.items || [])).catch(() => {});
    api("/v1/graph").then((d) => setLinks(d.items || [])).catch(() => {});
  }, []);
  return (
    <div className="grid">
      <p className="muted">Ce que le cerveau a conclu tout seul, et les liens qu’il a tirés entre tes fiches.</p>
      <div className="cards">
        {insights.map((m) => (
          <article key={m.id} className="card card-open" onClick={() => onOpen && onOpen(m.id)}>
            <div className="theme-bar" />
            <h3>{m.title}</h3>
            <p>{(m.content || "").slice(0, 220)}{(m.content || "").length > 220 ? "…" : ""}</p>
            <small className="muted">{rel(m.updated_at)}</small>
          </article>
        ))}
        {!insights.length && <p className="muted">Pas encore d’insight. Range des faits, le sommeil de 5h15 en formera.</p>}
      </div>
      <h2 style={{ fontSize: "1.05rem" }}>Graphe des liens</h2>
      <div className="graph">
        {links.map((l) => (
          <div key={l.id} className="graph-row">
            <button type="button" onClick={() => onOpen && onOpen(l.from_id)}>{l.from_title || l.from_id}</button>
            <span className="graph-kind">{l.kind}</span>
            <button type="button" onClick={() => onOpen && onOpen(l.to_id)}>{l.to_title || l.to_id}</button>
            {l.reason && <small className="muted">{l.reason}</small>}
          </div>
        ))}
        {!links.length && <p className="muted">Aucun lien pour l’instant.</p>}
      </div>
    </div>
  );
}

function CommandK({ onClose, onOpenMemory, onAsk }) {
  const [q, setQ] = useState("");
  const [hits, setHits] = useState([]);
  const [busy, setBusy] = useState(false);
  const input = useRef(null);
  useEffect(() => { input.current && input.current.focus(); }, []);
  useEffect(() => {
    function esc(e) { if (e.key === "Escape") onClose(); }
    window.addEventListener("keydown", esc);
    return () => window.removeEventListener("keydown", esc);
  }, [onClose]);
  useEffect(() => {
    const needle = q.trim();
    if (!needle) { setHits([]); return; }
    const t = setTimeout(async () => {
      setBusy(true);
      try {
        const data = await api("/v1/search?q=" + encodeURIComponent(needle) + "&limit=8");
        setHits(data.items || []);
      } catch { setHits([]); }
      setBusy(false);
    }, 220);
    return () => clearTimeout(t);
  }, [q]);
  return (
    <div className="sheet-bg cmdk-bg" onClick={onClose}>
      <div className="sheet cmdk" onClick={(e) => e.stopPropagation()}>
        <input
          ref={input}
          placeholder="Chercher dans le cerveau…"
          value={q}
          onChange={(e) => setQ(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === "Enter" && q.trim()) { onAsk(q.trim()); onClose(); }
          }}
        />
        <div className="cmdk-hits">
          {busy && <p className="muted">Recherche…</p>}
          {hits.map((m) => (
            <button key={m.id} type="button" onClick={() => { onOpenMemory(m.id); onClose(); }}>
              <strong>{m.title}</strong>
              <small>{m.theme} · {(m.content || "").slice(0, 90)}</small>
            </button>
          ))}
          {q.trim() && !busy && !hits.length && <p className="muted">Rien trouvé. Entrée pour poser la question.</p>}
        </div>
      </div>
    </div>
  );
}

function ConnectorsView() {
  const [items, setItems] = useState([]);
  const [form, setForm] = useState({ name: "", kind: "http", business: "", url: "", secret: "" });
  const [clipUrl, setClipUrl] = useState("");
  const [clipOk, setClipOk] = useState("");
  async function load() { setItems((await api("/v1/connectors")).items || []); }
  useEffect(() => { load().catch(() => {}); }, []);
  const set = (k) => (e) => setForm({ ...form, [k]: e.target.value });
  const bookmark = `javascript:void(open(${JSON.stringify(location.origin + "/?clip=")}+encodeURIComponent(location.href)))`;
  return (
    <div className="grid">
      <div className="card">
        <h3>Clipper une page</h3>
        <p className="muted">Glisse ce lien dans tes favoris, ou colle une URL ici.</p>
        <p><a className="chip" href={bookmark} onClick={(e) => e.preventDefault()}>Ranger dans Kachina</a></p>
        <div className="row">
          <input
            placeholder="https://…"
            value={clipUrl}
            onChange={(e) => setClipUrl(e.target.value)}
            style={{ flex: 1, background: "var(--side)", border: "1px solid var(--line)", borderRadius: 12, padding: "0.7rem 0.9rem" }}
          />
          <button className="btn" type="button" onClick={async () => {
            setClipOk("");
            await api("/v1/ingest/url", { method: "POST", body: { url: clipUrl } });
            setClipUrl("");
            setClipOk("Page rangée.");
          }}>Ranger</button>
        </div>
        {clipOk && <p>{clipOk}</p>}
      </div>
      <p className="muted">Branche tes MCP et APIs métier. Le cerveau sait qui fait quoi, et quand le connecteur a répondu.</p>
      <div className="form-grid">
        <label className="field"><span>Nom</span><input value={form.name} onChange={set("name")} placeholder="Restaurateur API" /></label>
        <label className="field"><span>Business</span><input value={form.business} onChange={set("business")} placeholder="Restaurateur" /></label>
        <label className="field"><span>Type</span>
          <select value={form.kind} onChange={set("kind")}><option value="http">API HTTP</option><option value="mcp">MCP</option></select>
        </label>
        <label className="field"><span>URL</span><input value={form.url} onChange={set("url")} placeholder="https://…" /></label>
      </div>
      <label className="field"><span>Secret (optionnel)</span><input value={form.secret} onChange={set("secret")} placeholder="Bearer / clé" /></label>
      <button className="btn" onClick={async () => { await api("/v1/connectors", { method: "POST", body: form }); setForm({ name: "", kind: "http", business: "", url: "", secret: "" }); load(); }}>Ajouter</button>
      <table className="table">
        <thead><tr><th>Nom</th><th>Business</th><th>Type</th><th>État</th><th>Dernier contact</th><th></th></tr></thead>
        <tbody>
          {items.map((x) => (
            <tr key={x.id}>
              <td>{x.name}</td>
              <td>{x.business || "-"}</td>
              <td>{x.kind}</td>
              <td>{x.status}{x.last_error ? " · " + x.last_error.slice(0, 40) : ""}</td>
              <td>{rel(x.last_sync_at)}</td>
              <td className="row">
                <button className="btn btn-ghost" onClick={async () => { await api("/v1/connectors/" + x.id + "/ping", { method: "POST" }); load(); }}>Ping</button>
                <button className="btn btn-ghost" onClick={async () => { await api("/v1/connectors/" + x.id, { method: "DELETE" }); load(); }}>Retirer</button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function SpiritView({ me, onSaved, onDeleted }) {
  const [name, setName] = useState(me.spirit_name || "Kachina");
  const [busy, setBusy] = useState(false);
  const [ok, setOk] = useState("");
  const [err, setErr] = useState("");
  const [wiping, setWiping] = useState(false);
  async function save(e) {
    e.preventDefault();
    setBusy(true); setOk(""); setErr("");
    try {
      const data = await api("/v1/account", { method: "PATCH", body: { spirit_name: name } });
      onSaved(data.spirit_name);
      setName(data.spirit_name);
      setOk("Ton esprit s’appelle " + data.spirit_name + ".");
    } catch (ex) {
      setErr(ex.message || "Impossible d’enregistrer.");
    } finally { setBusy(false); }
  }
  return (
    <div className="grid">
      <p className="muted">Donne un nom à ton esprit. Le chat, tes agents et l’accueil l’utilisent à la place de « Kachina ».</p>
      <form className="form-grid" onSubmit={save} style={{ gridTemplateColumns: "1fr auto", alignItems: "end" }}>
        <label className="field">
          <span>Nom de l’esprit</span>
          <input value={name} onChange={(e) => setName(e.target.value)} maxLength={40} placeholder="Kachina" />
        </label>
        <button className="btn" disabled={busy || !name.trim()}>{busy ? "…" : "Enregistrer"}</button>
      </form>
      {ok && <p>{ok}</p>}
      {err && <div className="err">{err}</div>}
      <div className="card" style={{ marginTop: "1.5rem" }}>
        <h3>Zone dangereuse</h3>
        <p className="muted">Supprime le compte et tout le cerveau : mémoires, discussions, clés. Irréversible.</p>
        <button className="btn btn-ghost" disabled={wiping} onClick={async () => {
          if (!confirm("Supprimer définitivement le compte et tout le cerveau ?")) return;
          setWiping(true); setErr("");
          try {
            await api("/v1/account", { method: "DELETE" });
            localStorage.removeItem("kachina_token");
            localStorage.removeItem("kachina_last_cid");
            onDeleted();
          } catch (ex) {
            setErr(ex.message || "Impossible de supprimer le compte.");
            setWiping(false);
          }
        }}>{wiping ? "Suppression…" : "Supprimer mon compte"}</button>
      </div>
    </div>
  );
}

const PROCESS_KIND = { reel: "Reel Instagram", chat: "Discussion", image: "Image", document: "Document", url: "Lien" };
const PROCESS_STEP = {
  lecture: "Lecture du lien",
  image: "Lecture de l’image",
  mp4: "Récupération du MP4",
  "sans-mp4": "MP4 introuvable",
  transcription: "Transcription",
  synthese: "Synthèse",
  reflexion: "Réflexion",
  termine: "Terminé",
  erreur: "Erreur",
};

function processStep(step) {
  return PROCESS_STEP[step] || step || "En cours";
}

function ProcessesView() {
  const [items, setItems] = useState([]);
  const [running, setRunning] = useState(0);
  const [open, setOpen] = useState("");
  async function load() {
    try {
      const data = await api("/v1/processes?limit=40");
      setItems(data.items || []);
      setRunning(data.running || 0);
    } catch { /* ignore */ }
  }
  useEffect(() => {
    load();
    const t = setInterval(load, 1600);
    return () => clearInterval(t);
  }, []);
  const live = items.filter((p) => p.status === "running");
  const done = items.filter((p) => p.status !== "running");
  return (
    <div className="process-page">
      <div className="process-head">
        <div>
          <h2>Processus</h2>
          <p className="muted">{running ? `${running} en cours` : "Rien en cours. Un reel ou une question apparaîtra ici."}</p>
        </div>
      </div>
      {!items.length && <p className="muted">Aucun traitement pour l’instant.</p>}
      {live.length ? (
        <section className="process-block">
          <h3>En cours</h3>
          {live.map((p) => <ProcessCard key={p.id} job={p} open={open === p.id} onToggle={() => setOpen(open === p.id ? "" : p.id)} />)}
        </section>
      ) : null}
      {done.length ? (
        <section className="process-block">
          <h3>Récents</h3>
          {done.map((p) => <ProcessCard key={p.id} job={p} open={open === p.id} onToggle={() => setOpen(open === p.id ? "" : p.id)} />)}
        </section>
      ) : null}
    </div>
  );
}

function ProcessCard({ job, open, onToggle }) {
  const kind = PROCESS_KIND[job.kind] || job.kind;
  return (
    <article className={"process-card" + (job.status === "running" ? " live" : "") + (job.status === "error" ? " err" : "")}>
      <button type="button" className="process-top" onClick={onToggle}>
        <div>
          <strong>{job.title || kind}</strong>
          <small>{kind} · {processStep(job.step)} · {rel(job.updated_at)}</small>
          {job.kind === "reel" ? <Mp4Pill info={mp4Info(job, {}, {})} /> : null}
        </div>
        <em className={"process-st " + job.status}>{job.status === "running" ? "en cours" : job.status === "error" ? "erreur" : "terminé"}</em>
      </button>
      {(open || job.status === "running") && (
        <div className="process-body">
          {job.source ? <p className="muted"><a href={job.source} target="_blank" rel="noreferrer">{job.source}</a></p> : null}
          {job.caption ? <div className="process-bit"><em>Légende</em><p>{job.caption}</p></div> : null}
          {job.transcript ? <div className="process-bit"><em>Transcription</em><p>{job.transcript}</p></div> : null}
          {job.summary ? <div className="process-bit"><em>Synthèse</em><p>{job.summary}</p></div> : null}
          {job.error ? <div className="process-bit"><em>Erreur</em><p>{job.error}</p></div> : null}
          {job.status === "running" && !job.transcript && !job.caption ? <p className="muted">Je travaille…</p> : null}
        </div>
      )}
    </article>
  );
}

function ActivityView() {
  const [agents, setAgents] = useState({ items: [], summary: [] });
  const [acts, setActs] = useState([]);
  useEffect(() => {
    api("/v1/agents").then(setAgents).catch(() => {});
    api("/v1/activity").then((d) => setActs(d.items || [])).catch(() => {});
  }, []);
  return (
    <div className="grid">
      <div className="cards">
        {(agents.summary || []).map((s) => (
          <article key={s.agent_name} className="card">
            <h3>{s.agent_name}</h3>
            <p>{s.n} connexion{s.n > 1 ? "s" : ""} · dernière {rel(s.last_at)}</p>
          </article>
        ))}
      </div>
      <table className="table">
        <thead><tr><th>Quand</th><th>Qui</th><th>Agent</th><th>Action</th><th>Ressource</th></tr></thead>
        <tbody>
          {acts.map((a) => (
            <tr key={a.id}>
              <td>{rel(a.created_at)}</td>
              <td>{a.actor}</td>
              <td>{a.agent || "-"}</td>
              <td>{a.action}</td>
              <td>{a.resource || "-"}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function App() {
  const [ready, setReady] = useState(false);
  const [me, setMe] = useState(null);
  const [page, setPage] = useState("chat");
  const [themes, setThemes] = useState([]);
  const [convs, setConvs] = useState([]);
  const [q, setQ] = useState("");
  const [chatKey, setChatKey] = useState(0);
  const [openCid, setOpenCid] = useState("");
  const [seedChat, setSeedChat] = useState("");
  const [openMemory, setOpenMemory] = useState(null);
  const [cmdk, setCmdk] = useState(false);
  const [clipNote, setClipNote] = useState("");
  const [drop, setDrop] = useState(false);
  const [runningJobs, setRunningJobs] = useState(0);
  const ingestRef = useRef(null);

  const refreshConvs = useCallback(async () => {
    const hist = await api("/v1/conversations").catch(() => ({ items: [] }));
    setConvs(hist.items || []);
  }, []);

  const boot = useCallback(async () => {
    try {
      const qs = new URLSearchParams(location.search);
      const qToken = qs.get("token");
      const clip = qs.get("clip");
      if (qToken && qToken.startsWith("kcs_")) {
        localStorage.setItem("kachina_token", qToken);
      }
      if (qToken || clip) history.replaceState(null, "", location.pathname);
      const data = await api("/v1/auth/me");
      setMe(data.actor);
      const lib = await api("/v1/library");
      setThemes(lib.themes || []);
      await refreshConvs();
      const last = localStorage.getItem("kachina_last_cid") || "";
      if (last) setOpenCid(last);
      if (clip) {
        try {
          await api("/v1/ingest/url", { method: "POST", body: { url: clip } });
          setClipNote("Page rangée dans le cerveau.");
        } catch {
          setClipNote("Impossible de clipper cette page.");
        }
      }
    } catch { setMe(null); }
    setReady(true);
  }, [refreshConvs]);

  useEffect(() => { boot(); }, [boot]);

  useEffect(() => {
    if (!me) return undefined;
    let dead = false;
    async function tick() {
      try {
        const data = await api("/v1/processes?limit=1");
        if (!dead) setRunningJobs(data.running || 0);
      } catch { /* ignore */ }
    }
    tick();
    const t = setInterval(tick, 2500);
    return () => { dead = true; clearInterval(t); };
  }, [me]);

  useEffect(() => {
    function down(e) {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
        e.preventDefault();
        setCmdk(true);
      }
    }
    window.addEventListener("keydown", down);
    return () => window.removeEventListener("keydown", down);
  }, []);

  useEffect(() => {
    function over(e) { if (e.dataTransfer?.types?.includes("Files")) { e.preventDefault(); setDrop(true); } }
    function leave(e) { if (!e.relatedTarget) setDrop(false); }
    function dropFile(e) {
      e.preventDefault(); setDrop(false);
      const file = e.dataTransfer.files?.[0];
      if (file && ingestRef.current && (file.type.startsWith("image/") || isDocFile(file))) ingestRef.current(file);
    }
    window.addEventListener("dragover", over);
    window.addEventListener("dragleave", leave);
    window.addEventListener("drop", dropFile);
    return () => {
      window.removeEventListener("dragover", over);
      window.removeEventListener("dragleave", leave);
      window.removeEventListener("drop", dropFile);
    };
  }, []);

  useEffect(() => {
    function down(e) {
      const target = e.target;
      if (target && target.closest && target.closest("input, textarea, select, [contenteditable='true']")) return;
      const active = document.activeElement;
      if (!active) return;
      const tag = (active.tagName || "").toLowerCase();
      if (tag === "input" || tag === "textarea" || tag === "select" || active.isContentEditable) active.blur();
    }
    document.addEventListener("pointerdown", down, true);
    return () => document.removeEventListener("pointerdown", down, true);
  }, []);

  if (!ready) {
    return (
      <div className="splash">
        <i className="splash-ring" />
        <i className="splash-ring delay" />
        <i className="splash-ring delay2" />
        <img src="assets/img/mark.png" alt="" />
        <strong>Kachina</strong>
      </div>
    );
  }
  if (!me) return <Login onIn={boot} />;

  return (
    <div className="app-frame">
      <div className="shell">
        <aside className="side">
          <div className="side-brand">
            <img className="brand-mark" src="assets/img/mark.png" alt="" />
            <strong>{me.spirit_name || "Kachina"}</strong>
          </div>
          <button className="btn btn-new" onClick={() => { localStorage.removeItem("kachina_last_cid"); setPage("chat"); setOpenCid(""); setChatKey((k) => k + 1); }}>+ New chat</button>
          <label className="search" onClick={() => setCmdk(true)}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>
            <input placeholder="Search" value={q} onChange={(e) => setQ(e.target.value)} onFocus={() => setCmdk(true)} />
            <kbd>⌘K</kbd>
          </label>
          <nav className="nav">
            {NAV.map(([id, label]) => (
              <button key={id} className={page === id ? "active" : ""} onClick={() => setPage(id)}>
                {ICONS[id]} {label}
                {id === "processes" && runningJobs > 0 ? <span className="nav-badge">{runningJobs}</span> : null}
              </button>
            ))}
          </nav>
          <div className="hist">
            {convs.length > 0 && <div className="hist-label">Aujourd’hui</div>}
            {convs.filter((c) => !q || (c.title || "").toLowerCase().includes(q.toLowerCase())).slice(0, 12).map((c) => (
              <button
                key={c.id}
                className={openCid === c.id ? "active" : ""}
                onClick={() => { setPage("chat"); setOpenCid(c.id); localStorage.setItem("kachina_last_cid", c.id); setChatKey((k) => k + 1); }}
              >
                {c.status === "running" ? "… " : ""}{c.title || "Sans titre"}
              </button>
            ))}
          </div>
          <div className="user-card">
            <div className="avatar" />
            <button type="button" className="user-card-id" onClick={() => setPage("esprit")}>
              <strong>{me.spirit_name || "Kachina"}</strong>
              <small>{me.email || "Esprit · tes agents y puisent"}</small>
            </button>
            <button className="out" title="Sortir" onClick={async () => { await api("/v1/auth/logout", { method: "POST" }); localStorage.removeItem("kachina_token"); setMe(null); }}>⎋</button>
          </div>
        </aside>
        <section className="main">
          <header className="top">
            <div className="brand-mini">
              <img src="assets/img/mark.png" alt="" />
              {NAV.find((n) => n[0] === page)?.[1] || "Kachina"}
            </div>
            <div className="top-actions">
              <span className="chip">{me.agent === "owner" ? "Toi" : me.agent}</span>
              <a className="chip" href="https://app-kachina-docs.pages.dev" target="_blank" rel="noreferrer">Docs</a>
            </div>
          </header>
          {clipNote && <p className="muted" style={{ padding: "0 1.5rem" }}>{clipNote}</p>}
          {page === "chat" && (
            <ChatView
              key={chatKey}
              seed={seedChat}
              spirit={me.spirit_name}
              conversationId={openCid}
              onConversation={(id) => { setOpenCid(id); localStorage.setItem("kachina_last_cid", id); refreshConvs(); }}
              onDropReady={(fn) => { ingestRef.current = fn; }}
            />
          )}
          {page === "library" && <LibraryView />}
          {page === "favorites" && <LibraryView favoritesOnly />}
          {page === "insights" && <InsightsView onOpen={setOpenMemory} />}
          {page === "themes" && <LibraryView startPane="themes" />}
          {page === "tasks" && <LibraryView startPane="tasks" />}
          {page === "processes" && <ProcessesView />}
          {page === "keys" && <KeysView spirit={me.spirit_name} />}
          {page === "connectors" && <ConnectorsView />}
          {page === "esprit" && (
            <SpiritView
              me={me}
              onSaved={(spirit_name) => setMe((cur) => ({ ...cur, spirit_name }))}
              onDeleted={() => setMe(null)}
            />
          )}
          {page === "activity" && <ActivityView />}
        </section>
      </div>
      {openMemory && (
        <MemorySheet id={openMemory} onClose={() => setOpenMemory(null)} onChanged={() => {}} />
      )}
      {cmdk && (
        <CommandK
          onClose={() => setCmdk(false)}
          onOpenMemory={(id) => { setOpenMemory(id); setPage("library"); }}
          onAsk={(text) => { setSeedChat(text); setPage("chat"); setOpenCid(""); setChatKey((k) => k + 1); }}
        />
      )}
      {drop && <div className="drop"><div className="drop-card"><h2>Lâche le fichier</h2><p className="muted">Image, PDF ou Word : je le lis et j’en tire les faits utiles.</p></div></div>}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("app")).render(<App />);
