/* global React, ReactDOM, lucide */
const { useState, useEffect, useRef, useCallback, useMemo } = React;

/* ---------- Tweakable defaults ---------- */
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": (typeof localStorage !== "undefined" ? localStorage.getItem("ethoryx_theme") || "light" : "light"),
  "compare": false,
  "density": "comfortable",
  "showLibrary": false,
  "storageMode": "local"
}/*EDITMODE-END*/;

/* ---------- Lucide icon wrapper ---------- */
function Icon({ name, size = 16, stroke = 1.6, className = "" }) {
  const ref = useRef(null);
  useEffect(() => {
    if (ref.current && window.lucide) {
      ref.current.innerHTML = "";
      const svg = window.lucide.createElement(window.lucide.icons[name] || window.lucide.icons.Circle);
      svg.setAttribute("width", size);
      svg.setAttribute("height", size);
      svg.setAttribute("stroke-width", stroke);
      ref.current.appendChild(svg);
    }
  }, [name, size, stroke]);
  return <span ref={ref} className={"inline-flex items-center justify-center " + className} aria-hidden="true" />;
}

/* ---------- Ethoryx mark ---------- */
function EthoryxMark({ size = 18, className = "" }) {
  return (
    <svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className} aria-hidden>
      <circle cx="16" cy="16" r="15" stroke="currentColor" strokeWidth="1.5"/>
      <path d="M9 16h14M9 11h10M9 21h8" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
      <circle cx="24" cy="11" r="2.5" fill="currentColor"/>
    </svg>
  );
}

function EthoryxMarkOld({ size = 22, className = "" }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <rect x="2.5" y="2.5" width="19" height="19" rx="5.5" stroke="currentColor" strokeWidth="1.4" />
      <path d="M8 12 L16 12" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
      <circle cx="12" cy="12" r="2.2" fill="currentColor" />
    </svg>
  );
}

/* ---------- Sovereign Shield (privacy emblem) ---------- */
function ShieldGlyph({ size = 14, strokeWidth = 1.6 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M12 2.5 L4.5 5.5 V12.5 C4.5 16.6 7.9 20.4 12 21.5 C16.1 20.4 19.5 16.6 19.5 12.5 V5.5 Z" />
      <path d="M9 12 L11 14 L15 10" />
    </svg>
  );
}

function LockGlyph({ size = 12, strokeWidth = 1.6 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <rect x="4" y="10" width="16" height="11" rx="2.5" />
      <path d="M8 10 V7 a4 4 0 0 1 8 0 V10" />
    </svg>
  );
}

/* ---------- Generic popover hook ---------- */
function usePopover() {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
    document.addEventListener("mousedown", onDoc);
    document.addEventListener("keydown", onKey);
    return () => { document.removeEventListener("mousedown", onDoc); document.removeEventListener("keydown", onKey); };
  }, [open]);
  return [open, setOpen, ref];
}

function MenuItem({ icon, label, hint, onClick, danger, tone }) {
  return (
    <button
      role="menuitem"
      onClick={onClick}
      className={"w-full flex items-center gap-2.5 px-3 h-9 text-[12.5px] tracking-tight transition-colors " + (danger
        ? "text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20"
        : tone === "gold"
        ? "text-goldink dark:text-goldsoft hover:bg-goldtint/60 dark:hover:bg-gold/10"
        : "text-ink dark:text-paper hover:bg-fog dark:hover:bg-graphite2")}
    >
      <Icon name={icon} size={14} className={danger ? "" : tone === "gold" ? "" : "text-mute dark:text-dim"} />
      <span className="flex-1 text-left">{label}</span>
      {hint && <span className="text-[10.5px] text-mute dark:text-dim font-mono">{hint}</span>}
    </button>
  );
}

function SegRow({ label, value, options, onChange }) {
  return (
    <div className="px-3 py-2">
      <div className="text-[10.5px] uppercase tracking-wider text-mute dark:text-dim mb-1.5">{label}</div>
      <div className="flex items-center gap-1">
        {options.map((o) => (
          <button
            key={o.value}
            onClick={() => onChange(o.value)}
            className={"flex-1 h-7 rounded-md text-[11.5px] tracking-tight transition-colors " + (value === o.value
              ? "bg-ink text-paper dark:bg-paper dark:text-ink"
              : "bg-fog dark:bg-graphite2 text-mute dark:text-dim hover:text-ink dark:hover:text-paper")}
          >{o.label}</button>
        ))}
      </div>
    </div>
  );
}

function ToggleRow({ label, sub, value, onChange }) {
  return (
    <button
      onClick={() => onChange(!value)}
      className="w-full flex items-center gap-3 px-3 py-2 text-left hover:bg-fog dark:hover:bg-graphite2 transition-colors"
    >
      <div className="flex-1 min-w-0">
        <div className="text-[12.5px] tracking-tight">{label}</div>
        {sub && <div className="text-[10.5px] text-mute dark:text-dim mt-0.5">{sub}</div>}
      </div>
      <span className={"relative w-9 h-5 rounded-full transition-colors shrink-0 " + (value ? "bg-ink dark:bg-paper" : "bg-fog dark:bg-graphite2 border border-line dark:border-hairline")}>
        <span className={"pill-thumb absolute top-0.5 w-4 h-4 rounded-full bg-paper dark:bg-ink shadow-sm " + (value ? "translate-x-4" : "translate-x-0.5")} />
      </span>
    </button>
  );
}

/* ---------- Header ---------- */
function Header({ theme, setTheme, compare, setCompare, libraryOpen, setLibraryOpen, libraryCount, storageMode, onNewThread, settings, setSettings, model, setModel, userName, userEmail, userInitials, onSignOut, onSaveProfile }) {
  const [shieldOpen, shieldSet, shieldRef] = usePopover();
  const [setOpen, setSetOpen, setRef] = usePopover();
  const [profOpen, setProfOpen, profRef] = usePopover();
  const [modelOpen, setModelOpen, modelRef] = usePopover();
  const [editingProfile, setEditingProfile] = useState(false);
  const [draftName, setDraftName] = useState(userName);
  const [draftEmail, setDraftEmail] = useState(userEmail);
  const currentModel = MODELS.find(m => m.id === model) || MODELS[0];

  return (
    <header className="fixed top-0 inset-x-0 z-40">
      <div className="h-14 px-6 flex items-center justify-between border-b border-line/70 dark:border-hairline bg-paper/70 dark:bg-ink/70 backdrop-blur-md">
        {/* Brand */}
        <div className="flex items-center gap-2.5">
          <span className="text-ink dark:text-paper"><EthoryxMark /></span>
          <div className="flex items-baseline gap-1.5">
            <span className="text-[15px] font-semibold tracking-tight font-display">Zera</span>
            <span className="text-[11px] text-mute dark:text-dim tracking-wide hidden sm:inline"> </span>
          </div>

          {/* Sovereign Shield — privacy emblem */}
          <div className="relative ml-2" ref={shieldRef}>
            <button
              onClick={() => shieldSet(!shieldOpen)}
              className={"h-7 pl-1.5 pr-2 rounded-full inline-flex items-center gap-1.5 border transition-colors " + (shieldOpen
                ? "border-gold/60 bg-goldtint/70 text-goldink dark:bg-gold/10 dark:text-goldsoft"
                : "border-line dark:border-hairline text-mute dark:text-dim hover:text-goldink dark:hover:text-goldsoft hover:border-gold/50")}
              aria-expanded={shieldOpen}
              aria-label="Sovereign Shield"
            >
              <span className="text-gold dark:text-goldsoft"><ShieldGlyph size={13} strokeWidth={1.8} /></span>
              <span className="text-[11px] tracking-tight font-medium">Sovereign</span>
              <span className="w-1 h-1 rounded-full bg-gold dark:bg-goldsoft" />
            </button>
            {shieldOpen && (
              <div role="dialog" className="absolute top-full left-0 mt-2 w-[320px] rounded-xl border border-line dark:border-hairline bg-paper dark:bg-graphite shadow-[0_24px_60px_-18px_rgba(0,0,0,0.25)] dark:shadow-[0_24px_60px_-12px_rgba(0,0,0,0.6)] p-4 z-50">
                <div className="flex items-start gap-2.5">
                  <div className="w-8 h-8 rounded-full bg-goldtint dark:bg-gold/15 grid place-items-center text-gold dark:text-goldsoft shrink-0">
                    <ShieldGlyph size={16} strokeWidth={1.7} />
                  </div>
                  <div className="min-w-0">
                    <div className="text-[12.5px] font-semibold tracking-tight">Your math, your data.</div>
                    <p className="text-[12px] leading-[1.55] text-mute dark:text-dim mt-1">
                      Ethoryx cannot read your vault unless you specifically grant session access. All indexing happens locally on your device.
                    </p>
                  </div>
                </div>
                <div className="mt-3 grid grid-cols-2 gap-2 text-[11px]">
                  <div className="rounded-lg border border-line dark:border-hairline px-2.5 py-2">
                    <div className="flex items-center gap-1.5 text-goldink dark:text-goldsoft">
                      <LockGlyph size={11} /><span className="tracking-tight font-medium">Vault</span>
                    </div>
                    <div className="text-mute dark:text-dim mt-1 capitalize">{storageMode === "local" ? "Local-only" : "Cloud sync"}</div>
                  </div>
                  <div className="rounded-lg border border-line dark:border-hairline px-2.5 py-2">
                    <div className="flex items-center gap-1.5"><Icon name="Cpu" size={11} /><span className="tracking-tight font-medium">Inference</span></div>
                    <div className="text-mute dark:text-dim mt-1">Constrained — TAPSN</div>
                  </div>
                </div>
              </div>
            )}
          </div>
        </div>

        {/* Model Comparison toggle */}
        <div className="absolute left-1/2 -translate-x-1/2">
          <button
            onClick={() => setCompare(!compare)}
            className="group flex items-center gap-2 h-8 pl-2.5 pr-1.5 rounded-full border border-line dark:border-hairline hover:border-ink/40 dark:hover:border-paper/30 transition-colors"
            aria-pressed={compare}
            aria-label="Model comparison"
          >
            <span className="text-[12px] tracking-tight text-mute dark:text-dim group-hover:text-ink dark:group-hover:text-paper">Compare models</span>
            <span className={"relative w-9 h-5 rounded-full transition-colors " + (compare ? "bg-ink dark:bg-paper" : "bg-fog dark:bg-graphite2")}>
              <span
                className={"pill-thumb absolute top-0.5 w-4 h-4 rounded-full bg-paper dark:bg-ink shadow-sm " + (compare ? "translate-x-4" : "translate-x-0.5")}
              />
            </span>
          </button>
        </div>

        {/* Right cluster */}
        <div className="flex items-center gap-1">
          <IconButton title="New thread (⌘ N)" onClick={onNewThread}><Icon name="SquarePen" size={16} /></IconButton>

          {/* Model selector */}
          <div className="relative mr-0.5" ref={modelRef}>
            <button
              onClick={() => setModelOpen(!modelOpen)}
              className="h-7 px-2.5 rounded-full inline-flex items-center gap-1.5 border border-line dark:border-hairline hover:border-ink/30 dark:hover:border-paper/25 text-mute dark:text-dim hover:text-ink dark:hover:text-paper transition-colors"
            >
              <span className="text-[13px]">{currentModel?.icon || "⚡"}</span>
              <span className="text-[11px] font-mono hidden sm:inline">{currentModel?.label || "TAI"}</span>
              <Icon name="ChevronDown" size={11} />
            </button>
            {modelOpen && (
              <div className="absolute top-full right-0 mt-2 w-52 rounded-xl border border-line dark:border-hairline bg-paper dark:bg-graphite shadow-[0_24px_60px_-18px_rgba(0,0,0,0.25)] dark:shadow-[0_24px_60px_-12px_rgba(0,0,0,0.6)] p-1 z-50">
                <div className="px-2.5 py-1.5 text-[10px] uppercase tracking-wider text-mute dark:text-dim">Model</div>
                {MODELS.map((m) => (
                  <button key={m.id} onClick={() => { setModel(m.id); setModelOpen(false); }}
                    className={"w-full text-left px-2.5 py-2 rounded-lg hover:bg-fog dark:hover:bg-graphite2 " + (m.id === model ? "bg-fog/70 dark:bg-graphite2/70" : "")}>
                    <div className="flex items-center gap-2">
                      <span className="text-[14px] shrink-0">{m.icon || "🧠"}</span>
                      <div className="flex-1 min-w-0">
                        <div className="text-[12.5px] font-medium leading-tight">{m.label}</div>
                        <div className="text-[10px] text-mute dark:text-dim mt-0.5">{m.badge === "FREE" ? "Free · Instant · CPU" : "GPU · Slower"}</div>
                      </div>
                      {m.id === model && <Icon name="Check" size={12} className="text-gold dark:text-goldsoft shrink-0" />}
                    </div>
                  </button>
                ))}
              </div>
            )}
          </div>

          {/* Settings */}
          <div className="relative" ref={setRef}>
            <IconButton title="Settings (⌘ ,)" onClick={() => setSetOpen(!setOpen)} active={setOpen}>
              <Icon name="Settings2" size={16} />
            </IconButton>
            {setOpen && (
              <div role="menu" className="absolute top-full right-0 mt-2 w-[300px] rounded-xl border border-line dark:border-hairline bg-paper dark:bg-graphite shadow-[0_24px_60px_-18px_rgba(0,0,0,0.25)] dark:shadow-[0_24px_60px_-12px_rgba(0,0,0,0.6)] z-50 overflow-hidden">
                <div className="px-3 pt-3 pb-1 flex items-center justify-between">
                  <div className="text-[12.5px] font-semibold tracking-tight">Settings</div>
                  <span className="text-[10.5px] text-mute dark:text-dim font-mono">{(MODELS.find(m => m.id === model) || MODELS[0])?.label}</span>
                </div>
                <SegRow
                  label="Model"
                  value={model}
                  onChange={(v) => setModel && setModel(v)}
                  options={MODELS.map(m => ({ value: m.id, label: m.label }))}
                />
                <SegRow
                  label="Reasoning depth"
                  value={settings.reasoning}
                  onChange={(v) => setSettings({ ...settings, reasoning: v })}
                  options={[
                    { value: "deep",     label: "Deep" },
                    { value: "balanced", label: "Balanced" },
                    { value: "quick",    label: "Quick" },
                  ]}
                />
                <div className="h-px bg-line dark:bg-hairline mx-3 my-1" />
                <ToggleRow label="Stream tokens"      sub="Show output as it generates" value={settings.stream}    onChange={(v) => setSettings({ ...settings, stream: v })} />
                <ToggleRow label="Autosave to vault"  sub="Every artifact — locally"      value={settings.autosave}  onChange={(v) => setSettings({ ...settings, autosave: v })} />
                <ToggleRow label="Voice input"         sub="Hold-to-talk dictation"       value={settings.voice}     onChange={(v) => setSettings({ ...settings, voice: v })} />
                <ToggleRow label="Sound effects"       sub="Subtle confirmations"          value={settings.sound}     onChange={(v) => setSettings({ ...settings, sound: v })} />
                <div className="px-3 py-1.5 text-[10.5px] text-mute dark:text-dim border-t border-line dark:border-hairline inline-flex items-center gap-1.5">
                  <span className="text-gold dark:text-goldsoft"><LockGlyph size={9} /></span>
                  All preferences stored locally
                </div>
              </div>
            )}
          </div>

          <IconButton
            title={theme === "dark" ? "Light mode" : "Dark mode"}
            onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
          >
            <Icon name={theme === "dark" ? "Sun" : "Moon"} size={16} />
          </IconButton>
          <div className="w-px h-5 bg-line dark:bg-hairline mx-1.5" />
          <button
            onClick={() => setLibraryOpen(!libraryOpen)}
            className="h-8 pl-2 pr-2.5 rounded-full border border-line dark:border-hairline hover:border-ink/40 dark:hover:border-paper/30 flex items-center gap-1.5 transition-colors"
            aria-pressed={libraryOpen}
          >
            <span className="text-gold dark:text-goldsoft"><LockGlyph size={12} /></span>
            <span className="text-[12px] tracking-tight">Vault</span>
            <span className="text-[11px] text-mute dark:text-dim tabular-nums">{libraryCount}</span>
          </button>

          {/* Profile */}
          <div className="relative ml-1.5" ref={profRef}>
            <button
              onClick={() => setProfOpen(!profOpen)}
              className={"w-7 h-7 rounded-full grid place-items-center text-[11px] font-medium transition-colors " + (profOpen
                ? "bg-ink text-paper dark:bg-paper dark:text-ink ring-2 ring-gold/40"
                : "bg-fog dark:bg-graphite2 hover:ring-2 hover:ring-line dark:hover:ring-hairline")}
              aria-haspopup="menu"
              aria-expanded={profOpen}
            >{userInitials}</button>
            {profOpen && (
              <div role="menu" className="absolute top-full right-0 mt-2 w-[268px] rounded-xl border border-line dark:border-hairline bg-paper dark:bg-graphite shadow-[0_24px_60px_-18px_rgba(0,0,0,0.25)] dark:shadow-[0_24px_60px_-12px_rgba(0,0,0,0.6)] z-50 overflow-hidden">
                {!editingProfile ? (
                  <>
                    <div className="px-3 pt-3 pb-3 flex items-center gap-2.5">
                      <div className="w-9 h-9 rounded-full bg-ink text-paper dark:bg-paper dark:text-ink grid place-items-center text-[12px] font-medium flex-shrink-0">{userInitials}</div>
                      <div className="min-w-0 flex-1">
                        <div className="text-[12.5px] font-medium tracking-tight truncate">{userName}</div>
                        <div className="text-[11px] text-mute dark:text-dim truncate">{userEmail || "No email set"}</div>
                      </div>
                      <button onClick={() => { setDraftName(userName); setDraftEmail(userEmail); setEditingProfile(true); }}
                        className="text-[11px] text-mute dark:text-dim hover:text-ink dark:hover:text-paper px-2 py-1 rounded-md hover:bg-fog dark:hover:bg-graphite2 transition-colors shrink-0">
                        Edit
                      </button>
                    </div>
                    <div className="h-px bg-line dark:bg-hairline" />
                    <MenuItem icon="Keyboard"   label="Shortcuts"       hint="⌘ ⇧ K" />
                    <MenuItem icon="Headphones" label="Help & support"  onClick={() => { setProfOpen(false); window.open("https://ethoryx.io/docs", "_blank"); }} />
                    <div className="h-px bg-line dark:bg-hairline" />
                    <MenuItem icon="LogOut" label="Sign out" danger onClick={() => { setProfOpen(false); onSignOut && onSignOut(); }} />
                  </>
                ) : (
                  <div className="px-3 pt-3 pb-3">
                    <div className="text-[12px] font-semibold tracking-tight mb-3">Edit profile</div>
                    <div className="space-y-2">
                      <div>
                        <div className="text-[10.5px] uppercase tracking-wider text-mute dark:text-dim mb-1">Display name</div>
                        <input
                          type="text"
                          value={draftName}
                          onChange={e => setDraftName(e.target.value)}
                          className="w-full h-8 px-2.5 rounded-lg border border-line dark:border-hairline bg-fog dark:bg-graphite2 text-[13px] outline-none focus:border-gold/50 dark:focus:border-goldsoft/50 transition-colors"
                          placeholder="Your name"
                          autoFocus
                        />
                      </div>
                      <div>
                        <div className="text-[10.5px] uppercase tracking-wider text-mute dark:text-dim mb-1">Email</div>
                        <input
                          type="email"
                          value={draftEmail}
                          onChange={e => setDraftEmail(e.target.value)}
                          className="w-full h-8 px-2.5 rounded-lg border border-line dark:border-hairline bg-fog dark:bg-graphite2 text-[13px] outline-none focus:border-gold/50 dark:focus:border-goldsoft/50 transition-colors"
                          placeholder="email@example.com"
                        />
                      </div>
                    </div>
                    <div className="flex gap-2 mt-3">
                      <button onClick={() => setEditingProfile(false)}
                        className="flex-1 h-8 rounded-lg border border-line dark:border-hairline text-[12px] hover:bg-fog dark:hover:bg-graphite2 transition-colors">
                        Cancel
                      </button>
                      <button onClick={() => {
                        const name = draftName.trim() || "Ethoryx User";
                        const email = draftEmail.trim();
                        localStorage.setItem("ethoryx_user_name", name);
                        localStorage.setItem("ethoryx_user_email", email);
                        onSaveProfile && onSaveProfile(name, email);
                        setEditingProfile(false);
                      }}
                        className="flex-1 h-8 rounded-lg bg-ink dark:bg-paper text-paper dark:text-ink text-[12px] font-medium hover:opacity-90 transition-opacity">
                        Save
                      </button>
                    </div>
                  </div>
                )}
              </div>
            )}
          </div>
        </div>
      </div>
    </header>
  );
}

