// Original garnet/cream gamecock-style avatar (not official UofSC "Cocky" artwork — that's
// trademarked). Beak animates open/closed while `talking` is true.
function TalkingAvatar({ talking }) {
  return (
    <div className={`bubble-avatar talk-avatar${talking ? " talking" : ""}`}>
      <svg viewBox="0 0 28 28" width="28" height="28" aria-hidden="true">
        <circle cx="14" cy="14" r="14" fill="var(--accent)" />
        <circle cx="7" cy="9" r="1.5" fill="var(--accent-deep)" />
        <circle cx="10" cy="6.5" r="1.7" fill="var(--accent-deep)" />
        <circle cx="13.5" cy="7.5" r="1.5" fill="var(--accent-deep)" />
        <circle cx="11" cy="15" r="7" fill="var(--paper)" />
        <circle cx="12" cy="13" r="1.1" fill="var(--ink)" />
        <polygon points="17,15 24,13 24,15" fill="var(--accent-deep)" />
        <polygon
          className="talk-beak-lower"
          points="17,15 24,15 24,17"
          fill="var(--ink)"
          style={{ transformOrigin: "17px 15px" }}
        />
      </svg>
    </div>
  );
}

// Main chat area: topbar + tabs + thread + composer
function ChatView({ panelOpen, togglePanel, currentUser, setCurrentUser, conversationId, setConversationId, loadedMessages, setLoadedMessages }) {
  const [messages, setMessages] = React.useState([]);
  const [draft, setDraft] = React.useState("");
  const [streaming, setStreaming] = React.useState(false);
  const [attaching, setAttaching] = React.useState(false);
  const [listening, setListening] = React.useState(false);
  const scrollRef = React.useRef(null);
  const taRef = React.useRef(null);
  const fileInputRef = React.useRef(null);
  const recognitionRef = React.useRef(null);
  const activeConvoId = React.useRef(conversationId);

  // load messages when switching to a saved conversation
  React.useEffect(() => {
    if (loadedMessages) {
      setMessages(loadedMessages);
      setLoadedMessages(null);
    } else if (conversationId === null) {
      setMessages([]);
    }
    activeConvoId.current = conversationId;
  }, [conversationId, loadedMessages]);

  const started = messages.length > 0;

  React.useEffect(() => {
    if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
  }, [messages, streaming]);

  const autoGrow = (el) => {
    if (!el) return;
    el.style.height = "auto";
    el.style.height = Math.min(el.scrollHeight, 180) + "px";
  };

  const send = async (textRaw) => {
    const text = (textRaw ?? draft).trim();
    if (!text || streaming) return;
    const userMsg = { id: Date.now(), role: "user", content: text };
    setMessages(m => [...m, userMsg]);
    setDraft("");
    if (taRef.current) taRef.current.style.height = "auto";
    setStreaming(true);

    try {
      const token = localStorage.getItem("token");

      // create a new conversation on the first message
      if (!activeConvoId.current) {
        const convoRes = await fetch(`${window.API_BASE}/api/conversations`, {
          method: "POST",
          headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token },
          body: JSON.stringify({ title: text.slice(0, 80) }),
        });
        const convo = await convoRes.json();
        activeConvoId.current = convo.id;
        if (setConversationId) setConversationId(convo.id);
        if (window.__refreshConversations) window.__refreshConversations();
      }

      // save user message
      await fetch(`${window.API_BASE}/api/conversations/${activeConvoId.current}/messages`, {
        method: "POST",
        headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token },
        body: JSON.stringify({ role: "user", content: text }),
      });

      const res = await fetch(`${window.API_BASE}/api/chat`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "Authorization": "Bearer " + token,
        },
        body: JSON.stringify({
          message: text,
          history: messages.map(m => ({
            role: m.role,
            content: m.role === "user" ? m.content : m.body?.join(" ") || ""
          }))
        }),
      });

      if (!res.ok) {
        const errText = await res.text();
        throw new Error("Server error " + res.status + ": " + errText);
      }

      const reader = res.body.getReader();
      const decoder = new TextDecoder();
      let fullText = "";
      const assistantId = Date.now() + 1;

      setMessages(m => [...m, {
        id: assistantId, role: "assistant",
        intro: null, tool: null, body: [""], pills: [],
      }]);

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        fullText += decoder.decode(value, { stream: true });
        setMessages(m => m.map(msg =>
          msg.id === assistantId ? { ...msg, body: [fullText] } : msg
        ));
      }

      // save assistant reply
      if (activeConvoId.current && fullText) {
        await fetch(`${window.API_BASE}/api/conversations/${activeConvoId.current}/messages`, {
          method: "POST",
          headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token },
          body: JSON.stringify({ role: "assistant", content: fullText }),
        });
      }
    } catch (err) {
      setMessages(m => [...m, {
        id: Date.now() + 1, role: "assistant",
        intro: null, tool: null,
        body: ["Error: " + (err.message || "Could not reach the server.")],
        pills: [],
      }]);
    } finally {
      setStreaming(false);
    }
  };

  const postAssistantNotice = async (text) => {
    const token = localStorage.getItem("token");
    if (!activeConvoId.current) {
      const convoRes = await fetch(`${window.API_BASE}/api/conversations`, {
        method: "POST",
        headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token },
        body: JSON.stringify({ title: text.slice(0, 80) }),
      });
      const convo = await convoRes.json();
      activeConvoId.current = convo.id;
      if (setConversationId) setConversationId(convo.id);
      if (window.__refreshConversations) window.__refreshConversations();
    }
    setMessages(m => [...m, { id: Date.now(), role: "assistant", intro: null, tool: null, body: [text], pills: [] }]);
    await fetch(`${window.API_BASE}/api/conversations/${activeConvoId.current}/messages`, {
      method: "POST",
      headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token },
      body: JSON.stringify({ role: "assistant", content: text }),
    });
  };

  const handleAttachClick = () => {
    if (fileInputRef.current) fileInputRef.current.click();
  };

  const handleFileChange = async (e) => {
    const file = e.target.files && e.target.files[0];
    e.target.value = ""; // allow re-selecting the same file next time
    if (!file) return;

    setAttaching(true);
    try {
      const token = localStorage.getItem("token");
      const formData = new FormData();
      formData.append("file", file);
      const res = await fetch(`${window.API_BASE}/api/parse-degreeworks`, {
        method: "POST",
        headers: { "Authorization": "Bearer " + token },
        body: formData,
      });
      const data = await res.json();
      if (data.error) throw new Error(data.error);

      const existing = (currentUser.completedCourses || []).map(c => c.toUpperCase());
      const parsed = (data.completed_courses || []).map(c => c.toUpperCase());
      const merged = Array.from(new Set([...existing, ...parsed]));
      const newlyAdded = parsed.filter(c => !existing.includes(c));

      const body = { completed_courses: merged };
      if (data.major && (!currentUser.major || currentUser.major === "Not set")) body.major = data.major;
      if (data.year && (!currentUser.year || currentUser.year === "Not set")) body.year = data.year;

      const profileRes = await fetch(`${window.API_BASE}/api/profile`, {
        method: "PUT",
        headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token },
        body: JSON.stringify(body),
      });
      const profileData = await profileRes.json();
      if (profileRes.ok && setCurrentUser) {
        setCurrentUser({
          ...currentUser,
          completedCourses: profileData.completed_courses || merged,
          major: profileData.major || currentUser.major,
          year: profileData.year || currentUser.year,
        });
      }

      const parts = [];
      if (newlyAdded.length) parts.push(`added **${newlyAdded.length}** newly completed course${newlyAdded.length === 1 ? "" : "s"}`);
      if (body.major) parts.push(`set your major to **${data.major}**`);
      if (body.year) parts.push(`set your year to **${data.year}**`);
      const summary = parts.length
        ? `📎 Read **${file.name}** and ${parts.join(", ")}.`
        : (merged.length
          ? `📎 Read **${file.name}** — your profile already matches, nothing new to add.`
          : `📎 Read **${file.name}** but couldn't find any recognizable course history in it. Try attaching your official DegreeWorks audit PDF.`);

      await postAssistantNotice(summary);
    } catch (err) {
      setMessages(m => [...m, {
        id: Date.now(), role: "assistant", intro: null, tool: null,
        body: ["I couldn't read that file: " + (err.message || "unknown error") + ". Try attaching a text-based PDF export of your DegreeWorks audit."],
        pills: [],
      }]);
    } finally {
      setAttaching(false);
    }
  };

  const stopVoice = () => {
    if (recognitionRef.current) recognitionRef.current.stop();
    setListening(false);
  };

  const startVoice = () => {
    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SpeechRecognition) {
      setMessages(m => [...m, {
        id: Date.now(), role: "assistant", intro: null, tool: null,
        body: ["Voice input isn't supported in this browser — try Chrome or Edge, or just type your question."],
        pills: [],
      }]);
      return;
    }

    const recognition = new SpeechRecognition();
    recognition.lang = "en-US";
    recognition.interimResults = true;
    recognition.continuous = true;

    let finalTranscript = draft ? draft + " " : "";
    recognition.onresult = (e) => {
      let interim = "";
      for (let i = e.resultIndex; i < e.results.length; i++) {
        const t = e.results[i][0].transcript;
        if (e.results[i].isFinal) finalTranscript += t + " ";
        else interim += t;
      }
      const next = (finalTranscript + interim).trim();
      setDraft(next);
      autoGrow(taRef.current);
    };
    recognition.onerror = () => setListening(false);
    recognition.onend = () => setListening(false);

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

  const toggleVoice = () => {
    if (listening) stopVoice();
    else startVoice();
  };

  React.useEffect(() => () => { if (recognitionRef.current) recognitionRef.current.stop(); }, []);

  const genericReply = (q) => ({
    intro: null,
    tool: null,
    body: [
      "Here's what I can tell from your transcript and the current catalog:",
      `You asked: *${q}* — I'll pull the most relevant courses and degree requirements and draft a recommendation. In a real run, this is where I'd call \`search_courses\` and \`get_requirements\` against the RAG store.`,
      "Want me to draft a specific semester plan, or check a prerequisite chain?",
    ],
    pills: [],
  });

  const composerEl = (
    <div className="composer">
      <textarea
        ref={taRef}
        rows={1}
        placeholder={started ? "Reply to your advisor…" : "Ask about courses, prerequisites, graduation plans…"}
        value={draft}
        onChange={(e) => { setDraft(e.target.value); autoGrow(e.target); }}
        onKeyDown={(e) => {
          if (e.key === "Enter" && !e.shiftKey) {
            e.preventDefault();
            send();
          }
        }}
      />
      <input
        ref={fileInputRef}
        type="file"
        accept="application/pdf"
        style={{ display: "none" }}
        onChange={handleFileChange}
      />
      <div className="composer-tools">
        <button
          className="tool-btn"
          title="Attach transcript"
          onClick={handleAttachClick}
          disabled={attaching}
        >
          {attaching ? <span className="tool-spinner" /> : <I.paperclip />}
        </button>
        <button
          className={`tool-btn${listening ? " listening" : ""}`}
          title={listening ? "Stop listening" : "Voice"}
          onClick={toggleVoice}
        >
          <I.mic />
        </button>
      </div>
      <button className="send" onClick={() => send()} disabled={!draft.trim() || streaming}>
        <I.send />
      </button>
    </div>
  );

  return (
    <div className="chat">
      <div className="topbar">
        <h1>Conversation with <em>your advisor</em></h1>
        <span className="meta">Session · {currentUser.name.split(" ")[0]} · {getCurrentTermLabel()}</span>
        <div className="topbar-right">
          <button className="icon-btn" onClick={togglePanel} title="Toggle context panel"><I.panel /></button>
        </div>
      </div>

      <div className="chat-scroll" ref={scrollRef}>
        {!started && <Greeting onPick={send} currentUser={currentUser} composer={composerEl} />}

        {started && (
          <div className="thread">
            {messages.map((m, i) => (
              <Message
                key={m.id}
                m={m}
                currentUser={currentUser}
                isTalking={streaming && i === messages.length - 1 && m.role === "assistant"}
              />
            ))}
            {streaming && messages[messages.length - 1]?.role !== "assistant" && (
              <div className="msg assistant">
                <TalkingAvatar talking={true} />
                <div className="msg-body">
                  <div className="msg-name">Advisor <span>thinking…</span></div>
                  <div className="msg-content">
                    <div className="streaming"><i/><i/><i/></div>
                  </div>
                </div>
              </div>
            )}
          </div>
        )}
      </div>

      {started && (
        <div className="composer-wrap">
          {composerEl}
          <div className="composer-footnote">
            Advisor can make mistakes · verify course details in Self-Service Carolina before registering
          </div>
        </div>
      )}
    </div>
  );
}

