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

// ---- tiny markdown → HTML (safe subset) ----
function mdToHtml(src) {
  if (!src) return "";
  let s = src
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
  s = s.replace(/^###\s+(.+)$/gm, "<h3>$1</h3>");
  s = s.replace(/^##\s+(.+)$/gm, "<h3>$1</h3>");
  s = s.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
  s = s.replace(/\*(.+?)\*/g, "<em>$1</em>");
  s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
  s = s.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
  // lists: group consecutive "- " lines
  s = s.replace(/(^|\n)([-*]) (.+?)(?=\n(?![-*])|$)/gs, (m, pre, _b, body) => {
    const items = body.split(/\n[-*] /).map(x => `<li>${x.trim()}</li>`).join("");
    return `${pre}<ul>${items}</ul>`;
  });
  // paragraphs
  s = s.split(/\n{2,}/).map(p => {
    if (/^(<h3|<ul|<ol)/.test(p.trim())) return p;
    return `<p>${p.replace(/\n/g, "<br/>")}</p>`;
  }).join("\n");
  return s;
}

// ---- streaming helpers ----
async function* streamNDJSON(response) {
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buf = "";
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buf += decoder.decode(value, { stream: true });
    let idx;
    while ((idx = buf.indexOf("\n")) !== -1) {
      const line = buf.slice(0, idx).trim();
      buf = buf.slice(idx + 1);
      if (line) {
        try { yield JSON.parse(line); } catch {}
      }
    }
  }
  if (buf.trim()) {
    try { yield JSON.parse(buf); } catch {}
  }
}

async function postChatStream({ message, history }) {
  const headers = { "Content-Type": "application/json" };
  const t = window.MGR_API && window.MGR_API.token();
  if (t) headers["Authorization"] = "Bearer " + t;
  const resp = await fetch("/api/chat/stream", {
    method: "POST",
    headers,
    body: JSON.stringify({ message, history }),
  });
  if (!resp.ok) {
    const text = await resp.text();
    throw new Error(`API ${resp.status}: ${text || resp.statusText}`);
  }
  return resp;
}

// ---- icons (inline SVG, same visual language) ----
const Ico = {
  spark: (p = {}) => (
    <svg width={p.size||16} height={p.size||16} viewBox="0 0 24 24" fill="none" {...p}>
      <path d="M12 3l1.9 5.4L19 10l-5.1 1.6L12 17l-1.9-5.4L5 10l5.1-1.6z" fill={p.color||"currentColor"}/>
      <circle cx="19" cy="5" r="1.5" fill={p.color||"currentColor"}/>
    </svg>
  ),
  send: (p = {}) => (
    <svg width={p.size||18} height={p.size||18} viewBox="0 0 24 24" fill="none">
      <path d="M4 12l16-8-7 16-2-7-7-1z" fill="currentColor"/>
    </svg>
  ),
  plus: (p = {}) => (
    <svg width={p.size||16} height={p.size||16} viewBox="0 0 24 24" fill="none">
      <path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
    </svg>
  ),
  mic: (p = {}) => (
    <svg width={p.size||15} height={p.size||15} viewBox="0 0 24 24" fill="none">
      <rect x="9" y="3" width="6" height="12" rx="3" fill="currentColor"/>
      <path d="M6 11a6 6 0 0012 0M12 17v4M9 21h6" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/>
    </svg>
  ),
  alert: (p = {}) => (
    <svg width={p.size||16} height={p.size||16} viewBox="0 0 24 24" fill="none">
      <path d="M12 3l10 17H2z" fill={p.color||"currentColor"}/>
      <path d="M12 10v5M12 17.5v.5" stroke="#fff" strokeWidth="1.8" strokeLinecap="round"/>
    </svg>
  ),
  note: (p = {}) => (
    <svg width={p.size||14} height={p.size||14} viewBox="0 0 24 24" fill="none">
      <path d="M6 3h9l5 5v13a2 2 0 01-2 2H6a2 2 0 01-2-2V5a2 2 0 012-2z" fill="currentColor"/>
      <path d="M15 3v5h5" stroke="#fff" strokeWidth="1.6"/>
    </svg>
  ),
};

// ---- conversation storage ----
const STORE_KEY = "mgr_chat_conversations_v1";
function loadConvos() {
  try { return JSON.parse(localStorage.getItem(STORE_KEY)) || []; } catch { return []; }
}
function saveConvos(list) {
  try { localStorage.setItem(STORE_KEY, JSON.stringify(list)); } catch {}
}

// ---- Voice orb: particle field / orbital ----
function VoiceOrbWeb({ state }) {
  const particles = useMemo(() => {
    const N = 48;
    const arr = [];
    for (let i = 0; i < N; i++) {
      const angle = (i / N) * 360 + (Math.random() * 10 - 5);
      const shell = Math.random();
      const radius = 70 + shell * 95;
      const size = 2.5 + Math.random() * Math.random() * 6;
      const duration = 7 + Math.random() * 10;
      const delay = -(Math.random() * duration);
      const opacity = 0.45 + Math.random() * 0.5;
      const durT = 2.2 + Math.random() * 1.8;
      const pulseDur = 0.55 + Math.random() * 0.5;
      arr.push({ angle, radius, size, duration, delay, opacity, durT, pulseDur });
    }
    return arr;
  }, []);

  return (
    <div className={`orb-wrap ${state}`}>
      <div className="pf-halo"/>
      <div className="pf-ring"/>
      <div className="pf-center"/>
      {particles.map((p, i) => (
        <span
          key={i}
          className="pf-particle"
          style={{
            ["--a"]: `${p.angle}deg`,
            ["--r"]: `${p.radius}px`,
            ["--s"]: `${p.size}px`,
            ["--dur"]: `${p.duration}s`,
            ["--delay"]: `${p.delay}s`,
            ["--op"]: p.opacity,
            ["--durT"]: `${p.durT}s`,
            ["--pulseDur"]: `${p.pulseDur}s`,
          }}
        />
      ))}
    </div>
  );
}

function fmtElapsed(s) {
  return `${String(Math.floor(s/60)).padStart(2,"0")}:${String(s%60).padStart(2,"0")}`;
}

function VoiceView({ conv, onAppendTurn, onAppendEvidence, onUrgency }) {
  const [state, setState] = useState("idle");
  const [elapsed, setElapsed] = useState(0);
  const [liveCaption, setLiveCaption] = useState({ who: "idle", text: "" });
  const [error, setError] = useState(null);
  const [muted, setMuted] = useState(false);
  const sessionRef = useRef(null);

  useEffect(() => {
    let iv;
    if (sessionRef.current) iv = setInterval(() => setElapsed(e => e + 1), 1000);
    return () => { if (iv) clearInterval(iv); };
  }, [sessionRef.current]);

  useEffect(() => {
    return () => { if (sessionRef.current) sessionRef.current.stop(); };
  }, []);

  async function start() {
    if (sessionRef.current) return;
    setError(null);
    setElapsed(0);
    setLiveCaption({ who: "idle", text: "Connecting…" });

    const s = new window.MGR_VOICE.VoiceSession({
      wsUrl: window.MGR_VOICE.buildWsUrl(),
      onState: (st) => setState(st === "connecting" ? "thinking" : st),
      onEvent: (e) => {
        switch (e.type) {
          case "session_ready":
            setLiveCaption({ who: "ready", text: "I'm listening — what's going on?" });
            break;
          case "assistant_transcript_delta":
            setLiveCaption({ who: "assistant", text: e.full });
            break;
          case "assistant_transcript_done":
            onAppendTurn && onAppendTurn("assistant", e.text);
            setLiveCaption({ who: "assistant", text: e.text });
            break;
          case "user_transcript":
            onAppendTurn && onAppendTurn("user", e.text);
            setLiveCaption({ who: "user", text: e.text });
            break;
          case "tool_call":
            setLiveCaption({ who: "tool", text: `Searching the graph for: "${e.query}"` });
            break;
          case "tool_result":
            onAppendEvidence && onAppendEvidence(e.chunks);
            break;
          case "session_timeout":
            setError("Session ended at the 5-minute cap.");
            break;
          case "server_error":
            setError(e.error && e.error.message ? e.error.message : "Server error");
            break;
          case "_closed":
            sessionRef.current = null;
            setState("idle");
            onUrgency && onUrgency(null);
            break;
          default: break;
        }
      },
    });
    try {
      await s.start();
      sessionRef.current = s;
    } catch (err) {
      setError(err && err.message ? err.message : "Could not start session");
      sessionRef.current = null;
    }
  }

  function stop() {
    if (sessionRef.current) sessionRef.current.stop();
    sessionRef.current = null;
    setState("idle");
  }

  function toggleMute() {
    const next = !muted;
    setMuted(next);
    if (sessionRef.current) sessionRef.current.toggleMute(next);
  }

  const stateCopy = {
    idle:      { label: "Ready",     dot: "#a3a597", caption: "Tap Start call to begin" },
    listening: { label: "Listening", dot: "#1a9eab", caption: "Go ahead, I'm listening" },
    thinking:  { label: "Thinking",  dot: "#d49341", caption: "Searching the knowledge graph…" },
    speaking:  { label: "Speaking",  dot: "#5a9e7a", caption: "" },
  };
  const sc = stateCopy[state] || stateCopy.idle;
  const showCaption = liveCaption.text || sc.caption;
  const isLive = !!sessionRef.current;

  const displayCaption = (showCaption || "").slice(-220);

  return (
    <div className="voice-stage">
      <div className="voice-status">
        <div className="chip">
          <span className="led" style={{ background: sc.dot, color: sc.dot }}/>
          {sc.label}
        </div>
        <div className="timer">{fmtElapsed(elapsed)} · {isLive ? "live" : "ready"} · shimmer</div>
        {error && <div style={{ marginTop: 8, fontSize: 12.5, color: "var(--critical)" }}>{error}</div>}
      </div>

      <div className="orb-stage">
        <VoiceOrbWeb state={state}/>
      </div>

      <div className="voice-caption">
        <div className="who">
          {liveCaption.who === "user" ? "You said" :
           liveCaption.who === "assistant" ? "MedGraphRag" :
           liveCaption.who === "tool" ? "Consulting sources" :
           liveCaption.who === "ready" ? "Ready" :
           state === "idle" ? "Ready" : state}
        </div>
        <div className="txt">
          {displayCaption ? (
            <>
              {showCaption && showCaption.length > 220 && <span style={{ color: "var(--text-dim)" }}>…</span>}
              {displayCaption}
            </>
          ) : <span style={{ color: "var(--text-dim)" }}>—</span>}
          {(state === "listening" || state === "speaking") && liveCaption.text && (
            <span className="caret" style={{ display: "inline-block", width: 2, height: 14, marginLeft: 3 }}/>
          )}
        </div>
      </div>

      <div className="voice-controls">
        <button className={`voice-btn ${muted ? "muted" : ""}`} onClick={toggleMute} disabled={!isLive} title={muted ? "Unmute" : "Mute"} style={{ opacity: isLive ? 1 : 0.5, cursor: isLive ? "pointer" : "not-allowed" }}>
          <Ico.mic size={20} color={muted ? "#fff" : "currentColor"}/>
        </button>

        {!isLive ? (
          <button className="voice-start-cta" onClick={start}>
            <Ico.mic size={17} color="#fff"/>
            Start call
          </button>
        ) : (
          <button className="voice-btn danger" onClick={stop} title="End call" style={{ width: 72, height: 56, borderRadius: 22 }}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><rect x="5" y="5" width="14" height="14" rx="2" fill="#fff"/></svg>
          </button>
        )}

        <a className="voice-btn" href="#" onClick={(e) => { e.preventDefault(); window.location.hash = "chat"; }} title="Switch to text">
          <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
            <path d="M4 5h16v10H7l-3 3V5z" stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round"/>
          </svg>
        </a>
      </div>
    </div>
  );
}

const QUICK_PROMPTS = [
  "I've had a headache on the right side for two days — should I worry?",
  "I have a fever of 38.5°C and a cough. What should I do?",
  "Can I take ibuprofen if I have high blood pressure?",
  "My throat hurts and I feel tired. Is this worth seeing a doctor?",
];

function App() {
  const [convos, setConvos] = useState(() => loadConvos());
  const [activeId, setActiveId] = useState(() => {
    const c = loadConvos();
    return c[0]?.id || null;
  });
  const [draft, setDraft] = useState("");
  const [streaming, setStreaming] = useState(false);
  const [error, setError] = useState(null);
  const [view, setView] = useState(() => (window.location.hash === "#voice" ? "voice" : "chat"));
  const threadRef = useRef(null);
  const taRef = useRef(null);

  useEffect(() => {
    const onHash = () => setView(window.location.hash === "#voice" ? "voice" : "chat");
    window.addEventListener("hashchange", onHash);
    return () => window.removeEventListener("hashchange", onHash);
  }, []);

  const active = useMemo(() => convos.find(c => c.id === activeId) || null, [convos, activeId]);

  useEffect(() => { saveConvos(convos); }, [convos]);
  useEffect(() => {
    if (threadRef.current) threadRef.current.scrollTop = threadRef.current.scrollHeight;
  }, [active, streaming]);

  function newConvo(seedMessage) {
    const id = `c_${Date.now()}`;
    const now = Date.now();
    const c = {
      id, title: "New chat", createdAt: now, updatedAt: now,
      messages: seedMessage ? [{ role: "user", content: seedMessage, t: now }] : [],
      urgency: null,
    };
    setConvos(prev => [c, ...prev]);
    setActiveId(id);
    return c;
  }

  function updateConvo(id, patch) {
    setConvos(prev => prev.map(c => c.id === id ? { ...c, ...patch } : c));
  }

  function appendAssistantDelta(id, delta) {
    setConvos(prev => prev.map(c => {
      if (c.id !== id) return c;
      const msgs = [...c.messages];
      const last = msgs[msgs.length - 1];
      if (last && last.role === "assistant") {
        msgs[msgs.length - 1] = { ...last, content: (last.content || "") + delta };
      } else {
        msgs.push({ role: "assistant", content: delta, t: Date.now() });
      }
      return { ...c, messages: msgs, updatedAt: Date.now() };
    }));
  }

  function setAssistantRetrieval(id, chunks) {
    setConvos(prev => prev.map(c => {
      if (c.id !== id) return c;
      const msgs = [...c.messages];
      msgs.push({ role: "assistant", content: "", chunks, t: Date.now() });
      return { ...c, messages: msgs };
    }));
  }

  function finalizeAssistant(id, urgency) {
    setConvos(prev => prev.map(c => {
      if (c.id !== id) return c;
      const msgs = [...c.messages];
      const last = msgs[msgs.length - 1];
      if (last && last.role === "assistant") {
        msgs[msgs.length - 1] = { ...last, urgency };
      }
      return { ...c, urgency, messages: msgs };
    }));
  }

  function voiceAppendTurn(role, text) {
    if (!text || !text.trim()) return;
    let conv = active;
    if (!conv) conv = newConvo();
    setConvos(prev => prev.map(c => {
      if (c.id !== conv.id) return c;
      const msgs = [...c.messages, { role, content: text.trim(), t: Date.now(), via: "voice" }];
      return { ...c, messages: msgs, updatedAt: Date.now(), title: c.messages.length === 0 && role === "user" ? text.slice(0, 60) : c.title };
    }));
  }

  function voiceAppendEvidence(chunks) {
    if (!chunks || !chunks.length) return;
    let conv = active;
    if (!conv) conv = newConvo();
    const normalized = chunks.map(c => ({
      chunk_id: c.chunk_id,
      title: c.title,
      source: c.source,
      url: c.url,
      path: c.retrieval_path || c.path,
      snippet: (c.text || c.snippet || "").slice(0, 320),
    }));
    setConvos(prev => prev.map(c => {
      if (c.id !== conv.id) return c;
      const msgs = [...c.messages];
      const last = msgs[msgs.length - 1];
      if (last && last.role === "assistant" && last.via === "voice") {
        msgs[msgs.length - 1] = { ...last, chunks: [...(last.chunks || []), ...normalized] };
      } else {
        msgs.push({ role: "assistant", content: "", chunks: normalized, t: Date.now(), via: "voice" });
      }
      return { ...c, messages: msgs, updatedAt: Date.now() };
    }));
  }

  async function send(text) {
    const t = (text ?? draft).trim();
    if (!t || streaming) return;
    setError(null);
    setDraft("");
    let conv = active;
    if (!conv) conv = newConvo(t);
    else updateConvo(conv.id, {
      messages: [...conv.messages, { role: "user", content: t, t: Date.now() }],
      updatedAt: Date.now(),
      title: conv.messages.length === 0 ? t.slice(0, 60) : conv.title,
    });

    const history = (conv?.messages || [])
      .filter(m => m.content && m.role !== "system")
      .map(m => ({ role: m.role, content: m.content }));

    setStreaming(true);
    try {
      const resp = await postChatStream({ message: t, history });
      for await (const evt of streamNDJSON(resp)) {
        if (evt.type === "retrieval") {
          setAssistantRetrieval(conv.id, evt.chunks || []);
        } else if (evt.type === "delta") {
          appendAssistantDelta(conv.id, evt.content || "");
        } else if (evt.type === "urgency") {
          finalizeAssistant(conv.id, evt.level);
        } else if (evt.type === "error") {
          setError(evt.message || "Stream error");
        }
      }
    } catch (e) {
      setError(e.message || String(e));
    } finally {
      setStreaming(false);
    }
  }

  function onKeyDown(e) {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      send();
    }
  }

  useEffect(() => {
    if (taRef.current) {
      taRef.current.style.height = "auto";
      taRef.current.style.height = Math.min(taRef.current.scrollHeight, 180) + "px";
    }
  }, [draft]);

  const lastAssistantChunks = useMemo(() => {
    if (!active) return [];
    for (let i = active.messages.length - 1; i >= 0; i--) {
      const m = active.messages[i];
      if (m.role === "assistant" && m.chunks && m.chunks.length) return m.chunks;
    }
    return [];
  }, [active]);

  const urgencyBanner = active?.urgency === "emergency"
    ? { cls: "emergency", text: "The assistant suggested this may be an emergency. If in doubt, call your local emergency number now." }
    : active?.urgency === "urgent"
      ? { cls: "urgent", text: "The assistant suggested same-day / urgent care. Consider contacting a clinician today." }
      : null;

  return (
    <div className="shell">
      {/* LEFT: conversations */}
      <aside className="aside aside-left">
        <div className="wordmark">
          <div className="dot"><Ico.spark size={16} color="#fff"/></div>
          <div>
            <div className="name">MedGraphRag</div>
            <div className="tag">Graph-grounded health chat</div>
          </div>
        </div>
        <button className="new-chat" onClick={() => newConvo()}>
          <Ico.plus size={15}/> New chat
        </button>

        <h4>Recent</h4>
        <div className="conv-list">
          {convos.length === 0 && (
            <div style={{ fontSize: 12, color: "var(--text-dim)", padding: "6px 10px" }}>
              No conversations yet. Ask something to start.
            </div>
          )}
          {convos.map(c => (
            <button key={c.id}
              className={`conv ${c.id === activeId ? "active" : ""}`}
              onClick={() => setActiveId(c.id)}>
              <div style={{ fontWeight: 600 }}>{c.title || "Untitled"}</div>
              <div className="meta">
                {new Date(c.updatedAt).toLocaleString()} · {c.messages.length} turns
              </div>
            </button>
          ))}
        </div>

        <h4>Quick prompts</h4>
        <div className="quick">
          {QUICK_PROMPTS.map((q, i) => (
            <button key={i} onClick={() => send(q)} disabled={streaming}>{q}</button>
          ))}
        </div>
      </aside>

      {/* CENTER: chat */}
      <main className="main">
        <div className="main-header">
          <h1>{active?.title || "New chat"}</h1>
          <span className="mode-chip">GraphRAG · Shimmer</span>
          <div className="view-toggle">
            <button className={view === "chat" ? "active" : ""} onClick={() => { window.location.hash = "chat"; setView("chat"); }}>
              <svg width="13" height="13" viewBox="0 0 24 24" fill="none"><path d="M4 5h16v10H7l-3 3V5z" stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round"/></svg>
              Chat
            </button>
            <button className={view === "voice" ? "active" : ""} onClick={() => { window.location.hash = "voice"; setView("voice"); }}>
              <Ico.mic size={13}/>
              Voice
            </button>
          </div>
        </div>

        {urgencyBanner && (
          <div className={`urgency-banner ${urgencyBanner.cls}`}>
            <Ico.alert color="currentColor" size={16}/>
            <div>{urgencyBanner.text}</div>
          </div>
        )}

        {view === "voice" && (
          <VoiceView
            conv={active}
            onAppendTurn={voiceAppendTurn}
            onAppendEvidence={voiceAppendEvidence}
            onUrgency={() => {}}
          />
        )}

        {view === "chat" && (
        <>
        <div className="thread" ref={threadRef}>
          {(!active || active.messages.length === 0) && (
            <div style={{ padding: "40px 10px", textAlign: "center", color: "var(--text-dim)" }}>
              <div style={{
                width: 64, height: 64, borderRadius: 18, margin: "0 auto 18px",
                background: "linear-gradient(135deg, var(--primary), #3cd0de)",
                display: "flex", alignItems: "center", justifyContent: "center",
                boxShadow: "0 14px 30px -10px var(--primary)",
              }}>
                <Ico.spark size={26} color="#fff"/>
              </div>
              <div style={{ fontFamily: "'Manrope', sans-serif", fontSize: 22, fontWeight: 700, color: "var(--text)", letterSpacing: "-0.02em", marginBottom: 8 }}>
                What can I help with today?
              </div>
              <div style={{ fontSize: 14, lineHeight: 1.5, maxWidth: 480, margin: "0 auto", textWrap: "pretty" }}>
                I'm a voice-first health guidance assistant. Ask me a question and I'll search a medical knowledge graph for grounded answers. I'm not a doctor — always verify with a clinician.
              </div>
            </div>
          )}

          {active?.messages.map((m, i) => {
            if (m.role === "user") return (
              <div key={i} className="bubble user">
                <div className="content">{m.content}</div>
              </div>
            );
            const html = mdToHtml(m.content || "");
            const isStreaming = streaming && i === active.messages.length - 1 && !m.urgency;
            return (
              <div key={i} className="bubble ai">
                <div className="avatar">M</div>
                <div style={{ maxWidth: "78%", flex: "0 1 auto" }}>
                  <div className="content">
                    {m.content ? (
                      <div dangerouslySetInnerHTML={{ __html: html + (isStreaming ? '<span class="caret"></span>' : "") }}/>
                    ) : (
                      <div className="thinking">
                        <span className="dot"/><span className="dot"/><span className="dot"/>
                        <span style={{ marginLeft: 8 }}>
                          {m.chunks && m.chunks.length ? `Reviewing ${m.chunks.length} source${m.chunks.length>1?"s":""}…` : "Searching the knowledge graph…"}
                        </span>
                      </div>
                    )}
                    {m.chunks && m.chunks.length > 0 && m.content && (
                      <div className="inline-refs">
                        {m.chunks.slice(0, 4).map((c, j) => (
                          <a key={j} className="ref" href={c.url || "#"} target="_blank" rel="noopener noreferrer" title={c.snippet}>
                            <Ico.note size={11}/>
                            {c.title || c.source || "source"}
                            <span className="path-tag">{c.path || "vector"}</span>
                          </a>
                        ))}
                      </div>
                    )}
                  </div>
                </div>
              </div>
            );
          })}

          {error && <div className="error-card">Error: {error}</div>}
        </div>

        <div className="composer">
          <div className="box">
            <textarea
              ref={taRef}
              value={draft}
              placeholder={streaming ? "Responding…" : "Describe your symptoms or ask a health question…"}
              onChange={(e) => setDraft(e.target.value)}
              onKeyDown={onKeyDown}
              disabled={streaming}
              rows={1}
            />
            <button className="send" onClick={() => send()} disabled={streaming || !draft.trim()} title="Send (Enter)">
              <Ico.send size={18}/>
            </button>
          </div>
          <div className="disclaimer">
            Educational guidance only · Not a diagnosis · In an emergency, call your local emergency number.
          </div>
        </div>
        </>
        )}
      </main>

      {/* RIGHT: evidence */}
      <aside className="aside aside-right">
        <h4>Evidence</h4>
        {lastAssistantChunks.length === 0 ? (
          <div className="evidence-empty">
            Sources retrieved from the MedGraphRag knowledge graph will show here with each answer.
          </div>
        ) : (
          <div style={{ display: "flex", flexDirection: "column", gap: 10, overflowY: "auto" }}>
            {lastAssistantChunks.map((c, i) => {
              const cls = c.path === "graph" ? "graph" : c.path === "vector+graph" ? "both" : "";
              return (
                <a key={i} className="ev-card" href={c.url || "#"} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
                  <div className="title">{c.title || "Source"}</div>
                  <div className="meta">
                    <span className={`tag ${cls}`}>{c.path || "vector"}</span>
                    <span>{c.source || "?"}</span>
                    {c.score && <span>score {Number(c.score).toFixed(2)}</span>}
                  </div>
                  <div className="snippet">{c.snippet}</div>
                </a>
              );
            })}
          </div>
        )}

        <h4 style={{ marginTop: 18 }}>How it works</h4>
        <div style={{ fontSize: 12, color: "var(--text-dim)", lineHeight: 1.55, letterSpacing: "-0.005em" }}>
          Your question is embedded and retrieved through a graph of medical concepts and documents. The model then answers using only those retrieved passages. The "path" tag shows whether a passage was found by vector similarity, graph traversal, or both.
        </div>
      </aside>
    </div>
  );
}

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