/* ---------- TAPSN Verified badge ---------- */
function TAPSNBadge() {
  return (
    <span className="group relative inline-flex items-center gap-1 h-5 pl-1.5 pr-1.5 rounded-full border border-gold/45 bg-goldtint/70 dark:bg-gold/10 text-goldink dark:text-goldsoft text-[10px] font-medium tracking-tight cursor-default select-none">
      <span className="-ml-0.5"><ShieldGlyph size={9} strokeWidth={2} /></span>
      <span className="leading-none">TAPSN Verified</span>
      <span
        role="tooltip"
        className="pointer-events-none absolute top-full left-1/2 -translate-x-1/2 mt-1.5 w-[260px] invisible opacity-0 translate-y-0.5 group-hover:visible group-hover:opacity-100 group-hover:translate-y-0 transition-all rounded-lg bg-ink dark:bg-paper text-paper dark:text-ink text-[11.5px] leading-[1.5] font-normal px-3 py-2 z-50 shadow-[0_10px_30px_-8px_rgba(0,0,0,0.35)]"
      >
        <span className="block font-medium text-goldsoft dark:text-gold">TAPSN constraint active</span>
        <span className="block text-paper/80 dark:text-ink/80 mt-0.5">Output constrained by Theorem 7 &amp; 9 — 0% Hallucination risk.</span>
        <span className="absolute -top-1 left-1/2 -translate-x-1/2 w-2 h-2 rotate-45 bg-ink dark:bg-paper" />
      </span>
    </span>
  );
}

function IconButton({ children, title, onClick, active }) {
  return (
    <button
      onClick={onClick}
      title={title}
      className={"w-8 h-8 rounded-full grid place-items-center transition-colors " + (active
        ? "bg-fog dark:bg-graphite2 text-ink dark:text-paper"
        : "hover:bg-fog dark:hover:bg-graphite2 text-mute dark:text-dim hover:text-ink dark:hover:text-paper")}
    >
      {children}
    </button>
  );
}

/* ---------- Message bubbles ---------- */
function UserBubble({ children, msgId, onReply, onEdit }) {
  const text = typeof children === "string" ? children : "";
  const timeStr = msgId ? new Date(msgId).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
  return (
    <div className="rise flex justify-end items-end gap-2 group">
      {/* Vertical action bar — left of bubble, hover-only */}
      <div className="flex flex-col items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity self-end pb-0.5">
        {timeStr && (
          <span className="text-[10px] text-mute dark:text-dim tabular-nums whitespace-nowrap mb-0.5">{timeStr}</span>
        )}
        {onReply && (
          <button onClick={onReply} title="Reply"
            className="w-6 h-6 rounded-md grid place-items-center text-mute dark:text-dim hover:bg-fog dark:hover:bg-graphite2 hover:text-ink dark:hover:text-paper transition-colors">
            <Icon name="CornerUpLeft" size={12} />
          </button>
        )}
        <button onClick={() => navigator.clipboard?.writeText(text)} title="Copy"
          className="w-6 h-6 rounded-md grid place-items-center text-mute dark:text-dim hover:bg-fog dark:hover:bg-graphite2 hover:text-ink dark:hover:text-paper transition-colors">
          <Icon name="Copy" size={12} />
        </button>
        {onEdit && (
          <button onClick={() => onEdit(text)} title="Edit"
            className="w-6 h-6 rounded-md grid place-items-center text-mute dark:text-dim hover:bg-fog dark:hover:bg-graphite2 hover:text-ink dark:hover:text-paper transition-colors">
            <Icon name="Pencil" size={12} />
          </button>
        )}
      </div>
      <div className="max-w-[72%] rounded-2xl rounded-br-md bg-fog dark:bg-graphite2 px-4 py-2.5 text-[14.5px] leading-relaxed">
        {children}
      </div>
    </div>
  );
}

function CertaintyDot({ certainty, z_score, latency_ms }) {
  // Don't show for the default "RELIABLE" — it's noise when every response has it
  if (!certainty || certainty === "RELIABLE" || certainty === "HIGHLY RELIABLE") return null;
  
  const color =
    certainty.startsWith("PROVEN") ? "#22c55e" :
    certainty.startsWith("UNDERSTOOD") ? "#22c55e" :
    certainty.startsWith("REASONED") ? "#6366f1" :
    certainty.startsWith("ENRICHED") ? "#eab308" :
    certainty.startsWith("GENERATED") ? "#3b82f6" :
    certainty === "UNKNOWN" || certainty === "LIMITATION" ? "#9ca3af" :
    "#eab308";
  const label = certainty || "UNKNOWN";
  return (
    <span className="group relative inline-flex items-center" title={label}>
      <span
        className="w-2 h-2 rounded-full inline-block cursor-default"
        style={{ background: color }}
      />
      <span
        role="tooltip"
        className="pointer-events-none absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-[200px] invisible opacity-0 translate-y-0.5 group-hover:visible group-hover:opacity-100 group-hover:translate-y-0 transition-all rounded-lg bg-ink dark:bg-paper text-paper dark:text-ink text-[11.5px] leading-[1.5] px-3 py-2 z-50 shadow-lg"
      >
        <span className="block font-medium">{label}</span>
        {z_score != null && <span className="block text-paper/70 dark:text-ink/70">z = {z_score}</span>}
        {latency_ms != null && <span className="block text-paper/70 dark:text-ink/70">{latency_ms}ms</span>}
        <span className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-2 h-2 rotate-45 bg-ink dark:bg-paper" />
      </span>
    </span>
  );
}

function AssistantBlock({ children, density, certainty, z_score, latency_ms, isDontKnow, file_url, email_html, email_subject, subject, generated_text, text, raw_text, reasoning, confidence }) {
  return (
    <div className="rise">
      <div className="flex items-center gap-2 mb-2">
        <span className="text-ink dark:text-paper"><EthoryxMark size={14} /></span>
        {certainty && <CertaintyDot certainty={certainty} z_score={z_score} latency_ms={latency_ms} />}
        {latency_ms && <span className="text-[10px] text-mute dark:text-dim tabular-nums">{latency_ms}ms</span>}
      </div>

      <ReasoningTrace steps={reasoning} confidence={confidence} />

      <div className={"text-[15px] leading-[1.65] " + (density === "compact" ? "space-y-2.5" : "space-y-3.5") + (isDontKnow ? " italic text-mute dark:text-dim" : "")}>
        {email_html ? null : children}
      </div>
      
      {/* Absolute Fallback Binding Pass */}
      {email_html && (
  <EmailCard
    email_html={email_html}
    email_subject={email_subject}
    text={text || raw_text}
  />
)}
      
      {file_url && <FileArtifact url={file_url} />}
    </div>
  );
}

/* ---------- File artifact inline (PPTX/DOCX/XLSX downloads) ---------- */
function FileArtifact({ url }) {
  if (!url) return null;
  const fullUrl = url.startsWith("http") ? url : CHAT_API + url;
  const name = url.split("/").pop() || "file";
  const ext = (name.match(/\.([^.]+)$/) || [])[1] || "";
  const typeLabel = { pptx: "Presentation", docx: "Document", pdf: "PDF", xlsx: "Spreadsheet" }[ext.toLowerCase()] || "File";
  const iconName = { pptx: "Presentation", docx: "FileText", pdf: "FileText", xlsx: "Sheet" }[ext.toLowerCase()] || "File";
  return (
    <a
      href={fullUrl}
      target="_blank"
      rel="noopener noreferrer"
      download
      className="group mt-3 inline-flex items-center gap-3 rounded-xl border border-line dark:border-hairline hover:border-gold/40 dark:hover:border-gold/30 px-3.5 py-2.5 transition-colors bg-paper dark:bg-graphite hover:bg-fog/50 dark:hover:bg-graphite2/50"
    >
      <div className="w-8 h-8 rounded-lg bg-goldtint/70 dark:bg-gold/15 grid place-items-center text-goldink dark:text-goldsoft shrink-0">
        <Icon name={iconName} size={15} />
      </div>
      <div className="min-w-0 flex-1">
        <div className="text-[12.5px] font-medium tracking-tight truncate">{name}</div>
        <div className="text-[10.5px] text-mute dark:text-dim">{typeLabel} · tap to download</div>
      </div>
      <span className="text-mute dark:text-dim group-hover:text-goldink dark:group-hover:text-goldsoft transition-colors">
        <Icon name="Download" size={14} />
      </span>
    </a>
  );
}

/* ---------- Email card (Claude-style: Subject label + structured body) ---------- */
// Email layouts the user can choose between. Add a new {id,label} here + a branch
// in EmailCard to extend with more designs later.
const EMAIL_LAYOUTS = [
  { id: 'card',  label: 'Card' },
  { id: 'gmail', label: 'Gmail' },
];

// Shared layout switcher (small segmented control) used in every layout's header.
function EmailLayoutSwitch({ layout, setLayout }) {
  return (
    <div className="flex items-center gap-0.5">
      {EMAIL_LAYOUTS.map(l => (
        <button key={l.id} onClick={() => setLayout(l.id)}
          className={`px-2 py-0.5 rounded-md text-[10.5px] font-medium transition-colors ${
            layout === l.id
              ? 'bg-paper dark:bg-graphite text-ink dark:text-paper border border-line dark:border-hairline'
              : 'text-mute dark:text-dim hover:text-ink dark:hover:text-paper'}`}>
          {l.label}
        </button>
      ))}
    </div>
  );
}

// Layout 2 — Gmail-style editable draft (To / Subject / Body + Send via Gmail).
function GmailDraftLayout({ subject, body, recipient, switcher }) {
  const [to, setTo] = useState(recipient || '');
  const [subj, setSubj] = useState(subject || '');
  const [bod, setBod] = useState(body || '');
  const [copied, setCopied] = useState(false);
  useEffect(() => { setTo(recipient || ''); }, [recipient]);
  useEffect(() => { setSubj(subject || ''); }, [subject]);
  useEffect(() => { setBod(body || ''); }, [body]);

  const copy = () => {
    navigator.clipboard?.writeText(`Subject: ${subj}\n\n${bod}`).then(() => {
      setCopied(true); setTimeout(() => setCopied(false), 1500);
    });
  };
  const gmail = () => {
    const p = new URLSearchParams({ view: 'cm', fs: '1', su: subj, body: bod });
    if (to) p.set('to', to);
    window.open(`https://mail.google.com/mail/?${p.toString()}`, '_blank');
  };
  const fieldCls = "rounded-lg border border-line dark:border-hairline px-3 py-2 bg-fog/30 dark:bg-graphite2/40";

  return (
    <div className="mt-3 rounded-2xl border border-line dark:border-hairline overflow-hidden bg-paper dark:bg-graphite shadow-sm text-left w-full">
      <div className="flex items-center justify-between px-4 py-2.5 border-b border-line dark:border-hairline bg-fog/40 dark:bg-graphite2/60">
        <span className="text-[12px] font-semibold tracking-tight text-ink dark:text-paper">Email draft</span>
        {switcher}
      </div>
      <div className="px-4 py-3 space-y-2.5">
        <div className={`flex items-center gap-2 ${fieldCls}`}>
          <label className="text-[12px] text-mute dark:text-dim w-9 shrink-0">To</label>
          <input value={to} onChange={e => setTo(e.target.value)} placeholder="recipient@example.com"
            className="flex-1 bg-transparent text-[13px] text-ink dark:text-paper outline-none" />
        </div>
        <div className={fieldCls}>
          <input value={subj} onChange={e => setSubj(e.target.value)} placeholder="Subject"
            className="w-full bg-transparent text-[13.5px] font-semibold text-ink dark:text-paper outline-none" />
        </div>
        <div className={fieldCls}>
          <textarea value={bod} onChange={e => setBod(e.target.value)} rows={8}
            className="w-full bg-transparent text-[13.5px] leading-[1.7] text-ink dark:text-paper outline-none resize-y" />
        </div>
      </div>
      <div className="flex items-center justify-between px-4 py-2.5 border-t border-line dark:border-hairline bg-fog/40 dark:bg-graphite2/50">
        <button onClick={copy}
          className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-[11.5px] font-medium text-mute dark:text-dim hover:text-ink dark:hover:text-paper hover:bg-paper dark:hover:bg-graphite transition-colors">
          <Icon name={copied ? "Check" : "Copy"} size={11} />{copied ? 'Copied' : 'Copy'}
        </button>
        <button onClick={gmail}
          className="inline-flex items-center gap-1.5 px-3 py-1 rounded-md text-[11.5px] font-medium bg-paper dark:bg-graphite border border-line dark:border-hairline text-ink dark:text-paper hover:border-gold/40 transition-colors">
          <Icon name="Send" size={11} />Send via Gmail
        </button>
      </div>
    </div>
  );
}

function EmailCard({email_html, email_subject, text }) {
  const [copied, setCopied] = useState(false);
  // User-chosen layout, remembered across emails/sessions.
  const [layout, setLayout] = useState(() => {
    try { return localStorage.getItem('ethoryx_email_layout') || 'card'; } catch (e) { return 'card'; }
  });
  useEffect(() => { try { localStorage.setItem('ethoryx_email_layout', layout); } catch (e) {} }, [layout]);

  // email_subject prop is the authoritative source (extracted from data.subject on the server).
  // Only fall back to parsing raw text if the prop is absent.
  const cleanSubject = useMemo(() => {
    if (email_subject) {
      const firstLine = email_subject.replace(/<[^>]+>/g, '').trim().split(/\r?\n/)[0].trim();
      return firstLine.length > 150 ? firstLine.slice(0, 150) : firstLine;
    }
    if (text) {
      const m = text.match(/^Subject:\s*(.+?)(?:\r?\n|$)/i);
      if (m && m[1].trim()) return m[1].trim().slice(0, 200);
    }
    return 'Email Draft';
  }, [email_subject, text]);

  const cleanBodyText = useMemo(() => {
    if (!text) return "";
    return text.replace(/^Subject:\s*[^\r\n]*\r?\n?/i, '').trim();
  }, [text]);

  // Plain-text body for copy/Gmail — prefer email_html content over raw text
  const emailBodyText = useMemo(() => {
    if (email_html) {
      return email_html
        .replace(/<\/p>\s*<p/gi, '\n\n<p')
        .replace(/<br\s*\/?>/gi, '\n')
        .replace(/<\/div>\s*<p/gi, '\n\n<p')
        .replace(/<[^>]+>/g, '')
        .replace(/\n{3,}/g, '\n\n')
        .trim();
    }
    return cleanBodyText;
  }, [email_html, cleanBodyText]);

  // Recipient parsed from the greeting line ("Hi Kathy," / "Dear Kathy,") for the To field.
  const recipient = useMemo(() => {
    const m = (emailBodyText || '').match(/^\s*(?:Hi|Hello|Dear|Hey)\s+([^,\n]{1,60}),/i);
    const name = m ? m[1].trim() : '';
    return /\[.*\]/.test(name) ? '' : name;   // skip "[Name/Team]" placeholders
  }, [emailBodyText]);

  const handleCopy = () => {
    navigator.clipboard?.writeText(`Subject: ${cleanSubject}\n\n${emailBodyText}`).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 1500);
    });
  };

  const handleGmail = () => {
    const sub = encodeURIComponent(cleanSubject);
    const body = encodeURIComponent(emailBodyText);
    window.open(`https://mail.google.com/mail/?view=cm&fs=1&su=${sub}&body=${body}`, '_blank');
  };

  const switcher = <EmailLayoutSwitch layout={layout} setLayout={setLayout} />;

  // ── Layout 2: Gmail-style editable draft ──
  if (layout === 'gmail') {
    return <GmailDraftLayout subject={cleanSubject} body={emailBodyText}
                             recipient={recipient} switcher={switcher} />;
  }

  // ── Layout 1 (default): Card ──
  return (
    <div className="mt-3 rounded-2xl border border-line dark:border-hairline overflow-hidden bg-paper dark:bg-graphite shadow-sm text-left w-full">
      {/* Subject Header Block */}
      <div className="flex items-center gap-3 px-5 py-3 bg-fog/40 dark:bg-graphite2/60 border-b border-line dark:border-hairline">
        <span className="text-[11px] font-semibold text-mute dark:text-dim uppercase tracking-wider shrink-0">Subject</span>
        <span className="text-[14px] font-semibold tracking-tight text-ink dark:text-paper leading-snug flex-1 min-w-0 break-words">
          {cleanSubject}
        </span>
        {switcher}
      </div>

      {/* Main Content Body Block */}
      <div className="px-5 py-5 text-[14px] leading-[1.7] text-ink/95 dark:text-paper/90 text-left">
        {email_html ? (
          <div
            className="email-preview-html space-y-4 text-left"
            dangerouslySetInnerHTML={{ __html: email_html }}
          />
        ) : (
          <div className="whitespace-pre-wrap text-left break-words">
            {cleanBodyText || "No content available"}
          </div>
        )}
      </div>

      {/* Footer Controls */}
      <div className="flex items-center justify-between px-4 py-2.5 border-t border-line dark:border-hairline bg-fog/40 dark:bg-graphite2/50">
        <button onClick={handleCopy}
          className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-[11.5px] font-medium text-mute dark:text-dim hover:text-ink dark:hover:text-paper hover:bg-paper dark:hover:bg-graphite transition-colors">
          <Icon name={copied ? "Check" : "Copy"} size={11} />
          {copied ? 'Copied' : 'Copy'}
        </button>
        <button onClick={handleGmail}
          className="inline-flex items-center gap-1.5 px-3 py-1 rounded-md text-[11.5px] font-medium bg-paper dark:bg-graphite border border-line dark:border-hairline text-ink dark:text-paper hover:border-gold/40 transition-colors">
          <Icon name="Send" size={11} />
          Send via Gmail
        </button>
      </div>
    </div>
  );
}
/* ---------- Local save helper (native Save As dialog) ---------- */
async function saveToLocal(filename, content, mime = "text/plain") {
  const blob = new Blob([content], { type: mime });
  if (window.showSaveFilePicker) {
    try {
      const ext = (filename.match(/\.[^.]+$/) || [""])[0];
      const handle = await window.showSaveFilePicker({
        suggestedName: filename,
        types: ext ? [{ description: `${ext.slice(1).toUpperCase()} file`, accept: { [mime]: [ext] } }] : undefined,
      });
      const w = await handle.createWritable();
      await w.write(blob);
      await w.close();
      return { ok: true, native: true };
    } catch (e) {
      if (e && e.name === "AbortError") return { ok: false, canceled: true };
    }
  }
  // Fallback — browser-managed download to default folder.
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url; a.download = filename; document.body.appendChild(a); a.click();
  setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 0);
  return { ok: true, native: false };
}