// UofSC runs Spring (Jan–Apr/May), Summer (May–Aug), Fall (Aug–Dec) terms.
function getCurrentTermLabel(date = new Date()) {
  const month = date.getMonth() + 1; // 1-12
  const yy = String(date.getFullYear()).slice(-2);
  if (month <= 4) return `Spring '${yy}`;
  if (month <= 7) return `Summer '${yy}`;
  return `Fall '${yy}`;
}

function greetingSubtitle(currentUser) {
  const hasTranscript = (currentUser.completedCourses || []).length > 0;
  const hasMajor = currentUser.major && currentUser.major !== "Not set";
  const term = getCurrentTermLabel();

  if (hasTranscript && hasMajor) {
    return `I have your completed courses, the ${term} catalog, and your declared major loaded. Ask anything about courses, prerequisites, or pacing toward graduation.`;
  }
  if (hasTranscript && !hasMajor) {
    return `I have your completed courses and the ${term} catalog loaded, but no declared major yet — set one in your profile so I can check specific degree requirements.`;
  }
  if (!hasTranscript && hasMajor) {
    return `I have the ${term} catalog and your declared major loaded, but no completed courses yet — attach your transcript or add them in your profile so I can track your progress toward graduation.`;
  }
  return `I have the ${term} catalog loaded, but no major or completed courses yet — add them in your profile (or attach your transcript here) so I can personalize your plan.`;
}