function useSaveStatus() {
  const [status, setStatus] = useState(null); // {filename, native} | null
  const trigger = useCallback(async (filename, content, mime) => {
    const r = await saveToLocal(filename, content, mime);
    if (r.ok) {
      setStatus({ filename, native: r.native });
      setTimeout(() => setStatus((s) => (s && s.filename === filename ? null : s)), 2400);
    }
  }, []);
  return [status, trigger];
}

/* ---------- Smart Preview Cards ---------- */
function CodeCard({ filename, lang, lines, onOpen }) {
  const preview = lines.slice(0, 6);
  const moreLines = Math.max(0, lines.length - 6);
  const [status, save] = useSaveStatus();
  return (
    <div className="border border-line dark:border-hairline rounded-xl overflow-hidden bg-paper dark:bg-graphite">
      <div className="flex items-center justify-between px-3.5 h-10 border-b border-line dark:border-hairline">
        <div className="flex items-center gap-2 min-w-0">
          <Icon name="FileCode2" size={14} className="text-mute dark:text-dim" />
          <span className="text-[12.5px] font-mono truncate">{filename}</span>
          <span className="text-[10.5px] uppercase tracking-wider text-mute dark:text-dim border border-line dark:border-hairline rounded px-1.5 py-0.5 ml-1">{lang}</span>
          <SaveStatusInline status={status} />
        </div>
        <div className="flex items-center gap-0.5">
          <CardChip icon="Copy" label="Copy" onClick={() => navigator.clipboard?.writeText(lines.join("\n"))} />
          <CardChip icon="Maximize2" label="Expand" onClick={onOpen} />
          <CardChip icon="FolderDown" label="Save locally" tone="gold" onClick={() => save(filename, lines.join("\n"), "text/x-python")} />
        </div>
      </div>
      <pre className="text-[12.5px] font-mono leading-[1.7] px-4 py-3.5 overflow-hidden text-ink/85 dark:text-paper/85">
        {preview.map((line, i) => (
          <div key={i} className="flex">
            <span className="select-none w-7 text-mute/60 dark:text-dim/60 tabular-nums">{i + 1}</span>
            <span dangerouslySetInnerHTML={{ __html: syntaxColor(line) }} />
          </div>
        ))}
        {moreLines > 0 && (
          <div className="flex pt-1">
            <span className="select-none w-7 text-mute/60 dark:text-dim/60" />
            <span className="text-mute dark:text-dim text-[11.5px]">+{moreLines} more lines</span>
          </div>
        )}
      </pre>
    </div>
  );
}