function Greeting({ onPick, currentUser, composer }) {
  const hour = new Date().getHours();
  const part = hour < 12 ? "morning" : hour < 18 ? "afternoon" : "evening";
  return (
    <div className="greeting-wrap">
      <div className="greet">
        <div className="kicker">◆ Session begins</div>
        <h2>Good {part}, {currentUser.name.split(" ")[0]}. <em>What are we figuring out today?</em></h2>
        <p>{greetingSubtitle(currentUser)}</p>
      </div>
      <div className="greet-composer">
        {composer}
        <div className="composer-footnote">
          Advisor can make mistakes · verify course details in Self-Service Carolina before registering
        </div>
      </div>
      <div className="suggest-grid">
        {getSuggestions(currentUser).map((s, i) => (
          <button key={i} className="suggest" onClick={() => onPick(s.q)}>
            <div className="sug-label">{s.label}</div>
            <div className="sug-q">{s.q}</div>
          </button>
        ))}
      </div>
    </div>
  );
}

function Message({ m, currentUser, isTalking }) {
  if (m.role === "user") {
    return (
      <div className="msg user">
        <div className="bubble-avatar">{currentUser.initials}</div>
        <div className="msg-body">
          <div className="msg-name">{currentUser.name} <span>just now</span></div>
          <div className="msg-content">{m.content}</div>
        </div>
      </div>
    );
  }
  return (
    <div className="msg assistant">
      <TalkingAvatar talking={!!isTalking} />
      <div className="msg-body">
        <div className="msg-name">Advisor <span>just now</span></div>
        <div className="msg-content">
          {m.intro && <p>{m.intro}</p>}
          {m.tool && <ToolCard tool={m.tool} />}
          {m.body && <FormattedP text={m.body.join("\n")} />}
          {m.pills && m.pills.length > 0 && (
            <div className="tool-card" style={{marginTop: 14}}>
              <div className="tool-head done">
                <span className="pulse" />
                <span>referenced courses</span>
              </div>
              <div className="course-pill-row">
                {m.pills.map(p => (
                  <span key={p.code} className="course-pill">
                    <b>{p.code}</b> · {p.title}
                  </span>
                ))}
              </div>
            </div>
          )}
        </div>
        <div className="msg-actions">
          <button className="msg-action" title="Copy"><I.copy /></button>
          <button className="msg-action" title="Helpful"><I.thumb /></button>
          <button className="msg-action" title="Regenerate"><I.refresh /></button>
        </div>
      </div>
    </div>
  );
}