function syntaxColor(s) {
  // Lightweight token coloring for visual rhythm — strings, comments, keywords, numbers.
  const esc = (t) => t.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  let out = esc(s);
  out = out.replace(/(#.*$)/g, '<span style="color:#9A9A95">$1</span>');
  out = out.replace(/("[^"]*"|'[^']*')/g, '<span style="color:#6E8B6B">$1</span>');
  out = out.replace(/\b(def|return|for|in|if|else|import|from|class|None|True|False|lambda|while|with|as)\b/g, '<span style="color:#7E6DAD">$1</span>');
  out = out.replace(/\b(\d+\.?\d*)\b/g, '<span style="color:#B07A4A">$1</span>');
  return out;
}

function DataCard({ filename, rows, columns, sample }) {
  const [status, save] = useSaveStatus();
  const csv = useMemo(() => {
    const head = columns.join(",");
    const body = sample.map((r) => r.join(",")).join("\n");
    return head + "\n" + body + "\n";
  }, [columns, sample]);
  return (
    <div className="border border-line dark:border-hairline rounded-xl overflow-hidden bg-paper dark:bg-graphite">
      <div className="flex items-center justify-between px-3.5 h-10 border-b border-line dark:border-hairline">
        <div className="flex items-center gap-2">
          <Icon name="Sheet" size={14} className="text-mute dark:text-dim" />
          <span className="text-[12.5px] font-mono">{filename}</span>
          <span className="text-[11px] text-mute dark:text-dim">· {rows.toLocaleString()} rows · {columns.length} cols</span>
          <SaveStatusInline status={status} />
        </div>
        <div className="flex items-center gap-0.5">
          <CardChip icon="Share2" label="Share" />
          <CardChip icon="FolderDown" label="Save locally" tone="gold" onClick={() => save(filename, csv, "text/csv")} />
        </div>
      </div>
      <div className="text-[12.5px]">
        <div className="grid border-b border-line dark:border-hairline" style={{ gridTemplateColumns: `repeat(${columns.length}, minmax(0,1fr))` }}>
          {columns.map((c) => (
            <div key={c} className="px-3.5 py-2 text-[11px] uppercase tracking-wider text-mute dark:text-dim font-medium">{c}</div>
          ))}
        </div>
        {sample.map((row, i) => (
          <div
            key={i}
            className={"grid font-mono tabular-nums " + (i < sample.length - 1 ? "border-b border-line/70 dark:border-hairline/70" : "")}
            style={{ gridTemplateColumns: `repeat(${columns.length}, minmax(0,1fr))` }}
          >
            {row.map((v, j) => (
              <div key={j} className="px-3.5 py-2 text-[12.5px]">{v}</div>
            ))}
          </div>
        ))}
        <div className="px-3.5 py-2 text-[11.5px] text-mute dark:text-dim border-t border-line dark:border-hairline">
          Showing 4 of {rows.toLocaleString()} rows
        </div>
      </div>
    </div>
  );
}

function ReportCard({ filename, pages, summary }) {
  const [status, save] = useSaveStatus();
  return (
    <div className="border border-line dark:border-hairline rounded-xl overflow-hidden bg-paper dark:bg-graphite flex">
      <div className="w-[88px] shrink-0 border-r border-line dark:border-hairline bg-fog/60 dark:bg-graphite2 grid place-items-center">
        <div className="w-12 h-16 rounded-sm bg-paper dark:bg-ink shadow-sm border border-line dark:border-hairline flex flex-col p-1.5 gap-1">
          <div className="h-0.5 bg-line dark:bg-hairline rounded-full" />
          <div className="h-0.5 bg-line dark:bg-hairline rounded-full w-3/4" />
          <div className="h-0.5 bg-line dark:bg-hairline rounded-full" />
          <div className="h-0.5 bg-line dark:bg-hairline rounded-full w-2/3" />
          <div className="mt-auto text-[7px] font-mono text-mute dark:text-dim text-center">PDF</div>
        </div>
      </div>
      <div className="flex-1 px-4 py-3 min-w-0">
        <div className="flex items-center justify-between gap-3">
          <div className="min-w-0">
            <div className="text-[13px] font-medium truncate">{filename}</div>
            <div className="text-[11.5px] text-mute dark:text-dim mt-0.5">{pages} pages · Mathematical report</div>
          </div>
          <div className="flex items-center gap-0.5 shrink-0">
            <CardChip icon="Share2" label="Share" />
            <CardChip icon="FolderDown" label="Save locally" tone="gold" onClick={() => save(filename, summary, "application/pdf")} />
          </div>
        </div>
        <SaveStatusInline status={status} className="mt-1" />
        <p className="text-[12.5px] text-mute dark:text-dim mt-2 leading-[1.55] line-clamp-2">{summary}</p>
      </div>
    </div>
  );
}

function CardChip({ icon, label, primary, tone, onClick }) {
  const isGold = tone === "gold";
  return (
    <button
      onClick={onClick}
      className={
        "h-7 px-2 rounded-md inline-flex items-center gap-1.5 text-[11.5px] transition-colors " +
        (isGold
          ? "text-goldink dark:text-goldsoft hover:bg-goldtint/70 dark:hover:bg-gold/15"
          : primary
          ? "text-ink dark:text-paper hover:bg-fog dark:hover:bg-graphite2"
          : "text-mute dark:text-dim hover:bg-fog dark:hover:bg-graphite2 hover:text-ink dark:hover:text-paper")
      }
      title={label}
    >
      <Icon name={icon} size={13} />
      <span className="hidden sm:inline">{label}</span>
    </button>
  );
}

function SaveStatusInline({ status, className = "" }) {
  if (!status) return null;
  return (
    <span className={"inline-flex items-center gap-1 text-[10.5px] text-goldink dark:text-goldsoft tracking-tight " + className}>
      <Icon name="CheckCircle2" size={11} />
      <span>{status.native ? "Saved to local folder" : "Saved to Downloads"}</span>
    </span>
  );
}

/* ---------- Compare cards ---------- */
function CompareGrid({ left, right }) {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
      <CompareCol label="Ethoryx · Sigma 2.4" tone="primary">{left}</CompareCol>
      <CompareCol label="Claude 4.5 Sonnet" tone="muted">{right}</CompareCol>
    </div>
  );
}

function CompareCol({ label, tone, children }) {
  return (
    <div className={"border rounded-xl p-4 " + (tone === "primary"
      ? "border-ink/15 dark:border-paper/15 bg-paper dark:bg-graphite"
      : "border-line dark:border-hairline bg-fog/40 dark:bg-graphite2/40")}>
      <div className="flex items-center gap-2 mb-2.5">
        {tone === "primary" ? <EthoryxMark size={12} /> : <span className="w-3 h-3 rounded-full border border-mute dark:border-dim" />}
        <span className="text-[11.5px] tracking-tight text-mute dark:text-dim">{label}</span>
        {tone === "primary" && (
          <span className="ml-auto text-[10.5px] uppercase tracking-wider text-mute dark:text-dim border border-line dark:border-hairline rounded px-1.5 py-0.5">Preferred</span>
        )}
      </div>
      <div className="text-[14px] leading-[1.65] space-y-2">{children}</div>
    </div>
  );
}

/* ---------- Floating Action Bar (fully functional) ---------- */
function ActionBar({ onSend, onSendWithFile, lang, setLang, vaultOpen, model, setModel, thinking, setInputRef }) {
  const [val, setVal] = useState("");
  const [langOpen, setLangOpen] = useState(false);
  const [listening, setListening] = useState(false);
  const [attachedFile, setAttachedFile] = useState([]);   // array of attachments
  const textRef = useRef(null);
  const fileInputRef = useRef(null);
  const recognitionRef = useRef(null);

  // Expose a setter so parent can inject text (for Edit action)
  useEffect(() => {
    if (setInputRef) setInputRef.current = (text) => { setVal(text); setTimeout(() => textRef.current?.focus(), 50); };
  }, [setInputRef]);

  // Auto-resize textarea
  useEffect(() => {
    if (!textRef.current) return;
    textRef.current.style.height = "auto";
    textRef.current.style.height = Math.min(textRef.current.scrollHeight, 180) + "px";
  }, [val]);

  const submit = () => {
    if (thinking) return;
    const text = val.trim();
    if (!text && attachedFile.length === 0) return;
    if (attachedFile.length) {
      onSendWithFile && onSendWithFile(text, attachedFile);
      setAttachedFile([]);
    } else {
      onSend(text);
    }
    setVal("");
  };

  // ── Upload ──────────────────────────────────────────────────
  const handleUploadClick = () => fileInputRef.current?.click();

  const handleFileChange = (e) => {
    const files = Array.from(e.target.files || []);
    if (!files.length) return;
    for (const file of files) {
      const isText = file.type.startsWith("text/") || file.type === "application/json";
      const isPDF = file.type === "application/pdf";
      const isImage = file.type.startsWith("image/");
      if (!isText && !isPDF && !isImage) {
        alert("Unsupported file type: " + file.name);
        continue;
      }
      if (file.size > 10 * 1024 * 1024) {
        alert("File too large (max 10MB): " + file.name);
        continue;
      }
      const reader = new FileReader();
      if (isImage) {
        reader.onload = (ev) => setAttachedFile((p) => [...p, { name: file.name, type: "image", dataUrl: ev.target.result, size: file.size }]);
        reader.readAsDataURL(file);
      } else if (isText) {
        reader.onload = (ev) => setAttachedFile((p) => [...p, { name: file.name, type: "text", content: ev.target.result, size: file.size }]);
        reader.readAsText(file);
      } else {
        // PDF — read as base64 data URL so the server can extract its tables.
        reader.onload = (ev) => setAttachedFile((p) => [...p, { name: file.name, type: "pdf", dataUrl: ev.target.result, size: file.size }]);
        reader.readAsDataURL(file);
      }
    }
    e.target.value = "";
  };

  // ── Voice ────────────────────────────────────────────────────
  const toggleVoice = () => {
    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SpeechRecognition) {
      alert("Voice input is not supported in this browser. Please use Chrome or Edge.");
      return;
    }

    if (listening) {
      recognitionRef.current?.stop();
      setListening(false);
      return;
    }

    const rec = new SpeechRecognition();
    rec.lang = lang === "Español" ? "es-ES" : lang === "Français" ? "fr-FR" :
               lang === "Deutsch" ? "de-DE" : lang === "日本語" ? "ja-JP" :
               lang === "한국어" ? "ko-KR" : "en-US";
    rec.continuous = false;
    rec.interimResults = false;

    rec.onresult = (e) => {
      const transcript = e.results[0][0].transcript;
      setVal((v) => v ? v + " " + transcript : transcript);
    };
    rec.onend = () => setListening(false);
    rec.onerror = () => setListening(false);

    recognitionRef.current = rec;
    rec.start();
    setListening(true);
  };

  return (
    <div
      className="fixed bottom-6 left-0 z-40 flex justify-center px-4 pointer-events-none transition-[right] duration-300"
      style={{ right: vaultOpen ? 380 : 0 }}
    >
      <div className="pointer-events-auto w-full max-w-[760px]">
        <div className="focus-ring rounded-3xl bg-paper/95 dark:bg-graphite/95 backdrop-blur-xl border border-line dark:border-hairline shadow-[0_18px_50px_-22px_rgba(0,0,0,0.18)] dark:shadow-[0_24px_60px_-20px_rgba(0,0,0,0.6)]">

          {/* Attached file previews */}
          {attachedFile.length > 0 && (
            <div className="px-4 pt-3 pb-0 flex items-center gap-2 flex-wrap">
              {attachedFile.map((af, ai) => (
                <div key={ai} className="flex items-center gap-2 px-2.5 py-1.5 rounded-lg bg-fog dark:bg-graphite2 border border-line dark:border-hairline text-[12px] max-w-full">
                  <Icon name={af.type === "image" ? "Image" : af.type === "pdf" ? "FileText" : "FileCode2"} size={13} className="text-gold dark:text-goldsoft shrink-0" />
                  <span className="truncate max-w-[200px] text-mute dark:text-dim">{af.name}</span>
                  <span className="text-mute/60 dark:text-dim/60 shrink-0">{(af.size / 1024).toFixed(0)}KB</span>
                  <button onClick={() => setAttachedFile((p) => p.filter((_, i) => i !== ai))} className="text-mute dark:text-dim hover:text-ink dark:hover:text-paper ml-1">
                    <Icon name="X" size={12} />
                  </button>
                </div>
              ))}
            </div>
          )}

          <div className="px-4 pt-3 pb-2">
            <textarea
              ref={textRef}
              value={val}
              onChange={(e) => setVal(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submit(); }
              }}
              rows={1}
              placeholder={listening ? "Listening… speak now" : attachedFile.length ? "Ask about these file(s)…" : "Ask anything…"}
              className={"caret w-full resize-none bg-transparent outline-none text-[15px] leading-relaxed placeholder:text-mute/80 dark:placeholder:text-dim/80 " + (listening ? "placeholder:text-gold/80 dark:placeholder:text-goldsoft/80" : "")}
            />
          </div>

          <div className="flex items-center justify-between px-2 pb-2">
            <div className="flex items-center gap-0.5">
              {/* Upload */}
              <input ref={fileInputRef} type="file" multiple accept=".pdf,.txt,.md,.csv,.json,.js,.ts,.py,.html,.css,.png,.jpg,.jpeg,.gif,.webp" className="hidden" onChange={handleFileChange} />
              <ActionPill icon="Plus" label="Upload" onClick={handleUploadClick} active={attachedFile.length > 0} />
              <ActionPill icon="AtSign" label="Mention" onClick={() => { textRef.current?.focus(); setVal(v => v.endsWith(" ") || v === "" ? v + "@" : v + " @"); }} />

              {/* Language */}
              <div className="relative">
                <button
                  onClick={() => setLangOpen(!langOpen)}
                  className="h-8 px-2.5 rounded-full inline-flex items-center gap-1.5 text-[12px] text-mute dark:text-dim hover:bg-fog dark:hover:bg-graphite2 hover:text-ink dark:hover:text-paper transition-colors"
                >
                  <Icon name="Globe" size={14} />
                  <span>{lang}</span>
                  <Icon name="ChevronDown" size={12} />
                </button>
                {langOpen && (
                  <div className="absolute bottom-full mb-2 left-0 w-44 rounded-xl border border-line dark:border-hairline bg-paper dark:bg-graphite shadow-lg p-1 z-50">
                    {["English", "Español", "Français", "Deutsch", "日本語", "한국어"].map((l) => (
                      <button key={l} onClick={() => { setLang(l); setLangOpen(false); }}
                        className={"w-full text-left px-2.5 py-1.5 rounded-lg text-[12.5px] hover:bg-fog dark:hover:bg-graphite2 " + (l === lang ? "font-medium" : "text-mute dark:text-dim")}>
                        {l}
                      </button>
                    ))}
                  </div>
                )}
              </div>

              {/* Voice */}
              <ActionPill icon="Mic" label={listening ? "Stop" : "Voice"} onClick={toggleVoice} active={listening} />
            </div>

            <div className="flex items-center gap-1.5">
              <span className="text-[11px] text-mute/70 dark:text-dim/70 mr-1 hidden sm:inline">⌘ ↵ to send</span>
              <button
                onClick={submit}
                disabled={thinking}
                className={"w-9 h-9 rounded-full grid place-items-center transition-all " + ((val.trim() || attachedFile.length) && !thinking
                  ? "bg-ink text-paper dark:bg-paper dark:text-ink hover:scale-[1.04]"
                  : "bg-fog dark:bg-graphite2 text-mute dark:text-dim")}
                aria-label="Send"
              >
                {thinking
                  ? <Icon name="Loader2" size={16} stroke={2} className="animate-spin" />
                  : <Icon name="ArrowUp" size={16} stroke={2} />}
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function ActionPill({ icon, label, onClick, active }) {
  return (
    <button
      onClick={onClick}
      className={"h-8 px-2.5 rounded-full inline-flex items-center gap-1.5 text-[12px] transition-colors " +
        (active
          ? "bg-gold/15 text-goldink dark:text-goldsoft border border-gold/30"
          : "text-mute dark:text-dim hover:bg-fog dark:hover:bg-graphite2 hover:text-ink dark:hover:text-paper")}
    >
      <Icon name={icon} size={14} />
      <span className="hidden sm:inline">{label}</span>
    </button>
  );
}

/* ---------- Sovereign Vault (right slide-out) ---------- */
const MEMORY_FOLDERS = [];

function SovereignVault({ open, setOpen, items, hover, setHover, storageMode, setStorageMode, sessionList, activeSessionId, onSwitchSession, onDeleteSession, onNewSession }) {
  const shown = open || hover;
  const [folders, setFolders] = useState(() => {
    try { return JSON.parse(localStorage.getItem("ethoryx_vault_folders") || "[]"); }
    catch { return []; }
  });
  const [indexing, setIndexing] = useState({});
  const folderPickerRef = useRef(null);

  useEffect(() => {
    localStorage.setItem("ethoryx_vault_folders", JSON.stringify(folders));
  }, [folders]);

  const handleFolderPick = (e) => {
    const files = Array.from(e.target.files || []);
    if (!files.length) return;
    const folderName = files[0].webkitRelativePath.split("/")[0] || files[0].name;
    const totalSize = files.reduce((acc, f) => acc + f.size, 0);
    const sizeLabel = totalSize > 1048576 ? `${(totalSize / 1048576).toFixed(1)} MB` : `${(totalSize / 1024).toFixed(0)} KB`;
    setFolders(f => [...f, {
      id: Date.now().toString(),
      name: folderName,
      path: folderName,
      meta: `${files.length} files · ${sizeLabel}`,
      state: "unindexed",
    }]);
    e.target.value = "";
  };

  const deleteFolder = (id) => setFolders(f => f.filter(x => x.id !== id));
  const [tab, setTab] = useState("history"); // 'history' | 'memory' | 'artifacts'
  const [filter, setFilter] = useState("All");
  const [toast, setToast] = useState(null);
  const local = storageMode === "local";

  // ── Enterprise brand training ──
  const [enterpriseId, setEnterpriseId] = useState(() => localStorage.getItem("ethoryx_enterprise_id") || null);
  const [enterpriseInfo, setEnterpriseInfo] = useState(null);
  const [enterpriseAnalysis, setEnterpriseAnalysis] = useState(null);
  const [brandLearning, setBrandLearning] = useState(false);
  const [learningResult, setLearningResult] = useState(null);
  const [showBrandModal, setShowBrandModal] = useState(false);
  const [brandText, setBrandText] = useState("");
  const [showSetupModal, setShowSetupModal] = useState(false);
  const [setupName, setSetupName] = useState("");
  const [setupIndustry, setSetupIndustry] = useState("");

  const loadEnterprise = (eid) => {
    if (!eid) return;
    fetch(`${CHAT_API}/v1/tai/enterprise/${eid}`)
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (d && d.exists) setEnterpriseInfo(d); })
      .catch(() => {});
    fetch(`${CHAT_API}/v1/tai/enterprise/${eid}/analysis`)
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (d && d.exists) setEnterpriseAnalysis(d); })
      .catch(() => {});
  };

  useEffect(() => {
    if (enterpriseId) loadEnterprise(enterpriseId);
  }, [enterpriseId]);

  const createEnterprise = async () => {
    if (!setupName.trim()) return;
    try {
      const res = await fetch(`${CHAT_API}/v1/tai/enterprise`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ company_name: setupName.trim(), industry: setupIndustry.trim() }),
      });
      if (!res.ok) return;
      const data = await res.json();
      const eid = data.enterprise_id;
      localStorage.setItem("ethoryx_enterprise_id", eid);
      setEnterpriseId(eid);
      setShowSetupModal(false);
      setSetupName(""); setSetupIndustry("");
      flash(`Enterprise "${data.company_name}" created`);
      loadEnterprise(eid);
    } catch { flash("Setup failed"); }
  };

  const learnFromText = async () => {
    if (!enterpriseId || !brandText.trim()) return;
    setBrandLearning(true);
    setLearningResult(null);
    try {
      const res = await fetch(`${CHAT_API}/v1/tai/enterprise/${enterpriseId}/learn`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ text: brandText.trim(), source: "manual-paste" }),
      });
      const data = await res.json();
      setLearningResult(data);
      setBrandText("");
      flash(`Learned: ${(data.extractions || []).length} signals extracted`);
      loadEnterprise(enterpriseId);
    } catch { flash("Learning failed"); }
    finally { setBrandLearning(false); }
  };

  const learnFromFiles = async (files) => {
    if (!enterpriseId || !files.length) return;
    setBrandLearning(true);
    setLearningResult(null);
    try {
      // Accept all brand-relevant file types — backend extracts content
      const allowedFiles = Array.from(files).filter(f => {
        const name = f.name.toLowerCase();
        return /\.(txt|md|html|css|json|csv|svg|pdf|pptx|docx|xlsx|png|jpg|jpeg|gif|webp)$/i.test(name);
      });
      if (!allowedFiles.length) {
        flash("Supported: PDF, PPTX, DOCX, images, text files");
        setBrandLearning(false);
        return;
      }
      // Cap at 20 files per upload to avoid huge payloads
      const filesToUpload = allowedFiles.slice(0, 20);
      // Build multipart form data
      const formData = new FormData();
      for (const f of filesToUpload) {
        formData.append('files', f);
      }
      const res = await fetch(`${CHAT_API}/v1/tai/enterprise/${enterpriseId}/learn-files`, {
        method: "POST",
        body: formData,
      });
      if (!res.ok) {
        const err = await res.json().catch(() => ({}));
        flash(err.error || `Upload failed (${res.status})`);
        setBrandLearning(false);
        return;
      }
      const data = await res.json();
      setLearningResult(data);
      const sigs = (data.extractions || []).length;
      flash(`Analyzed ${filesToUpload.length} file${filesToUpload.length === 1 ? '' : 's'} — ${sigs} brand signal${sigs === 1 ? '' : 's'} extracted`);
      loadEnterprise(enterpriseId);
    } catch (e) { flash("Upload failed — check connection"); }
    finally { setBrandLearning(false); }
  };

  const disconnectEnterprise = () => {
    localStorage.removeItem("ethoryx_enterprise_id");
    setEnterpriseId(null);
    setEnterpriseInfo(null);
    setEnterpriseAnalysis(null);
    setLearningResult(null);
    flash("Disconnected from enterprise");
  };

  const brandPickerRef = useRef(null);
  const brandFolderRef = useRef(null);
  const flash = (msg) => { setToast(msg); setTimeout(() => setToast(null), 1800); };
  const filtered = filter === "All" ? items : items.filter((it) => {
    const m = { Code: "code", Data: "data", Reports: "report", Images: "image" };
    return it.type === m[filter];
  });

  const reindex = (id) => {
    if (indexing[id]) return;
    setIndexing((s) => ({ ...s, [id]: true }));
    setTimeout(() => {
      setIndexing((s) => { const n = { ...s }; delete n[id]; return n; });
      setFolders((fs) => fs.map((f) => f.id === id ? { ...f, state: "indexed" } : f));
    }, 1400);
  };

  return (
    <>
      {/* Hover trigger strip on right edge */}
      <div
        className="fixed right-0 top-14 bottom-0 w-3 z-30 edge-hint"
        onMouseEnter={() => setHover(true)}
        onMouseLeave={() => setHover(false)}
      />

      {/* Panel */}
      <aside
        onMouseEnter={() => setHover(true)}
        onMouseLeave={() => setHover(false)}
        className={
          "panel-slide fixed top-14 bottom-0 right-0 w-[380px] z-30 " +
          "border-l border-line dark:border-hairline bg-paper dark:bg-ink " +
          (shown ? "translate-x-0" : "translate-x-full")
        }
      >
        <div className="h-full flex flex-col">
          {/* Header */}
          <div className="px-5 pt-5 pb-3 flex items-start justify-between gap-3">
            <div className="min-w-0">
              <div className="flex items-center gap-1.5">
                <span className="text-gold dark:text-goldsoft"><LockGlyph size={13} strokeWidth={1.8} /></span>
                <div className="text-[14px] font-medium tracking-tight">Sovereign Vault</div>
              </div>
              <div className="text-[11.5px] text-mute dark:text-dim mt-0.5">{items.length} artifacts · {folders.length} mapped folders</div>
            </div>
            <button
              onClick={() => setOpen(false)}
              className="w-7 h-7 rounded-full hover:bg-fog dark:hover:bg-graphite2 grid place-items-center text-mute dark:text-dim"
              aria-label="Close"
            >
              <Icon name="X" size={14} />
            </button>
          </div>

          {/* Storage mode segmented */}
          <div className="px-5 pb-2.5">
            <div className="relative grid grid-cols-2 h-8 p-0.5 rounded-full bg-fog dark:bg-graphite2 text-[11.5px]">
              <span
                className={"absolute top-0.5 bottom-0.5 w-[calc(50%-2px)] rounded-full transition-transform duration-300 ease-out " + (local ? "bg-paper dark:bg-graphite border border-gold/40 shadow-sm" : "translate-x-full bg-paper dark:bg-graphite border border-line dark:border-hairline shadow-sm")}
              />
              <button
                onClick={() => setStorageMode("local")}
                className={"relative inline-flex items-center justify-center gap-1.5 rounded-full transition-colors " + (local ? "text-goldink dark:text-goldsoft" : "text-mute dark:text-dim")}
              >
                <LockGlyph size={11} />
                <span className="tracking-tight font-medium">Local Storage Only</span>
              </button>
              <button
                onClick={() => setStorageMode("cloud")}
                className={"relative inline-flex items-center justify-center gap-1.5 rounded-full transition-colors " + (!local ? "text-ink dark:text-paper" : "text-mute dark:text-dim")}
              >
                <Icon name="Cloud" size={11} />
                <span className="tracking-tight font-medium">Cloud Sync</span>
              </button>
            </div>

            {/* Encryption status indicator */}
            <div className={"mt-2 flex items-center gap-2 px-2.5 py-1.5 rounded-lg border text-[11.5px] " + (local
              ? "border-gold/35 bg-goldtint/50 dark:bg-gold/10 text-goldink dark:text-goldsoft"
              : "border-line dark:border-hairline text-mute dark:text-dim")}>
              <span className="relative flex w-2 h-2 shrink-0">
                <span className={"absolute inset-0 rounded-full opacity-70 " + (local ? "bg-gold dark:bg-goldsoft animate-ping" : "bg-mute dark:bg-dim")} />
                <span className={"relative w-2 h-2 rounded-full " + (local ? "bg-gold dark:bg-goldsoft" : "bg-mute dark:bg-dim")} />
              </span>
              <span className="tracking-tight">
                {local ? "Data encrypted on your device" : "Syncing to ethoryx.cloud \u00b7 end-to-end encrypted"}
              </span>
              <span className="ml-auto font-mono text-[10px] opacity-70">AES-256</span>
            </div>
          </div>

          {/* Parallel tab switcher */}
          <div className="px-5 pt-1 pb-2">
            <div className="relative grid grid-cols-3 h-9 rounded-lg bg-fog/70 dark:bg-graphite2 p-0.5 text-[11.5px]">
              <span
                className={"absolute top-0.5 bottom-0.5 w-[calc(33.33%-2px)] rounded-md bg-paper dark:bg-graphite border border-line dark:border-hairline shadow-sm transition-transform duration-300 ease-out " + (tab === "memory" ? "translate-x-full" : tab === "artifacts" ? "translate-x-[200%]" : "")}
              />
              <button
                onClick={() => setTab("history")}
                className={"relative inline-flex items-center justify-center gap-1 rounded-md transition-colors " + (tab === "history" ? "text-ink dark:text-paper" : "text-mute dark:text-dim")}
              >
                <Icon name="MessageSquare" size={11} />
                <span className="tracking-tight font-medium">History</span>
                <span className={"text-[10px] font-mono px-1 rounded " + (tab === "history" ? "bg-fog dark:bg-graphite2 text-mute dark:text-dim" : "text-mute/70 dark:text-dim/70")}>{(sessionList || []).length}</span>
              </button>
              <button
                onClick={() => setTab("memory")}
                className={"relative inline-flex items-center justify-center gap-1 rounded-md transition-colors " + (tab === "memory" ? "text-ink dark:text-paper" : "text-mute dark:text-dim")}
              >
                <Icon name="Network" size={11} />
                <span className="tracking-tight font-medium">Memory</span>
                <span className={"text-[10px] font-mono px-1 rounded " + (tab === "memory" ? "bg-fog dark:bg-graphite2 text-mute dark:text-dim" : "text-mute/70 dark:text-dim/70")}>{folders.length}</span>
              </button>
              <button
                onClick={() => setTab("artifacts")}
                className={"relative inline-flex items-center justify-center gap-1 rounded-md transition-colors " + (tab === "artifacts" ? "text-ink dark:text-paper" : "text-mute dark:text-dim")}
              >
                <Icon name="Layers" size={11} />
                <span className="tracking-tight font-medium">Artifacts</span>
                <span className={"text-[10px] font-mono px-1 rounded " + (tab === "artifacts" ? "bg-fog dark:bg-graphite2 text-mute dark:text-dim" : "text-mute/70 dark:text-dim/70")}>{items.length}</span>
              </button>
            </div>
          </div>

          {/* Search */}
          <div className="px-5 pt-1 pb-2">
            <div className="relative">
              <Icon name="Search" size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-mute dark:text-dim" />
              <input
                type="text"
                placeholder={tab === "history" ? "Search chats…" : tab === "memory" ? "Search folders…" : "Search artifacts, code, reports…"}
                className="w-full h-8 pl-7 pr-2.5 rounded-lg bg-fog/70 dark:bg-graphite2 text-[12.5px] outline-none placeholder:text-mute/80 dark:placeholder:text-dim/80 border border-transparent focus:border-line dark:focus:border-hairline"
              />
            </div>
          </div>

          {/* Scrollable body */}
          <div className="flex-1 overflow-y-auto scroll-hide px-5 pb-6">
            {tab === "history" ? (
              <div className="mt-3 rise">
                <div className="flex items-center justify-between mb-2">
                  <div className="flex items-center gap-1.5">
                    <span className="text-gold dark:text-goldsoft"><Icon name="MessageSquare" size={12} /></span>
                    <div className="text-[11.5px] tracking-tight font-medium">Chat history</div>
                  </div>
                  <button onClick={() => onNewSession && onNewSession()}
                    className="text-[10.5px] uppercase tracking-wider text-mute dark:text-dim hover:text-ink dark:hover:text-paper inline-flex items-center gap-1">
                    <Icon name="Plus" size={10} /> New chat
                  </button>
                </div>
                {(sessionList || []).length === 0 ? (
                  <div className="mt-4 text-center text-[12px] text-mute dark:text-dim border border-dashed border-line dark:border-hairline rounded-lg py-8">
                    No chat history yet. Start a conversation.
                  </div>
                ) : (
                  <div className="space-y-1">
                    {(sessionList || []).map((s) => (
                      <div
                        key={s.id}
                        className={"group flex items-center gap-2.5 px-2.5 py-2 rounded-lg cursor-pointer transition-colors " + (s.id === activeSessionId
                          ? "bg-fog dark:bg-graphite2 border border-gold/30"
                          : "border border-transparent hover:bg-fog/70 dark:hover:bg-graphite2/70 hover:border-line dark:hover:border-hairline")}
                        onClick={() => onSwitchSession && onSwitchSession(s.id)}
                      >
                        <div className="w-7 h-7 rounded-md bg-fog dark:bg-graphite2 grid place-items-center text-mute dark:text-dim shrink-0">
                          <Icon name="MessageSquare" size={12} />
                        </div>
                        <div className="min-w-0 flex-1">
                          <div className="text-[12.5px] font-medium tracking-tight truncate">{s.title || "Untitled"}</div>
                          <div className="text-[10.5px] text-mute dark:text-dim mt-0.5 truncate">
                            {s.message_count || 0} msgs{s.preview ? ` · ${s.preview.slice(0, 40)}` : ""}
                          </div>
                        </div>
                        <button
                          onClick={(e) => { e.stopPropagation(); onDeleteSession && onDeleteSession(s.id); }}
                          className="w-6 h-6 rounded-md grid place-items-center opacity-0 group-hover:opacity-100 text-mute dark:text-dim hover:text-red-500 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-all shrink-0"
                          title="Delete chat"
                        >
                          <Icon name="Trash2" size={11} />
                        </button>
                      </div>
                    ))}
                  </div>
                )}
              </div>
            ) : tab === "memory" ? (
              <div className="mt-3 rise space-y-5">
                {/* ── Enterprise Brand Training ── */}
                {enterpriseId && enterpriseInfo ? (
                  <div className="rounded-xl border border-gold/30 dark:border-gold/25 bg-goldtint/30 dark:bg-gold/5 p-3.5">
                    <div className="flex items-center justify-between mb-2">
                      <div className="flex items-center gap-2 min-w-0">
                        <div className="w-7 h-7 rounded-lg bg-goldtint dark:bg-gold/20 grid place-items-center text-goldink dark:text-goldsoft shrink-0">
                          <ShieldGlyph size={12} strokeWidth={1.8} />
                        </div>
                        <div className="min-w-0">
                          <div className="text-[12.5px] font-semibold tracking-tight truncate">{enterpriseInfo.company_name}</div>
                          <div className="text-[10.5px] text-mute dark:text-dim truncate">
                            {enterpriseInfo.industry || 'Enterprise brand'} · {Math.round((enterpriseInfo.confidence || 0) * 100)}% learned
                          </div>
                        </div>
                      </div>
                      <button onClick={disconnectEnterprise} title="Disconnect"
                        className="w-6 h-6 rounded-md grid place-items-center text-mute dark:text-dim hover:text-red-500 hover:bg-fog dark:hover:bg-graphite2 transition-colors shrink-0">
                        <Icon name="LogOut" size={11} />
                      </button>
                    </div>
                    {/* Knowledge analysis */}
                    {enterpriseAnalysis ? (
                      <>
                        {/* Quick stats row */}
                        <div className="grid grid-cols-3 gap-1.5 mb-2.5">
                          <div className="rounded-md bg-paper/60 dark:bg-graphite2/60 px-2 py-1.5 text-center">
                            <div className="text-[13px] font-semibold tabular-nums text-ink dark:text-paper">{enterpriseAnalysis.stats.sources_analyzed}</div>
                            <div className="text-[9px] text-mute dark:text-dim uppercase tracking-wide">Sources</div>
                          </div>
                          <div className="rounded-md bg-paper/60 dark:bg-graphite2/60 px-2 py-1.5 text-center">
                            <div className="text-[13px] font-semibold tabular-nums text-ink dark:text-paper">{enterpriseAnalysis.stats.domain_terms}</div>
                            <div className="text-[9px] text-mute dark:text-dim uppercase tracking-wide">Terms</div>
                          </div>
                          <div className="rounded-md bg-paper/60 dark:bg-graphite2/60 px-2 py-1.5 text-center">
                            <div className="text-[13px] font-semibold tabular-nums text-ink dark:text-paper">{enterpriseAnalysis.stats.complete}<span className="text-[9px] text-mute dark:text-dim">/{enterpriseAnalysis.stats.total}</span></div>
                            <div className="text-[9px] text-mute dark:text-dim uppercase tracking-wide">Complete</div>
                          </div>
                        </div>
                        {/* Assessment sentence */}
                        <div className="text-[10.5px] leading-[1.6] text-mute dark:text-dim border-l-2 border-gold/50 pl-2 py-0.5 mb-2.5">
                          {enterpriseAnalysis.assessment}
                        </div>
                        {/* Category rows */}
                        <div className="space-y-[5px] mb-2.5">
                          {enterpriseAnalysis.categories.map(cat => (
                            <div key={cat.key} className="flex items-start gap-1.5">
                              <span className={`mt-[3px] w-2 h-2 rounded-full shrink-0 ${
                                cat.status === 'complete' ? 'bg-gold dark:bg-goldsoft' :
                                cat.status === 'partial'  ? 'bg-amber-400' :
                                cat.status === 'missing'  ? 'bg-red-400/60' :
                                                            'bg-line dark:bg-hairline'
                              }`} />
                              <div className="min-w-0 flex-1 flex items-baseline gap-1 flex-wrap">
                                <span className="text-[10px] font-medium text-ink dark:text-paper whitespace-nowrap">{cat.label}</span>
                                <span className="text-[9.5px] text-mute dark:text-dim leading-snug">{cat.detail}</span>
                              </div>
                            </div>
                          ))}
                        </div>
                        {/* Confidence bar with % */}
                        <div className="flex items-center gap-2 mb-2.5">
                          <div className="flex-1 h-1 rounded-full bg-paper/60 dark:bg-graphite overflow-hidden">
                            <div className="h-full bg-gold dark:bg-goldsoft transition-all duration-500"
                              style={{ width: `${Math.round((enterpriseInfo.confidence || 0) * 100)}%` }} />
                          </div>
                          <span className="text-[9.5px] tabular-nums text-mute dark:text-dim shrink-0">
                            {Math.round((enterpriseInfo.confidence || 0) * 100)}% learned
                          </span>
                        </div>
                        {/* Gaps / recommendations */}
                        {enterpriseAnalysis.gaps && enterpriseAnalysis.gaps.length > 0 && (
                          <div className="rounded-lg bg-paper/50 dark:bg-graphite2/50 px-2.5 py-2 mb-2.5">
                            <div className="text-[9px] uppercase tracking-wider text-mute dark:text-dim mb-1.5 font-medium">Recommended next</div>
                            <div className="space-y-0.5">
                              {enterpriseAnalysis.gaps.map((g, i) => (
                                <div key={i} className="text-[10px] text-ink dark:text-paper flex items-center gap-1.5">
                                  <span className="text-gold dark:text-goldsoft text-[11px] leading-none">›</span>
                                  <span>{g}</span>
                                </div>
                              ))}
                            </div>
                          </div>
                        )}
                      </>
                    ) : (
                      /* fallback while analysis loads */
                      <>
                        <div className="grid grid-cols-3 gap-1.5 mb-2.5">
                          <div className="rounded-md bg-paper/60 dark:bg-graphite2/60 px-2 py-1.5">
                            <div className="text-[14px] font-semibold tabular-nums text-ink dark:text-paper">{enterpriseInfo.sources_analyzed || 0}</div>
                            <div className="text-[9.5px] text-mute dark:text-dim uppercase tracking-wide">Sources</div>
                          </div>
                          <div className="rounded-md bg-paper/60 dark:bg-graphite2/60 px-2 py-1.5">
                            <div className="text-[14px] font-semibold tabular-nums text-ink dark:text-paper">{enterpriseInfo.domain_terms || 0}</div>
                            <div className="text-[9.5px] text-mute dark:text-dim uppercase tracking-wide">Terms</div>
                          </div>
                          <div className="rounded-md bg-paper/60 dark:bg-graphite2/60 px-2 py-1.5">
                            <div className="text-[14px] font-semibold tabular-nums text-ink dark:text-paper">{enterpriseInfo.has_custom_colors ? '✓' : '—'}</div>
                            <div className="text-[9.5px] text-mute dark:text-dim uppercase tracking-wide">Colors</div>
                          </div>
                        </div>
                        <div className="h-1 rounded-full bg-paper/60 dark:bg-graphite overflow-hidden mb-2.5">
                          <div className="h-full bg-gold dark:bg-goldsoft transition-all duration-500"
                            style={{ width: `${Math.round((enterpriseInfo.confidence || 0) * 100)}%` }} />
                        </div>
                      </>
                    )}
                    {/* Train actions */}
                    <div className="grid grid-cols-3 gap-1.5">
                      <button onClick={() => setShowBrandModal(true)} disabled={brandLearning}
                        className="text-[10.5px] font-medium tracking-tight px-2 py-1.5 rounded-lg border border-line dark:border-hairline bg-paper dark:bg-graphite hover:border-gold/40 transition-colors flex items-center justify-center gap-1 disabled:opacity-50">
                        <Icon name="Type" size={10} /> Paste
                      </button>
                      <button onClick={() => brandPickerRef.current?.click()} disabled={brandLearning}
                        className="text-[10.5px] font-medium tracking-tight px-2 py-1.5 rounded-lg border border-line dark:border-hairline bg-paper dark:bg-graphite hover:border-gold/40 transition-colors flex items-center justify-center gap-1 disabled:opacity-50">
                        <Icon name="File" size={10} /> Files
                      </button>
                      <button onClick={() => brandFolderRef.current?.click()} disabled={brandLearning}
                        className="text-[10.5px] font-medium tracking-tight px-2 py-1.5 rounded-lg border border-line dark:border-hairline bg-paper dark:bg-graphite hover:border-gold/40 transition-colors flex items-center justify-center gap-1 disabled:opacity-50">
                        <Icon name="FolderOpen" size={10} /> Folder
                      </button>
                      <input
                        ref={brandPickerRef}
                        type="file"
                        multiple
                        accept=".txt,.md,.html,.css,.json,.csv,.svg,.pdf,.pptx,.docx,.xlsx,.png,.jpg,.jpeg,.gif,.webp"
                        className="hidden"
                        onChange={(e) => { learnFromFiles(e.target.files); e.target.value = ''; }}
                      />
                      <input
                        ref={brandFolderRef}
                        type="file"
                        multiple
                        {...{"webkitdirectory": "", "directory": ""}}
                        className="hidden"
                        onChange={(e) => { learnFromFiles(e.target.files); e.target.value = ''; }}
                      />
                    </div>
                    {brandLearning && (
                      <div className="mt-2 text-[10.5px] text-mute dark:text-dim flex items-center gap-1.5">
                        <span className="inline-block w-3 h-3 rounded-full border-2 border-gold/30 border-t-gold animate-spin" />
                        Learning brand patterns…
                      </div>
                    )}
                    {learningResult && learningResult.extractions && (
                      <div className="mt-2.5 pt-2.5 border-t border-gold/20 dark:border-gold/15">
                        <div className="text-[10.5px] uppercase tracking-wider text-mute dark:text-dim mb-1.5">Just learned</div>
                        <div className="flex flex-wrap gap-1">
                          {learningResult.extractions.map((ex, i) => (
                            <span key={i} className="text-[10.5px] px-1.5 py-0.5 rounded bg-goldtint/70 dark:bg-gold/15 text-goldink dark:text-goldsoft">{ex}</span>
                          ))}
                        </div>
                      </div>
                    )}
                  </div>
                ) : (
                  <div className="rounded-xl border border-dashed border-line dark:border-hairline p-3.5">
                    <div className="flex items-center gap-2 mb-1.5">
                      <span className="text-goldink dark:text-goldsoft"><ShieldGlyph size={12} strokeWidth={1.8} /></span>
                      <div className="text-[11.5px] tracking-tight font-medium">Enterprise brand</div>
                    </div>
                    <p className="text-[11px] leading-[1.55] text-mute dark:text-dim mb-2.5">
                      Teach Zera your company's voice, colors, and templates. Every email, presentation, and document will then carry your brand.
                    </p>
                    <button onClick={() => setShowSetupModal(true)}
                      className="w-full text-[11.5px] font-medium tracking-tight px-3 py-2 rounded-lg bg-ink dark:bg-paper text-paper dark:text-ink hover:opacity-90 transition-opacity inline-flex items-center justify-center gap-1.5">
                      <Icon name="Plus" size={12} /> Set up enterprise
                    </button>
                  </div>
                )}

                {/* ── Local Folders (existing) ── */}
                <div>
                <div className="flex items-center justify-between mb-1.5">
                  <div className="flex items-center gap-1.5">
                    <span className="text-gold dark:text-goldsoft"><Icon name="Network" size={12} /></span>
                    <div className="text-[11.5px] tracking-tight font-medium">Local folders</div>
                  </div>
                  <button onClick={() => folderPickerRef.current?.click()}
                    className="text-[10.5px] uppercase tracking-wider text-mute dark:text-dim hover:text-ink dark:hover:text-paper inline-flex items-center gap-1">
                    <Icon name="Plus" size={10} /> Add folder
                  </button>
                  <input
                    ref={folderPickerRef}
                    type="file"
                    {...{"webkitdirectory": "", "directory": ""}}
                    multiple
                    className="hidden"
                    onChange={handleFolderPick}
                  />
                </div>
                <p className="text-[11px] leading-[1.5] text-mute dark:text-dim mb-2">
                  Folders Ethoryx is allowed to consult. Indexing builds a mathematical map of the data — contents never leave your device.
                </p>
                <div className="space-y-1.5">
                  {folders.map((f) => (
                    <MemoryRow key={f.id} folder={f} busy={!!indexing[f.id]} onIndex={() => reindex(f.id)} onDelete={() => deleteFolder(f.id)} />
                  ))}
                </div>
                </div>
              </div>
            ) : (
              <div className="mt-3 rise">
                {/* Filter chips */}
                <div className="flex items-center gap-1 overflow-x-auto scroll-hide pb-1.5">
                  {["All", "Code", "Data", "Reports", "Images"].map((c) => (
                    <button
                      key={c}
                      onClick={() => setFilter(c)}
                      className={"h-6 px-2.5 rounded-full text-[11.5px] tracking-tight whitespace-nowrap transition-colors " + (c === filter
                        ? "bg-ink text-paper dark:bg-paper dark:text-ink"
                        : "text-mute dark:text-dim hover:bg-fog dark:hover:bg-graphite2")}
                    >
                      {c}
                    </button>
                  ))}
                </div>

                {filtered.length === 0 ? (
                  <div className="mt-6 text-center text-[12px] text-mute dark:text-dim border border-dashed border-line dark:border-hairline rounded-lg py-8">
                    No {filter.toLowerCase()} artifacts in this thread yet.
                  </div>
                ) : (
                  groupedItems(filtered).map(([group, list]) => (
                    <div key={group} className="mt-3">
                      <div className="text-[10.5px] uppercase tracking-wider text-mute/80 dark:text-dim/80 mb-1.5">{group}</div>
                      <div className="space-y-0.5 -mx-2">
                        {list.map((it) => <ResourceRow key={it.id} item={it} flash={flash} />)}
                      </div>
                    </div>
                  ))
                )}
              </div>
            )}
          </div>

          {/* Toast */}
          {toast && (
            <div className="absolute bottom-16 left-1/2 -translate-x-1/2 rise px-3 h-8 rounded-full bg-ink text-paper dark:bg-paper dark:text-ink text-[11.5px] tracking-tight inline-flex items-center gap-1.5 shadow-lg z-10">
              <Icon name="CheckCircle2" size={12} />
              <span>{toast}</span>
            </div>
          )}

          {/* Footer */}
          <div className="border-t border-line dark:border-hairline px-5 py-3 flex items-center justify-between">
            <div className="text-[11.5px] text-mute dark:text-dim inline-flex items-center gap-1.5">
              <span className="text-gold dark:text-goldsoft"><LockGlyph size={11} /></span>
              <span>{local ? "Stored locally \u00b7 your device" : "Synced \u00b7 your account"}</span>
            </div>
            <button onClick={() => window.open("https://ethoryx.io/dashboard", "_blank")} className="text-[11.5px] tracking-tight inline-flex items-center gap-1 hover:text-ink dark:hover:text-paper text-mute dark:text-dim">
              <Icon name="ExternalLink" size={12} />
              <span>Open vault</span>
            </button>
          </div>
        </div>

        {/* ── Enterprise Setup Modal ── */}
        {showSetupModal && (
          <div className="absolute inset-0 z-50 bg-ink/40 dark:bg-black/60 backdrop-blur-sm grid place-items-center p-4">
            <div className="w-full max-w-sm rounded-2xl bg-paper dark:bg-graphite border border-line dark:border-hairline p-5 shadow-xl">
              <div className="flex items-center gap-2 mb-3">
                <ShieldGlyph size={14} strokeWidth={1.8} />
                <h3 className="text-[15px] font-semibold tracking-tight">Set up enterprise</h3>
              </div>
              <p className="text-[12px] text-mute dark:text-dim leading-relaxed mb-4">
                Zera will keep this brand isolated — every output (email, presentation, document) will use your colors, voice, and templates.
              </p>
              <div className="space-y-2.5 mb-4">
                <label className="block">
                  <span className="text-[11px] uppercase tracking-wider text-mute dark:text-dim">Company name</span>
                  <input value={setupName} onChange={(e) => setSetupName(e.target.value)}
                    autoFocus placeholder="Acme Corporation"
                    className="mt-1 w-full px-3 py-2 rounded-lg bg-fog dark:bg-graphite2 border border-line dark:border-hairline text-[13px] outline-none focus:border-gold/50" />
                </label>
                <label className="block">
                  <span className="text-[11px] uppercase tracking-wider text-mute dark:text-dim">Industry (optional)</span>
                  <input value={setupIndustry} onChange={(e) => setSetupIndustry(e.target.value)}
                    placeholder="Technology, Finance, Healthcare…"
                    className="mt-1 w-full px-3 py-2 rounded-lg bg-fog dark:bg-graphite2 border border-line dark:border-hairline text-[13px] outline-none focus:border-gold/50" />
                </label>
              </div>
              <div className="flex items-center gap-2">
                <button onClick={() => setShowSetupModal(false)}
                  className="flex-1 px-3 py-2 rounded-lg border border-line dark:border-hairline text-[12.5px] hover:bg-fog dark:hover:bg-graphite2">Cancel</button>
                <button onClick={createEnterprise} disabled={!setupName.trim()}
                  className="flex-1 px-3 py-2 rounded-lg bg-ink dark:bg-paper text-paper dark:text-ink text-[12.5px] font-medium disabled:opacity-50">Create</button>
              </div>
            </div>
          </div>
        )}

        {/* ── Brand Text Paste Modal ── */}
        {showBrandModal && (
          <div className="absolute inset-0 z-50 bg-ink/40 dark:bg-black/60 backdrop-blur-sm grid place-items-center p-4">
            <div className="w-full max-w-md rounded-2xl bg-paper dark:bg-graphite border border-line dark:border-hairline p-5 shadow-xl">
              <div className="flex items-center gap-2 mb-3">
                <Icon name="Type" size={14} className="text-goldink dark:text-goldsoft" />
                <h3 className="text-[15px] font-semibold tracking-tight">Train on brand text</h3>
              </div>
              <p className="text-[12px] text-mute dark:text-dim leading-relaxed mb-3">
                Paste brand guidelines, sample marketing copy, mission statement, or any text that shows your company's voice and terminology.
              </p>
              <textarea value={brandText} onChange={(e) => setBrandText(e.target.value)}
                placeholder="Paste your brand content here — product descriptions, taglines, mission, voice guidelines, sample emails, etc."
                rows={8} autoFocus
                className="w-full px-3 py-2.5 rounded-lg bg-fog dark:bg-graphite2 border border-line dark:border-hairline text-[12.5px] outline-none focus:border-gold/50 resize-none leading-relaxed mb-4" />
              <div className="text-[10.5px] text-mute dark:text-dim mb-3">
                {brandText.length} chars · {brandText.trim().split(/\s+/).filter(Boolean).length} words
              </div>
              <div className="flex items-center gap-2">
                <button onClick={() => { setShowBrandModal(false); setBrandText(""); }}
                  className="flex-1 px-3 py-2 rounded-lg border border-line dark:border-hairline text-[12.5px] hover:bg-fog dark:hover:bg-graphite2">Cancel</button>
                <button onClick={() => { learnFromText(); setShowBrandModal(false); }} disabled={brandText.trim().length < 20 || brandLearning}
                  className="flex-1 px-3 py-2 rounded-lg bg-ink dark:bg-paper text-paper dark:text-ink text-[12.5px] font-medium disabled:opacity-50">
                  {brandLearning ? "Learning…" : "Train"}
                </button>
              </div>
            </div>
          </div>
        )}
      </aside>
    </>
  );
}