function FormattedP({ text }) {
  const lines = text.split("\n");
  const elements = [];
  let i = 0;

  while (i < lines.length) {
    const line = lines[i];

    // blank line
    if (!line.trim()) { i++; continue; }

    // heading: ### or ## or #
    const hMatch = line.match(/^(#{1,3})\s+(.+)/);
    if (hMatch) {
      const level = hMatch[1].length;
      const Tag = `h${level + 2}`; // h3, h4, h5
      const size = level === 1 ? "21px" : level === 2 ? "18px" : "15.5px";
      elements.push(<Tag key={i} style={{fontWeight:700, marginTop: i===0?0:20, marginBottom:8, lineHeight:1.3, fontSize:size, color:"var(--ink)"}}>{renderInline(hMatch[2])}</Tag>);
      i++; continue;
    }

    // table: header row, separator row ("|---|---|"), then data rows — all pipe-delimited
    if (/^\|.+\|$/.test(line.trim()) && i + 1 < lines.length && /^\|?[\s:|-]+\|?$/.test(lines[i + 1].trim()) && lines[i + 1].includes("-")) {
      const parseRow = (rowLine) => rowLine.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map(c => c.trim());
      const headerCells = parseRow(line);
      i += 2;
      const rows = [];
      while (i < lines.length && /^\|.+\|$/.test(lines[i].trim())) {
        rows.push(parseRow(lines[i]));
        i++;
      }
      elements.push(
        <div key={"tbl" + i} style={{ overflowX: "auto", margin: "12px 0" }}>
          <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13.5 }}>
            <thead>
              <tr>
                {headerCells.map((h, hi) => (
                  <th key={hi} style={{ textAlign: "left", fontWeight: 700, padding: "7px 12px", borderBottom: "2px solid var(--line)", whiteSpace: "nowrap" }}>{renderInline(h)}</th>
                ))}
              </tr>
            </thead>
            <tbody>
              {rows.map((r, ri) => (
                <tr key={ri}>
                  {r.map((c, ci) => (
                    <td key={ci} style={{ padding: "7px 12px", borderBottom: "1px solid var(--line)" }}>{renderInline(c)}</td>
                  ))}
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      );
      continue;
    }

    // bullet list: collect consecutive "- " or "* " lines
    if (/^[-*]\s/.test(line)) {
      const items = [];
      let isResourceList = false;
      while (i < lines.length && /^[-*]\s/.test(lines[i])) {
        const content = lines[i].replace(/^[-*]\s/, "");
        // check if this bullet is purely a markdown link [text](url)
        const linkOnly = content.match(/^\[(.+?)\]\((https?:\/\/[^\s)]+)\)(.*)$/);
        if (linkOnly) {
          isResourceList = true;
          items.push(<LinkCard key={i} text={linkOnly[1]} url={linkOnly[2]} />);
        } else {
          items.push(<li key={i} style={{marginBottom:3}}>{renderInline(content)}</li>);
        }
        i++;
      }
      if (isResourceList) {
        elements.push(<div key={"rc"+i} style={{display:"flex",flexDirection:"column",gap:4,margin:"8px 0"}}>{items}</div>);
      } else {
        elements.push(<ul key={"ul"+i} style={{paddingLeft:20, margin:"8px 0"}}>{items}</ul>);
      }
      continue;
    }

    // numbered list: collect consecutive "1. " lines
    if (/^\d+\.\s/.test(line)) {
      const items = [];
      while (i < lines.length && /^\d+\.\s/.test(lines[i])) {
        items.push(<li key={i} style={{marginBottom:4}}>{renderInline(lines[i].replace(/^\d+\.\s/, ""))}</li>);
        i++;
      }
      elements.push(<ol key={"ol"+i} style={{paddingLeft:20, margin:"8px 0"}}>{items}</ol>);
      continue;
    }

    // horizontal rule
    if (/^---+$/.test(line.trim())) {
      elements.push(<hr key={i} style={{border:"none",borderTop:"1px solid var(--line)",margin:"12px 0"}} />);
      i++; continue;
    }

    // plain paragraph
    elements.push(<p key={i} style={{marginBottom:8, lineHeight:1.6}}>{renderInline(line)}</p>);
    i++;
  }

  return <div>{elements}</div>;
}

const LINK_META = {
  "academicbulletins.sc.edu":    { img: "https://www.sc.edu/favicon.ico", desc: "Browse all UofSC courses and degree requirements" },
  "selfservice.sc.edu":          { img: "https://www.sc.edu/favicon.ico", desc: "Register for classes, view your schedule" },
  "degreeworks.sc.edu":          { img: "https://www.sc.edu/favicon.ico", desc: "Check your degree progress and audit" },
  "sc.edu/about/offices_and_divisions/registrar/transcripts": { img: "https://www.sc.edu/favicon.ico", desc: "Transfer credit policies and procedures" },
  "ap_ib_credits":               { img: "https://www.sc.edu/favicon.ico", desc: "AP and IB credit equivalency table" },
  "financial_aid":               { img: "https://www.sc.edu/favicon.ico", desc: "Financial aid, scholarships, and FAFSA" },
  "registrar":                   { img: "https://www.sc.edu/favicon.ico", desc: "Registrar office — transcripts, enrollment verification" },
  "academic_calendars":          { img: "https://www.sc.edu/favicon.ico", desc: "Add/drop deadlines, exam schedules, holidays" },
  "palmetto_college":            { img: "https://www.sc.edu/favicon.ico", desc: "Transfer from a SC community college to UofSC" },
};

function getLinkMeta(url) {
  for (const [key, val] of Object.entries(LINK_META)) {
    if (url.includes(key)) return val;
  }
  try {
    const domain = new URL(url).hostname;
    return { img: `https://www.google.com/s2/favicons?domain=${domain}&sz=32`, desc: domain };
  } catch { return null; }
}

function LinkCard({ text, url }) {
  const [preview, setPreview] = React.useState(null);
  const fallback = getLinkMeta(url);

  React.useEffect(() => {
    fetch(`${window.API_BASE}/api/link-preview?url=${encodeURIComponent(url)}`)
      .then(r => r.json())
      .then(d => setPreview(d))
      .catch(() => {});
  }, [url]);

  const image = preview?.image;
  const desc = preview?.description || fallback?.desc;

  return (
    <a href={url} target="_blank" rel="noopener noreferrer"
      onClick={(e) => { e.stopPropagation(); window.open(url, "_blank"); }}
      style={{
        display:"flex", flexDirection:"column",
        marginTop:6, borderRadius:12, overflow:"hidden",
        border:"1px solid var(--line)", textDecoration:"none", color:"inherit",
        background:"var(--card)", cursor:"pointer",
        transition:"border-color 0.15s",
      }}
      onMouseEnter={e => e.currentTarget.style.borderColor="var(--accent)"}
      onMouseLeave={e => e.currentTarget.style.borderColor="var(--line)"}
    >
      {image && (
        <img src={image} alt="" style={{width:"100%", height:140, objectFit:"cover", display:"block"}}
          onError={e => e.target.style.display="none"} />
      )}
      <div style={{display:"flex", alignItems:"center", gap:10, padding:"10px 14px"}}>
        <img src={`https://www.google.com/s2/favicons?domain=${new URL(url).hostname}&sz=32`}
          alt="" style={{width:18, height:18, borderRadius:3, flexShrink:0}}
          onError={e=>e.target.style.display="none"} />
        <div style={{minWidth:0}}>
          <div style={{fontWeight:600, fontSize:13, color:"var(--ink)", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis"}}>{text}</div>
          {desc && <div style={{fontSize:11.5, color:"var(--ink-3)", marginTop:1, whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis"}}>{desc}</div>}
        </div>
        <svg style={{marginLeft:"auto", flexShrink:0, opacity:0.4}} width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
      </div>
    </a>
  );
}

function renderInline(line) {
  // bold: **x**, italic: *x*, code: `x`, link: [text](url)
  const nodes = [];
  const re = /\[(.+?)\]\((https?:\/\/[^\s)]+)\)|\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`/g;
  let last = 0, m, i = 0;
  while ((m = re.exec(line)) !== null) {
    if (m.index > last) nodes.push(line.slice(last, m.index));
    if (m[1] !== undefined) nodes.push(<a key={i++} href={m[2]} target="_blank" rel="noopener noreferrer" style={{color:"var(--accent)",textDecoration:"underline",cursor:"pointer"}} onClick={(e)=>{e.stopPropagation();window.open(m[2],"_blank");}}>{m[1]}</a>);
    else if (m[3] !== undefined) nodes.push(<strong key={i++}>{m[3]}</strong>);
    else if (m[4] !== undefined) nodes.push(<em key={i++}>{m[4]}</em>);
    else if (m[5] !== undefined) nodes.push(<code key={i++}>{m[5]}</code>);
    last = re.lastIndex;
  }
  if (last < line.length) nodes.push(line.slice(last));
  return nodes;
}

function ToolCard({ tool }) {
  return (
    <div className="tool-card">
      <div className="tool-head done">
        <span className="pulse" />
        <span>tool · {tool.name}({Object.entries(tool.args).map(([k,v]) => `${k}=${JSON.stringify(v)}`).join(", ")})</span>
      </div>
      <div className="course-pill-row">
        {tool.result.map((r, i) => (
          <span key={i} className="course-pill">{r}</span>
        ))}
      </div>
    </div>
  );
}

Object.assign(window, { ChatView, Greeting, Message, ToolCard });