function MemoryRow({ folder, busy, onIndex, onDelete }) {
  const stateMeta = {
    indexed:   { label: "Indexed",    dot: "bg-gold dark:bg-goldsoft", btn: "Re-index", btnTone: "ghost" },
    stale:     { label: "Re-index",   dot: "bg-mute dark:bg-dim",      btn: "Re-index", btnTone: "gold"  },
    unindexed: { label: "Not indexed",dot: "bg-line dark:bg-hairline", btn: "Index",    btnTone: "gold"  },
  }[folder.state] || {};

  return (
    <div className="group flex items-center gap-2.5 px-2 py-2 rounded-lg border border-line dark:border-hairline hover:border-gold/35 dark:hover:border-gold/30 transition-colors bg-paper dark:bg-graphite">
      <div className="w-7 h-7 rounded-md bg-fog dark:bg-graphite2 grid place-items-center text-mute dark:text-dim shrink-0 relative">
        <Icon name="FolderClosed" size={13} />
        <span className="absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full bg-paper dark:bg-graphite grid place-items-center text-gold dark:text-goldsoft">
          <LockGlyph size={7} strokeWidth={2} />
        </span>
      </div>
      <div className="min-w-0 flex-1">
        <div className="flex items-center gap-1.5">
          <span className="text-[12.5px] font-medium tracking-tight">{folder.name}</span>
          <span className="inline-flex items-center gap-1 text-[10px] text-mute dark:text-dim">
            <span className={"w-1.5 h-1.5 rounded-full " + stateMeta.dot} />
            <span>{busy ? "Indexing\u2026" : stateMeta.label}</span>
          </span>
        </div>
        <div className="text-[10.5px] text-mute dark:text-dim font-mono mt-0.5 truncate">{folder.path}</div>
        <div className="text-[10.5px] text-mute/85 dark:text-dim/85 mt-0.5">{folder.meta}</div>
      </div>
      <div className="flex items-center gap-1 shrink-0">
        <button
          onClick={onIndex}
          disabled={busy}
          className={"h-7 px-2.5 rounded-md text-[11px] tracking-tight inline-flex items-center gap-1 transition-colors " + (busy
            ? "text-mute dark:text-dim bg-fog dark:bg-graphite2 cursor-wait"
            : stateMeta.btnTone === "gold"
            ? "text-goldink dark:text-goldsoft bg-goldtint/70 dark:bg-gold/15 hover:bg-goldtint dark:hover:bg-gold/25 border border-gold/40"
            : "text-mute dark:text-dim hover:bg-fog dark:hover:bg-graphite2 border border-line dark:border-hairline")}
        >
          {busy
            ? <><Icon name="Loader2" size={11} className="animate-spin" /><span>Mapping</span></>
            : <><Icon name={folder.state === "indexed" ? "RefreshCw" : "Sparkles"} size={11} /><span>{stateMeta.btn}</span></>}
        </button>
        {onDelete && (
          <button onClick={onDelete} title="Remove folder"
            className="w-7 h-7 rounded-md grid place-items-center text-mute dark:text-dim hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-500 dark:hover:text-red-400 transition-colors border border-line dark:border-hairline">
            <Icon name="Trash2" size={11} />
          </button>
        )}
      </div>
    </div>
  );
}

function ResourceRow({ item, flash }) {
  const iconByType = { code: "FileCode2", data: "Sheet", report: "FileText", image: "Image", notes: "NotebookPen" };
  const mimeByType = { code: "text/x-python", data: "text/csv", report: "text/plain", image: "image/svg+xml", notes: "text/markdown" };
  const sampleContent = {
    code: `# ${item.name}\n# Ethoryx Sigma 2.4 — TAPSN constrained\nfrom functools import lru_cache\n\n@lru_cache(maxsize=None)\ndef fib(n):\n    return n if n < 2 else fib(n-1) + fib(n-2)\n`,
    data: "n,value,runtime_us,memory_kb\n10,55,0.8,0.3\n100,3.54e20,4.2,2.1\n1000,4.35e208,42.1,18.4\n",
    report: `Ethoryx report — ${item.name}\nGenerated locally. TAPSN verified.\n`,
    image: `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' fill='#1c1c1c'/></svg>`,
    notes: `# ${item.name}\n\nLocal notes — your data, your math.\n`,
  };

  const download = (e) => {
    e.stopPropagation();
    // If this artifact has a server file (PPTX, DOCX, etc.), download from URL
    if (item.file_url) {
      const url = item.file_url.startsWith("http") ? item.file_url : CHAT_API + item.file_url;
      window.open(url, '_blank');
      flash && flash(`Downloading — ${item.name}`);
      return;
    }
    const realContent = item.content || sampleContent[item.type] || `Ethoryx artifact: ${item.name}
`;
    saveToLocal(item.name, realContent, mimeByType[item.type] || "text/plain");
    flash && flash(`Saved — ${item.name}`);
  };
  const share = (e) => {
    e.stopPropagation();
    const link = `https://ethoryx.io/share/${item.id}-${item.name.replace(/[^a-z0-9.]/gi, "-")}`;
    navigator.clipboard?.writeText(link);
    flash && flash("Share link copied");
  };
  const preview = () => flash && flash(`Opening ${item.name}…`);

  return (
    <button
      onClick={preview}
      className="group w-full text-left flex items-center gap-2.5 rounded-lg px-2 py-2 hover:bg-fog/70 dark:hover:bg-graphite2/70 transition-colors"
    >
      <div className="w-8 h-8 rounded-md bg-fog dark:bg-graphite2 grid place-items-center shrink-0 text-mute dark:text-dim group-hover:text-goldink dark:group-hover:text-goldsoft">
        <Icon name={iconByType[item.type] || "File"} size={14} />
      </div>
      <div className="min-w-0 flex-1">
        <div className="text-[12.5px] font-mono truncate">{item.name}</div>
        <div className="text-[10.5px] text-mute dark:text-dim mt-0.5 tracking-tight flex items-center gap-1.5">
          <span className="text-gold/80 dark:text-goldsoft/80"><LockGlyph size={9} /></span>
          <span>{item.size} · {item.when}</span>
        </div>
      </div>
      <div className="opacity-0 group-hover:opacity-100 flex items-center gap-0.5 transition-opacity">
        <span
          onClick={share}
          className="w-7 h-7 rounded-md grid place-items-center text-mute dark:text-dim hover:bg-paper dark:hover:bg-ink hover:text-ink dark:hover:text-paper cursor-pointer"
          title="Copy share link"
        >
          <Icon name="Share2" size={13} />
        </span>
        <span
          onClick={download}
          className="w-7 h-7 rounded-md grid place-items-center text-goldink dark:text-goldsoft hover:bg-goldtint/70 dark:hover:bg-gold/15 cursor-pointer"
          title="Save to local folder"
        >
          <Icon name="FolderDown" size={13} />
        </span>
      </div>
    </button>
  );
}

function groupedItems(items) {
  const groups = { Today: [], "Earlier this thread": [] };
  for (const it of items) (groups[it.group || "Today"] ||= []).push(it);
  return Object.entries(groups).filter(([, l]) => l.length);
}

/* ---------- Sample content ---------- */
const fibCode = [];

const SAMPLE_LIBRARY = [];

/* ---------- Empty thread / new chat state ---------- */
function EmptyThread({ onPick, storageMode, userName }) {
  const suggestions = [
    { icon: "Mail",          label: "Write me an email about renewable energy" },
    { icon: "Presentation",  label: "Create a presentation about Ethiopian history" },
    { icon: "BookOpen",      label: "Explain how DNA replication works in detail" },
    { icon: "GitCompare",    label: "Compare quantum computing and classical computing" },
    { icon: "FileText",      label: "Draft a research report on climate change" },
    { icon: "Brain",         label: "Reason about why volcanoes form at plate boundaries" },
  ];
  return (
    <div className="max-w-[760px] mx-auto px-6 min-h-[calc(100vh-14rem)] flex flex-col justify-center rise">
      <div className="flex flex-col items-center text-center">
        <div className="w-12 h-12 rounded-2xl bg-ink dark:bg-paper text-paper dark:text-ink grid place-items-center mb-5 shadow-[0_8px_24px_-8px_rgba(0,0,0,0.25)]">
          <EthoryxMark size={20} />
        </div>
        <h1 className="text-[32px] font-display font-semibold tracking-tight leading-[1.05]">
          {userName && userName !== "You" ? `Welcome back, ${userName.split(" ")[0]}.` : "A new thread."}
        </h1>
        <p className="text-[14px] text-mute dark:text-dim mt-2 max-w-[480px] leading-[1.6]">
          {userName && userName !== "You"
            ? "Zera remembers you. Ask anything; your brand, your knowledge, your context."
            : "Your knowledge, your brand. Zera reasons over 500K facts with graph-based proof — every output traceable, every fact verifiable."}
        </p>
        <div className="mt-4 flex items-center gap-2 text-[11px]">
          <span className="inline-flex items-center gap-1.5 px-2 h-6 rounded-full border border-gold/40 bg-goldtint/60 dark:bg-gold/10 text-goldink dark:text-goldsoft">
            <ShieldGlyph size={11} strokeWidth={1.8} />
            <span className="tracking-tight font-medium">TAPSN Verified</span>
          </span>
          <span className="inline-flex items-center gap-1.5 px-2 h-6 rounded-full border border-line dark:border-hairline text-mute dark:text-dim">
            <LockGlyph size={10} />
            <span className="tracking-tight">{storageMode === "local" ? "Local-only vault" : "Cloud sync on"}</span>
          </span>
        </div>
      </div>

      <div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5 mt-10">
        {suggestions.map((s, i) => (
          <button
            key={i}
            onClick={() => onPick(s.label)}
            className="group text-left rounded-xl border border-line dark:border-hairline px-3.5 py-3 hover:border-ink/30 dark:hover:border-paper/30 hover:bg-fog/50 dark:hover:bg-graphite2/50 transition-colors flex items-center gap-3"
          >
            <span className="w-7 h-7 rounded-lg bg-fog dark:bg-graphite2 grid place-items-center text-mute dark:text-dim group-hover:text-goldink dark:group-hover:text-goldsoft transition-colors shrink-0">
              <Icon name={s.icon} size={14} />
            </span>
            <span className="text-[12.5px] tracking-tight leading-[1.45]">{s.label}</span>
            <span className="ml-auto text-mute/60 dark:text-dim/60 opacity-0 group-hover:opacity-100 transition-opacity">
              <Icon name="ArrowUpRight" size={13} />
            </span>
          </button>
        ))}
      </div>

      <div className="mt-8 flex items-center justify-center gap-4 text-[11px] text-mute dark:text-dim">
        <span className="inline-flex items-center gap-1"><kbd className="px-1.5 h-5 inline-flex items-center rounded border border-line dark:border-hairline font-mono text-[10px]">↵</kbd> send</span>
        <span className="inline-flex items-center gap-1"><kbd className="px-1.5 h-5 inline-flex items-center rounded border border-line dark:border-hairline font-mono text-[10px]">⇧↵</kbd> new line</span>
        <span className="inline-flex items-center gap-1"><kbd className="px-1.5 h-5 inline-flex items-center rounded border border-line dark:border-hairline font-mono text-[10px]">⌘K</kbd> commands</span>
      </div>
    </div>
  );
}

/* ---------- Rich content renderer (code blocks, paragraphs, bold, headers) ---------- */
/* Inline markdown: **bold**, *italic* / _italic_, `code`. */
function renderInline(str, kp) {
  if (str == null) return null;
  const out = [];
  const re = /\*\*([^*]+?)\*\*|`([^`]+?)`|\*([^*\n]+?)\*|_([^_\n]+?)_/;
  let rest = String(str), k = 0, m;
  while ((m = re.exec(rest)) !== null) {
    if (m.index > 0) out.push(rest.slice(0, m.index));
    if (m[1] !== undefined) out.push(<strong key={kp + 'b' + k}>{m[1]}</strong>);
    else if (m[2] !== undefined) out.push(<code key={kp + 'c' + k} className="px-1 py-0.5 rounded bg-fog dark:bg-graphite2 text-[12.5px] font-mono">{m[2]}</code>);
    else if (m[3] !== undefined) out.push(<em key={kp + 'i' + k}>{m[3]}</em>);
    else if (m[4] !== undefined) out.push(<em key={kp + 'u' + k}>{m[4]}</em>);
    rest = rest.slice(m.index + m[0].length);
    k++;
  }
  if (rest) out.push(rest);
  return out;
}

/* Bold a leading "Label:" — but only when it's a genuine short label
   (starts capitalized, ≤ 4 words / ≤ 28 chars, with content after). This keeps
   real labels like "Social:" / "Why:" bold without bolding every sentence that
   happens to contain a colon. */
function renderItem(str, kp) {
  const s = String(str == null ? '' : str);
  const m = s.match(/^([A-Z][A-Za-z0-9][A-Za-z0-9 &/\-]{0,26}):\s+(\S.*)$/);
  if (m && m[1].trim().split(/\s+/).length <= 4) {
    const rest = renderInline(m[2], kp + 'r');
    return [<strong key={kp + 'L'}>{m[1]}:</strong>, ' ',
            ...(Array.isArray(rest) ? rest : [rest])];
  }
  const inl = renderInline(s, kp);
  return Array.isArray(inl) ? inl : [inl];
}

/* Render one list item. A trailing analytical clause ("Why: …" / "Because: …")
   is pulled out and shown as a nested second-level bullet under the item. */
function renderListItem(it, kp) {
  const m = String(it).match(/^(.*?\S)\s+((?:Why|Because|Reason|Rationale)\s*:\s*\S.*)$/);
  const main = m ? m[1].trim() : String(it);
  const sub = m ? m[2].trim() : null;
  return (
    <li key={kp}>
      {renderItem(main, kp + 'm')}
      {sub && (
        <ul className="list-disc pl-5 mt-1 space-y-1 text-mute dark:text-dim">
          <li>{renderItem(sub, kp + 's')}</li>
        </ul>
      )}
    </li>
  );
}

/* Block markdown: paragraphs, headings, bullet/numbered lists, line breaks.
   Consecutive list lines (single-newline separated) become a real <ul>/<ol>
   instead of collapsing into one run-on paragraph. */
function renderMarkdown(content, base) {
  const lines = String(content).replace(/\r/g, '').split('\n');
  const out = [];
  let para = [];
  const flush = () => {
    if (!para.length) return;
    const key = base + '-p' + out.length;
    const kids = [];
    para.forEach((l, li) => {
      if (li > 0) kids.push(<br key={key + 'br' + li} />);
      kids.push(...renderItem(l, key + 's' + li));
    });
    out.push(<p key={key}>{kids}</p>);
    para = [];
  };
  let i = 0;
  while (i < lines.length) {
    const raw = lines[i];
    const t = raw.trim();
    if (t === '') { flush(); i++; continue; }
    const h = t.match(/^(#{1,4})\s+(.*)$/);
    if (h) {
      flush();
      const lvl = h[1].length;
      const cls = lvl <= 1 ? 'text-lg font-semibold mt-3 mb-1'
        : lvl === 2 ? 'text-base font-semibold mt-2.5 mb-1'
        : 'text-sm font-semibold mt-2 mb-0.5';
      const hk = base + '-h' + out.length;
      out.push(React.createElement('h' + Math.min(lvl + 1, 4),
        { key: hk, className: cls }, renderInline(h[2], hk)));
      i++; continue;
    }
    if (/^\s*[-*•]\s+/.test(raw)) {
      flush();
      const items = [];
      while (i < lines.length && /^\s*[-*•]\s+/.test(lines[i])) {
        items.push(lines[i].replace(/^\s*[-*•]\s+/, '')); i++;
      }
      const key = base + '-ul' + out.length;
      out.push(<ul key={key} className="list-disc pl-5 space-y-1 my-1">
        {items.map((it, ii) => renderListItem(it, key + '-' + ii))}
      </ul>);
      continue;
    }
    if (/^\s*\d+[.)]\s+/.test(raw)) {
      flush();
      const startMatch = raw.match(/^\s*(\d+)[.)]/);
      const startNum = startMatch ? parseInt(startMatch[1], 10) : 1;
      const items = [];
      // Group numbered items even when the model separates them with a blank
      // line — otherwise each becomes its own <ol> and restarts at "1.".
      while (i < lines.length) {
        if (/^\s*\d+[.)]\s+/.test(lines[i])) {
          items.push(lines[i].replace(/^\s*\d+[.)]\s+/, '')); i++;
        } else if (lines[i].trim() === '' &&
                   i + 1 < lines.length && /^\s*\d+[.)]\s+/.test(lines[i + 1])) {
          i++; // skip a blank line that merely separates two numbered items
        } else {
          break;
        }
      }
      const key = base + '-ol' + out.length;
      out.push(<ol key={key} start={startNum} className="list-decimal pl-5 space-y-1 my-1">
        {items.map((it, ii) => renderListItem(it, key + '-' + ii))}
      </ol>);
      continue;
    }
    para.push(t);
    i++;
  }
  flush();
  return out;
}

function RichContent({ text }) {
  if (!text || typeof text !== 'string') return <p>{text || ''}</p>;

  // Split on code blocks first
  const codeBlockRegex = /```(\w*)\n?([\s\S]*?)```/g;
  const parts = [];
  let lastIndex = 0;
  let match;

  while ((match = codeBlockRegex.exec(text)) !== null) {
    // Text before code block
    if (match.index > lastIndex) {
      parts.push({ type: 'text', content: text.slice(lastIndex, match.index) });
    }
    parts.push({ type: 'code', lang: match[1] || '', content: match[2].trim() });
    lastIndex = match.index + match[0].length;
  }
  // Remaining text after last code block
  if (lastIndex < text.length) {
    parts.push({ type: 'text', content: text.slice(lastIndex) });
  }
  if (parts.length === 0) parts.push({ type: 'text', content: text });

  return (
    <>
      {parts.map((part, i) => {
        if (part.type === 'code') {
          return (
            <div key={i} className="my-3 rounded-xl border border-line dark:border-hairline overflow-hidden bg-fog/50 dark:bg-graphite2">
              <div className="flex items-center justify-between px-3 py-1.5 bg-fog dark:bg-graphite2 border-b border-line dark:border-hairline">
                <span className="text-[10.5px] font-mono text-mute dark:text-dim">{part.lang || 'code'}</span>
                <button onClick={() => navigator.clipboard?.writeText(part.content)}
                  className="text-[10px] text-mute dark:text-dim hover:text-ink dark:hover:text-paper flex items-center gap-1">
                  <Icon name="Copy" size={10} /> Copy
                </button>
              </div>
              <pre className="p-3 overflow-x-auto text-[12.5px] font-mono leading-relaxed text-ink dark:text-paper/90">
                <code>{part.content}</code>
              </pre>
            </div>
          );
        }
        // Text part — full block markdown (paragraphs, lists, headings, inline).
        return <React.Fragment key={i}>{renderMarkdown(part.content, 'm' + i)}</React.Fragment>;
      })}
    </>
  );
}

/* ---------- Login prompt for unauthenticated users ---------- */
function LoginPrompt() {
  return (
    <div className="flex flex-col items-center justify-center h-full px-6 py-20 text-center">
      <div className="w-14 h-14 rounded-2xl bg-goldtint/60 dark:bg-gold/15 grid place-items-center mb-5">
        <EthoryxMark size={20} />
      </div>
      <h2 className="text-xl font-semibold tracking-tight mb-2">Welcome to Ethoryx</h2>
      <p className="text-[14px] text-mute dark:text-dim max-w-sm leading-relaxed mb-8">
        Sign in to start chatting with TAI. Your conversations, artifacts, and preferences will be saved to your account.
      </p>
      <a
        href="/login.html"
        className="inline-flex items-center gap-2 px-6 py-2.5 rounded-xl bg-ink dark:bg-paper text-paper dark:text-ink text-[13.5px] font-medium tracking-tight hover:opacity-90 transition-opacity"
      >
        <Icon name="LogIn" size={15} />
        Sign in to Ethoryx
      </a>
      <a
        href="/register.html"
        className="mt-3 text-[12.5px] text-mute dark:text-dim hover:text-ink dark:hover:text-paper transition-colors"
      >
        Don't have an account? <span className="underline">Create one</span>
      </a>
    </div>
  );
}

/* ---------- Thinking indicator ----------
   Cycles plain-wording status so the user can see Ethoryx is taking a moment to
   think (not an opaque "reasoning…"). Mirrors the backend deliberation steps. */
const THINKING_PHRASES = [
  "Reading your question…",
  "Looking through what I know…",
  "Weighing what's relevant…",
  "Composing the answer…",
];
function ThinkingDots() {
  const [i, setI] = useState(0);
  useEffect(() => {
    const t = setInterval(() => setI((n) => (n + 1) % THINKING_PHRASES.length), 1300);
    return () => clearInterval(t);
  }, []);
  return (
    <div className="rise flex items-center gap-2 text-[12.5px] text-mute dark:text-dim pt-1">
      <span className="relative flex w-1.5 h-1.5">
        <span className="absolute inset-0 rounded-full bg-gold/80 animate-ping" />
        <span className="relative w-1.5 h-1.5 rounded-full bg-gold" />
      </span>
      <span>{THINKING_PHRASES[i]}</span>
    </div>
  );
}

/* ---------- Reasoning trace ----------
   Shows what Ethoryx actually did before answering (the backend's deliberation
   steps) + a confidence tag. Collapsed by default to stay clean. */
function ReasoningTrace({ steps, confidence }) {
  const [open, setOpen] = useState(false);
  const hasSteps = Array.isArray(steps) && steps.length > 0;
  if (!hasSteps && !confidence) return null;
  return (
    <div className="mb-2 text-[12px]">
      <button
        type="button"
        onClick={() => setOpen((o) => !o)}
        className="inline-flex items-center gap-1.5 text-mute dark:text-dim hover:text-ink dark:hover:text-paper transition-colors"
      >
        <span className="text-gold">✦</span>
        <span>Thought process</span>
        {confidence && (
          <span className="px-1.5 py-[1px] rounded-full bg-fog dark:bg-graphite2 text-[9.5px] uppercase tracking-wide">
            {String(confidence).toLowerCase()}
          </span>
        )}
        {hasSteps && <span className="text-[9px]">{open ? "▾" : "▸"}</span>}
      </button>
      {open && hasSteps && (
        <div className="mt-1.5 pl-3 border-l-2 border-line dark:border-hairline space-y-1">
          {steps.map((s, idx) => (
            <div key={idx} className="text-mute dark:text-dim">{s}</div>
          ))}
        </div>
      )}
    </div>
  );
}

/* ---------- Models available ---------- */
const MODELS = typeof ETHORYX_MODELS !== "undefined"
  ? Object.entries(ETHORYX_MODELS).map(([id, config]) => ({
      id,
      label: config.name,
      badge: config.badge,
      icon: config.icon,
      color: config.color,
    }))
  : [{ id: "tapsn-8b-v2", label: "TAPSN-8B v2", badge: "TAPSN Verified", icon: "🧠", color: "#7c6ff7" }];

/* ---------- App ---------- */
const REASONER_API = "https://api.ethoryx.io/v1/reasoner/solve";
const CHAT_API     = "https://api.ethoryx.io";

function App() {
  const [t, setTweak] = window.useTweaks(TWEAK_DEFAULTS);
  const [libraryOpen, setLibraryOpen] = useState(t.showLibrary);
  const [libraryHover, setLibraryHover] = useState(false);
  const [lang, setLang] = useState("English");
  const [messages, setMessages] = useState([]);
  const [compare, setCompare] = useState(false);
  const [thinking, setThinking] = useState(false);
  // ── Session management (server-side chat history) ──
  const [sessionId, setSessionId] = useState(null);
  const [sessionList, setSessionList] = useState([]);
  const sessionIdRef = useRef(null); // stable ref for async closures
  const [settings, setSettings] = useState({
    model: typeof DEFAULT_MODEL !== "undefined" ? DEFAULT_MODEL : (MODELS[0]?.id || "tapsn-8b-v2"),
    reasoning: "balanced",
    stream: true,
    autosave: true,
    voice: false,
    sound: true,
  });
  // User identity — reads from localStorage, synced with server when logged in
  const [userName, setUserName] = useState(() => {
    // Prefer stored display name, fall back to ts_user.name from login
    const saved = localStorage.getItem("ethoryx_user_name");
    if (saved) return saved;
    try { return JSON.parse(localStorage.getItem("ts_user") || "{}").name || ""; }
    catch { return ""; }
  });
  const [userEmail, setUserEmail] = useState(() => {
    const saved = localStorage.getItem("ethoryx_user_email");
    if (saved) return saved;
    try { return JSON.parse(localStorage.getItem("ts_user") || "{}").email || ""; }
    catch { return ""; }
  });
  const displayName = userName || "You";
  const userInitials = displayName.split(" ").filter(Boolean).map(w => w[0]).join("").toUpperCase().slice(0, 2) || "U";
  // ── Auth: derive userId from login token for session scoping ──
  const [isLoggedIn, setIsLoggedIn] = useState(() => !!localStorage.getItem("ts_token"));
  const getUserId = useCallback(() => {
    try { return JSON.parse(localStorage.getItem("ts_user") || "{}").id || localStorage.getItem("ts_key") || null; }
    catch { return null; }
  }, []);

  // On mount: if the user is logged in, pull their chat profile from the server.
  // Server is the source of truth; localStorage is the offline cache.
  useEffect(() => {
    const token = localStorage.getItem("ts_token");
    if (!token) return;
    fetch(`${CHAT_API}/v1/chat/profile`, {
      headers: { "Authorization": `Bearer ${token}` },
    })
      .then(r => r.ok ? r.json() : null)
      .then(data => {
        if (!data?.profile) return;
        const { display_name, email, default_model } = data.profile;
        if (display_name) { setUserName(display_name); localStorage.setItem("ethoryx_user_name", display_name); }
        if (email)        { setUserEmail(email);        localStorage.setItem("ethoryx_user_email", email); }
        if (default_model && MODELS.find(m => m.id === default_model)) setModel(default_model);
      })
      .catch(() => {}); // stay silent — localStorage values are still used
  }, []);

  const handleSaveProfile = useCallback((name, email) => {
    setUserName(name);
    setUserEmail(email);
    // Save to server if logged in; localStorage was already written by the profile editor
    const token = localStorage.getItem("ts_token");
    if (!token) return;
    fetch(`${CHAT_API}/v1/chat/profile`, {
      method: "PUT",
      headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
      body: JSON.stringify({ display_name: name, email }),
    }).catch(() => {});
  }, []);

  const handleSignOut = useCallback(() => {
    localStorage.removeItem("ethoryx_user_name");
    localStorage.removeItem("ethoryx_user_email");
    localStorage.removeItem("ts_token");
    localStorage.removeItem("ts_user");
    localStorage.removeItem("ts_key");
    setUserName("");
    setUserEmail("");
    setIsLoggedIn(false);
    setSessionId(null);
    sessionIdRef.current = null;
    setSessionList([]);
    setMessages([]);
  }, []);

  // ── Session helpers ──
  // Keep ref in sync so async closures always have latest session ID
  useEffect(() => { sessionIdRef.current = sessionId; }, [sessionId]);

  // Persist active session ID so it survives page refresh
  useEffect(() => {
    if (sessionId) localStorage.setItem("ethoryx_current_session", sessionId);
    else localStorage.removeItem("ethoryx_current_session");
  }, [sessionId]);

  // Restore last active session on mount once login is confirmed
  useEffect(() => {
    if (!isLoggedIn) return;
    const saved = localStorage.getItem("ethoryx_current_session");
    if (saved) switchSession(saved);
  }, [isLoggedIn]); // intentionally omitting switchSession — stable ref, runs once

  const loadSessionList = useCallback(() => {
    const uid = getUserId();
    const url = uid ? `${CHAT_API}/v1/tai/sessions?user_id=${encodeURIComponent(uid)}` : `${CHAT_API}/v1/tai/sessions`;
    fetch(url)
      .then(r => r.ok ? r.json() : null)
      .then(data => { if (data?.sessions) setSessionList(data.sessions); })
      .catch(() => {});
  }, [getUserId]);

  const createNewSession = useCallback(async () => {
    try {
      const uid = getUserId();
      const body = uid ? JSON.stringify({ user_id: uid }) : "{}";
      const res = await fetch(`${CHAT_API}/v1/tai/sessions`, { method: "POST", headers: { "Content-Type": "application/json" }, body });
      if (!res.ok) return null;
      const s = await res.json();
      setSessionId(s.id);
      sessionIdRef.current = s.id;
      setMessages([]);
      loadSessionList();
      return s.id;
    } catch { return null; }
  }, [loadSessionList, getUserId]);

  const switchSession = useCallback(async (id) => {
    try {
      const res = await fetch(`${CHAT_API}/v1/tai/sessions/${id}`);
      if (!res.ok) return;
      const s = await res.json();
      setSessionId(s.id);
      sessionIdRef.current = s.id;
      // Map server messages to UI format — reconstruct email fields if server didn't persist them
      setMessages((s.messages || []).map(m => {
        let email_html = m.email_html || null;
        let email_subject = m.email_subject || m.subject || '';
        let output_format = m.output_format || 'text';
        // If the server didn't store email_html but the content is an email, rebuild it
        if (!email_html && m.role === 'assistant' && m.content && /^Subject:\s*.+/i.test(m.content)) {
          const lines = m.content.split('\n');
          const subjectLine = lines.find(l => /^Subject:/i.test(l.trim()));
          if (!email_subject) email_subject = subjectLine ? subjectLine.replace(/^Subject:\s*/i, '').trim() : '';
          const bodyText = m.content.replace(/^Subject:[^\n]*\n*/i, '').trim();
          const paras = bodyText.split(/\n{2,}/).filter(Boolean);
          let greetingLine = '', signatureLines = [];
          if (paras.length && /^(Dear|Hi|Hello|Greetings)/i.test(paras[0])) greetingLine = paras.shift();
          const signOffIdx = paras.findIndex(p => /^(Best regards|Sincerely|Kind regards|Thanks|Best|Regards)/i.test(p.trim()));
          if (signOffIdx >= 0) signatureLines = paras.splice(signOffIdx);
          const bodyHtml = paras.map(p => `<p data-section="body">${p.trim()}</p>`).join('\n');
          const sigHtml = signatureLines.length
            ? `<p data-section="signoff">${signatureLines[0]}</p>` +
              (signatureLines.slice(1).length ? `<div data-section="signature">${signatureLines.slice(1).map(l => `<p>${l}</p>`).join('')}</div>` : '')
            : '';
          email_html = `<div data-email-body>${greetingLine ? `<p data-section="greeting">${greetingLine}</p>` : ''}${bodyHtml}${sigHtml}</div>`;
          output_format = 'email';
        }
        return {
          role: m.role,
          content: m.content,
          id: m.id || m.timestamp * 1000 || Date.now(),
          certainty: m.certainty,
          z_score: m.z_score,
          latency_ms: m.latency_ms,
          file_url: m.file_url,
          email_html,
          email_subject,
          output_format,
        };
      }));
    } catch {}
  }, []);

  const deleteSessionById = useCallback(async (id) => {
    try {
      await fetch(`${CHAT_API}/v1/tai/sessions/${id}`, { method: "DELETE" });
      setSessionList(prev => prev.filter(s => s.id !== id));
      if (sessionId === id) { setSessionId(null); setMessages([]); }
    } catch {}
  }, [sessionId]);

  // Load session list on mount (only when logged in)
  useEffect(() => { if (isLoggedIn) loadSessionList(); }, [loadSessionList, isLoggedIn]);

  // ── Client-side response parser: extracts file_url, email, code from raw text ──
  // Handles both old backend (raw text) and new backend (structured fields)
  const parseResponse = useCallback((rawText, serverData) => {
    let text = rawText || '';
    let file_url = serverData?.file_url || null;
    let email_html = serverData?.email_html || null;
    let email_subject = serverData?.subject || null;
    let output_format = serverData?.output_format || 'text';

    // ── Strip internal file-path prefix lines if backend didn't catch them ──
    text = text.replace(/^(PPTX_FILE|DOCX_FILE|XLSX_FILE):[^\n]+\n?/i, '').trim();
    // Strip any exposed Download: URL lines regardless of whether file_url is set
    text = text.replace(/Download:\s*\/v1\/tai\/download\/[^\s\n]+/gi, '').trim();

    // ── Extract file download URL from text (old backend format) ──
    // Matches: "Download: /v1/tai/download/filename.pptx" or similar
    if (!file_url) {
      const dlMatch = text.match(/Download:\s*(\/v1\/tai\/download\/[^\s]+)/i);
      if (dlMatch) {
        file_url = dlMatch[1];
        text = text.replace(/Download:\s*\/v1\/tai\/download\/[^\s]+/i, '').trim();
        output_format = 'file';
      }
    }

    // ── Detect email response (old backend format) ──
    // Matches: "Subject: ... Dear/Hi [Recipient]..."
    if (!email_html && /^Subject:\s*.+/i.test(text)) {
      const lines = text.split('\n');
      const subjectLine = lines.find(l => /^Subject:/i.test(l.trim()));
      email_subject = subjectLine ? subjectLine.replace(/^Subject:\s*/i, '').trim() : '';
      // Strip subject line, build semantic HTML from remaining text
      const bodyText = text.replace(/^Subject:[^\n]*\n*/i, '').trim();
      const paras = bodyText.split(/\n{2,}/).filter(Boolean);
      // First paragraph is likely greeting, last is signature
      let greetingLine = '', signatureLines = [];
      if (paras.length && /^(Dear|Hi|Hello|Greetings)/i.test(paras[0])) {
        greetingLine = paras.shift();
      }
      // Detect sign-off + signature at end
      const signOffIdx = paras.findIndex(p => /^(Best regards|Sincerely|Kind regards|Thanks|Best|Regards)/i.test(p.trim()));
      if (signOffIdx >= 0) signatureLines = paras.splice(signOffIdx);
      const bodyHtml = paras.map(p => `<p data-section="body">${p.trim()}</p>`).join('\n');
      const sigHtml = signatureLines.length
        ? `<p data-section="signoff">${signatureLines[0]}</p>` +
          (signatureLines.slice(1).length ? `<div data-section="signature">${signatureLines.slice(1).map(l => `<p>${l}</p>`).join('')}</div>` : '')
        : '';
      email_html = `<div data-email-body>` +
        (greetingLine ? `<p data-section="greeting">${greetingLine}</p>` : '') +
        bodyHtml + sigHtml + `</div>`;
      output_format = 'email';
    }

    // ── Detect code response ──
    if (output_format === 'text' && (
      /^"""\s*Program/i.test(text) ||
      /^(def |class |import |from |#!|function |const |var |let )/.test(text) ||
      /# Context:|# TODO:|if __name__/.test(text)
    )) {
      // Wrap in code block markers so RichContent renders it properly
      const langGuess = /def |import |class |if __name__/.test(text) ? 'python'
        : /function |const |var |let /.test(text) ? 'javascript' : 'code';
      text = '```' + langGuess + '\n' + text + '\n```';
      output_format = 'code';
    }

    return { text, file_url, email_html, email_subject, output_format };
  }, []);

  const scrollRef = useRef(null);
  const bottomRef = useRef(null);
  const actionBarSetInput = useRef(null);

  useEffect(() => {
    document.documentElement.classList.toggle("dark", t.theme === "dark");
    localStorage.setItem("ethoryx_theme", t.theme);
  }, [t.theme]);

  useEffect(() => { setLibraryOpen(t.showLibrary); }, [t.showLibrary]);

  // Persist chat history to localStorage
  useEffect(() => {
    localStorage.setItem("ethoryx_chat_history", JSON.stringify(messages));
  }, [messages]);

  // Auto-scroll to bottom when messages change
  useEffect(() => {
    if (bottomRef.current) {
      bottomRef.current.scrollIntoView({ behavior: "smooth" });
    }
  }, [messages, thinking]);

  const [model, setModel] = useState(typeof DEFAULT_MODEL !== "undefined" ? DEFAULT_MODEL : MODELS[0]?.id);

  const callReasoner = useCallback(async (question, fileContext, imageData = null, fileData = null, fileNames = null) => {
    setThinking(true);
    const modelConfig = typeof ETHORYX_MODELS !== "undefined" ? ETHORYX_MODELS[model] : null;
    const fullQuestion = fileContext ? fileContext + "\n\nUser question: " + question : question;
    try {
      let res, data, answer;
      if (modelConfig) {
        let requestConfig = modelConfig.requestFormat(fullQuestion);
        // Inject session/user/enterprise context for TAI-type models
        if (modelConfig.type === 'tai') {
          const bodyObj = JSON.parse(requestConfig.body);
          const sid = sessionIdRef.current;
          if (sid) bodyObj.session_id = sid;
          const uid = getUserId();
          if (uid) bodyObj.user_id = uid;
          const storedEnt = localStorage.getItem('ethoryx_enterprise_id');
          if (storedEnt) bodyObj.enterprise_id = storedEnt;
          if (imageData) bodyObj.image_data = imageData;
          if (fileData) bodyObj.file_data = fileData;
          if (fileNames) bodyObj.file_names = fileNames;
          requestConfig = { ...requestConfig, body: JSON.stringify(bodyObj) };
        }
        res = await fetch(modelConfig.endpoint, requestConfig);
        data = await res.json();
        const mcParsed = modelConfig.parseResponse(data);
        answer = (mcParsed.text || "No response.")
          .replace(/\[TAI Certainty:[^\]]*\]/gi, "")
          .replace(/[ \t]{2,}/g, " ").trim();
        const mcMeta = parseResponse(answer, data);
        setMessages((prev) => [...prev, {
          role: "assistant", content: mcMeta.text, id: Date.now() + 1,
          certainty: mcParsed.certainty, z_score: mcParsed.z_score, latency_ms: mcParsed.latency_ms,
          file_url: mcMeta.file_url, email_html: mcMeta.email_html,
          email_subject: mcMeta.email_subject, output_format: mcMeta.output_format,
        }]);
      } else {
        // TAI brain — send with session + enterprise + user context
        const sid = sessionIdRef.current;
        const payload = { question: fullQuestion };
        if (sid) payload.session_id = sid;
        const storedEnt = localStorage.getItem('ethoryx_enterprise_id');
        if (storedEnt) payload.enterprise_id = storedEnt;
        const uid = getUserId();
        if (uid) payload.user_id = uid;
        // Send the signed-in user's name so composed emails sign off with it
        // instead of "[Your Name]". Resolve from EVERY source, and as a last
        // resort derive a clean name from the email local-part — so a logged-in
        // user always has a signature even if the account stored no name.
        let _senderName = (userName || '').trim();
        let _emailForName = (userEmail || '').trim();
        try {
          const u = JSON.parse(localStorage.getItem('ts_user') || '{}');
          if (!_senderName) _senderName = (u.name || u.full_name || u.fullName ||
                                           u.first_name || u.username || '').trim();
          if (!_emailForName) _emailForName = (u.email || '').trim();
        } catch (e) { /* no stored user */ }
        if (!_senderName && _emailForName) {
          const seg = _emailForName.split('@')[0].split(/[._\-+]+/)
            .filter(s => s && !/\d/.test(s));   // drop digit segments like "bg23"
          const pick = seg.length ? seg : _emailForName.split('@')[0].split(/[._\-+]+/);
          _senderName = pick.map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ').trim();
        }
        if (_senderName) payload.user_name = _senderName;
        if (imageData) payload.image_data = imageData;
        if (fileData) payload.file_data = fileData;
        if (fileNames) payload.file_names = fileNames;
        res = await fetch(`${CHAT_API}/v1/tai/chat`, {
          method: "POST", headers: { "Content-Type": "application/json" },
          body: JSON.stringify(payload),
        });
        data = await res.json();
        answer = (data.answer || data.generated_text || data.error || "No response.")
          .replace(/\\[TAI Certainty:[^\]]*\\]/gi, "").trim();

   // When the backend already provides email_html + output_format === 'email',
       // use its fields directly. Do NOT pass generated_text into parseResponse —
       // that re-triggers client-side email detection and overwrites the correct
       // server subject/html with a broken re-parse of the full body text.

        let msgPayload;
        if (data.email_html && data.output_format === 'email') {
          msgPayload = {
            role: "assistant",
            content: answer,
            id: Date.now() + 1,
            certainty: data.certainty,
            z_score: data.z_score,
            latency_ms: data.latency_ms,
            email_html: data.email_html,
            email_subject: (() => {
              const s = data.subject || data.email_subject || '';
              if (s) return s;
              // Fallback: extract from generated_text if backend omitted subject field
              const m = (data.generated_text || '').match(/^Subject:\s*(.+?)(?:\r?\n|$)/i);
              return m ? m[1].trim() : '';
            })(),
            output_format: 'email',
            file_url: data.file_url || null,
            enterprise: data.enterprise || null,
            reasoning: data.reasoning || null,
            confidence: data.confidence || null,
          };
        } else {
          const parsed = parseResponse(answer, data);
          msgPayload = {
            role: "assistant",
            content: parsed.text,
            id: Date.now() + 1,
            certainty: data.certainty,
            z_score: data.z_score,
            latency_ms: data.latency_ms,
            file_url: parsed.file_url,
            email_html: parsed.email_html,
            email_subject: parsed.email_subject,
            output_format: parsed.output_format,
            enterprise: data.enterprise || null,
            reasoning: data.reasoning || null,
            confidence: data.confidence || null,
          };
        }
        setMessages((prev) => [...prev, msgPayload]);
        // Refresh session list so sidebar updates with new title/preview
        if (sid) loadSessionList();
      }
    } catch (err) {
      setMessages((prev) => [...prev, { role: "assistant", content: "Connection error — please try again.", id: Date.now() + 1 }]);
    } finally {
      setThinking(false);
    }
  }, [model, loadSessionList, userName, userEmail]);

  const handleSend = useCallback(async (text) => {
    if (!text.trim() || thinking) return;
    // Auto-create a server session if none exists
    if (!sessionIdRef.current) {
      const newId = await createNewSession();
      if (!newId) { /* session creation failed — continue without session */ }
    }
    const userMsg = { role: "user", content: text.trim(), id: Date.now() };
    setMessages((prev) => [...prev, userMsg]);
    if (compare && MODELS.length > 1) {
      setThinking(true);
      try {
        const modelConfig = typeof ETHORYX_MODELS !== "undefined" ? ETHORYX_MODELS[model] : null;
        const currentModelMeta = MODELS.find(m => m.id === model) || MODELS[0];
        let res, data, answer;
        if (modelConfig) {
          const requestConfig = modelConfig.requestFormat(text.trim());
          res = await fetch(modelConfig.endpoint, requestConfig);
          data = await res.json();
          const parsed = modelConfig.parseResponse(data);
          answer = (parsed.text || "No response.").replace(/\[TAI Certainty:[^\]]*\]/gi, "").trim();
        } else {
          const sid = sessionIdRef.current;
          const payload = { question: text.trim() };
          if (sid) payload.session_id = sid;
          res = await fetch(`${CHAT_API}/v1/tai/chat`, {
            method: "POST", headers: { "Content-Type": "application/json" },
            body: JSON.stringify(payload),
          });
          data = await res.json();
          answer = (data.answer || data.generated_text || "No response.").replace(/\[TAI Certainty:[^\]]*\]/gi, "").trim();
          const cp = parseResponse(answer, data);
          answer = cp.text;
        }
        setMessages((prev) => [...prev, { role: "assistant", content: answer, id: Date.now() + 1, compared: true, modelLabel: currentModelMeta.label }]);
      } catch { setMessages((prev) => [...prev, { role: "assistant", content: "Connection error.", id: Date.now() + 1 }]); }
      finally { setThinking(false); }
    } else {
      await callReasoner(text.trim(), null);
    }
  }, [thinking, callReasoner, compare, model, createNewSession]);

  const handleSendWithFile = useCallback(async (text, files) => {
    if (thinking) return;
    const list = Array.isArray(files) ? files : [files];
    if (!list.length) return;
    // Auto-create a server session if none exists, so a conversation that STARTS
    // with a file upload still carries session context (same as handleSend).
    if (!sessionIdRef.current) {
      await createNewSession();
    }
    const names = list.map((f) => f.name).join(", ");
    const userContent = text ? `[Files: ${names}] ${text}`
                             : `[Files: ${names}] Please prepare a document from these.`;
    setMessages((prev) => [...prev, {
      role: "user", content: userContent, id: Date.now(),
      attachment: { name: names, type: list.length > 1 ? "files" : list[0].type },
    }]);
    let fileContext = "";
    let imageData = null;
    const fileData = [], fileNames = [];
    for (const f of list) {
      if (f.type === "text" && f.content) {
        fileContext += `File content (${f.name}):\n\`\`\`\n${f.content.slice(0, 3000)}\n\`\`\`\n\n`;
      } else if (f.type === "image") {
        if (!imageData) imageData = f.dataUrl || null;
        fileContext += `[Image attached: ${f.name}]\n`;
      } else if (f.type === "pdf" && f.dataUrl) {
        fileData.push(f.dataUrl);
        fileNames.push(f.name);
      } else {
        fileContext += `[File: ${f.name} — ${(f.size / 1024).toFixed(0)}KB]\n`;
      }
    }
    const q = text || (fileData.length ? "Prepare an Excel from these files"
                       : (imageData ? "What is this image?" : "Please analyze this file."));
    await callReasoner(q, fileContext, imageData,
                       fileData.length ? fileData : null,
                       fileNames.length ? fileNames : null);
  }, [thinking, callReasoner, createNewSession]);

  const density = t.density;
  const threadEmpty = messages.length === 0;

  return (
    <div className="min-h-screen bg-paper dark:bg-ink text-ink dark:text-paper">
      <Header
        theme={t.theme}
        setTheme={(v) => setTweak("theme", v)}
        compare={compare}
        setCompare={setCompare}
        libraryOpen={libraryOpen}
        setLibraryOpen={(v) => { setLibraryOpen(v); setTweak("showLibrary", v); }}
        libraryCount={messages.filter(m => m.role === "assistant" && (m.file_url || m.email_html || m.output_format === 'code' || (typeof m.content === 'string' && m.content.includes('```')))).length}
        storageMode={t.storageMode}
        onNewThread={() => { setSessionId(null); sessionIdRef.current = null; setMessages([]); localStorage.removeItem("ethoryx_chat_history"); localStorage.removeItem("ethoryx_current_session"); window.scrollTo({ top: 0, behavior: "smooth" }); }}
        settings={settings}
        setSettings={setSettings}
        model={model}
        setModel={setModel}
        userName={displayName}
        userEmail={userEmail}
        userInitials={userInitials}
        onSignOut={handleSignOut}
        onSaveProfile={handleSaveProfile}
      />

      <main
        ref={scrollRef}
        className="pt-14 pb-44 transition-[padding] duration-300"
        style={{ paddingRight: libraryOpen ? 380 : 0 }}
      >
        {!isLoggedIn ? (
          <LoginPrompt />
        ) : threadEmpty ? (
          <EmptyThread
            onPick={handleSend}
            density={density}
            storageMode={t.storageMode}
            userName={displayName}
          />
        ) : (
          <div className="max-w-[760px] mx-auto px-6">
            <div className="pt-10 pb-6">
              <div className="text-[11px] tracking-[0.18em] uppercase text-mute dark:text-dim">Ethoryx · {(MODELS.find(m => m.id === model) || MODELS[0])?.label || "TAI v2"}</div>
              <div className="mt-5 h-px bg-line dark:bg-hairline" />
            </div>

            <div className="space-y-8">
              {messages.map((msg) =>
                msg.role === "user" ? (
                  <UserBubble
                    key={msg.id}
                    msgId={msg.id}
                    onEdit={(text) => actionBarSetInput.current?.(text)}
                    onReply={(text) => actionBarSetInput.current?.(`> ${msg.content}\n\n`)}
                  >{msg.content}</UserBubble>
                ) : (
                  <AssistantBlock
                                               key={msg.id}
                                               density={density}
                                               certainty={msg.certainty}
                                               z_score={msg.z_score}
                                               latency_ms={msg.latency_ms}
                                               isDontKnow={typeof msg.content === "string" && msg.content.includes("I don't have knowledge")}
                                               file_url={msg.file_url}
                                               email_html={msg.email_html}
                                               email_subject={msg.email_subject}
                                               subject={msg.subject}
                                               generated_text={msg.generated_text}
                                               raw_text={msg.content}
                                               reasoning={msg.reasoning}
                                               confidence={msg.confidence}
                                            >
                    <RichContent text={msg.content} />
                  </AssistantBlock>
                )
              )}
              {thinking && <ThinkingDots />}
              <div ref={bottomRef} />
            </div>
          </div>
        )}
      </main>

      {isLoggedIn && <ActionBar onSend={handleSend} onSendWithFile={handleSendWithFile} lang={lang} setLang={setLang} vaultOpen={libraryOpen} model={model} setModel={setModel} thinking={thinking} setInputRef={actionBarSetInput} />}

      <SovereignVault
        open={libraryOpen}
        setOpen={(v) => { setLibraryOpen(v); setTweak("showLibrary", v); }}
        items={messages.filter(m => m.role === "assistant").map((m, i) => {
          const hasFile = !!m.file_url;
          const hasEmail = !!m.email_html;
          const hasCode = m.output_format === 'code' || (typeof m.content === 'string' && m.content.includes('```'));
          const ext = hasFile ? (m.file_url.match(/\.([^.]+)$/) || [])[1] || '' : '';
          const typeMap = { pptx: 'report', docx: 'report', xlsx: 'data', pdf: 'report', csv: 'data' };
          // Only include actual artifacts — skip plain text responses
          if (!hasFile && !hasEmail && !hasCode) return null;
          return {
            id: String(i + 1),
            name: hasFile ? m.file_url.split('/').pop() : hasEmail ? (m.email_subject || `email-${i+1}`) : `code-${i+1}`,
            type: hasFile ? (typeMap[ext.toLowerCase()] || 'report') : hasEmail ? 'report' : 'code',
            size: hasFile ? ext.toUpperCase() : hasEmail ? 'Email' : `${(m.content || '').length} chars`,
            when: "this session",
            group: "Today",
            content: m.content,
            file_url: m.file_url || null,
            email_html: m.email_html || null,
          };
        }).filter(Boolean)}
        hover={libraryHover}
        setHover={setLibraryHover}
        storageMode={t.storageMode}
        setStorageMode={(v) => setTweak("storageMode", v)}
        sessionList={sessionList}
        activeSessionId={sessionId}
        onSwitchSession={switchSession}
        onDeleteSession={deleteSessionById}
        onNewSession={() => { setSessionId(null); sessionIdRef.current = null; setMessages([]); }}
      />

      <TweakPanel t={t} setTweak={setTweak} />
    </div>
  );
}

/* ---------- Tweaks panel ---------- */
function TweakPanel({ t, setTweak }) {
  const { TweaksPanel, TweakSection, TweakRadio, TweakToggle } = window;
  if (!TweaksPanel) return null;
  return (
    <TweaksPanel title="Tweaks">
      <TweakSection label="Appearance">
        <TweakRadio
          label="Theme"
          value={t.theme}
          onChange={(v) => setTweak("theme", v)}
          options={[{ value: "light", label: "Light" }, { value: "dark", label: "Dark" }]}
        />
        <TweakRadio
          label="Density"
          value={t.density}
          onChange={(v) => setTweak("density", v)}
          options={[{ value: "comfortable", label: "Cozy" }, { value: "compact", label: "Compact" }]}
        />
      </TweakSection>
      <TweakSection label="Workspace">
        <TweakToggle label="Compare models" value={t.compare} onChange={(v) => setTweak("compare", v)} />
        <TweakToggle label="Pin vault open" value={t.showLibrary} onChange={(v) => setTweak("showLibrary", v)} />
        <TweakRadio
          label="Storage"
          value={t.storageMode}
          onChange={(v) => setTweak("storageMode", v)}
          options={[{ value: "local", label: "Local" }, { value: "cloud", label: "Cloud" }]}
        />
      </TweakSection>
    </TweaksPanel>
  );
}

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