// app/chat.jsx — Ribbon 챗봇: 대화 오케스트레이션 훅 + 채팅 UI
// window.RibbonChat 로 노출.

const { useState: useStateC, useEffect: useEffectC, useRef: useRefC } = React;

// ── 인라인 아이콘 (Lucide 스타일, 2px 라운드 스트로크) ──────
const I = {
  send: 'M5 12h14M13 6l6 6-6 6',
  arrowUp: 'M12 19V5M6 11l6-6 6 6',
  copy: 'M9 9h10v10H9zM5 15V5h10',
  refresh: 'M21 12a9 9 0 1 1-3-6.7M21 4v4h-4',
  up: 'M7 11v9M14 4l-2 7h7a2 2 0 0 1 2 2.3l-1 5A2 2 0 0 1 18 20H7',
  down: 'M17 13V4M10 20l2-7H5a2 2 0 0 1-2-2.3l1-5A2 2 0 0 1 7 4h9',
  download: 'M12 3v12M7 11l5 5 5-5M5 21h14',
  mic: 'M12 3a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V6a3 3 0 0 0-3-3zM5 11a7 7 0 0 0 14 0M12 18v3',
  clip: 'M21 11l-8.5 8.5a5 5 0 0 1-7-7L14 4a3.5 3.5 0 0 1 5 5l-8.5 8.5a2 2 0 0 1-3-3L15 6',
  expand: 'M4 14v6h6M20 10V4h-6M14 4h6v6M10 20H4v-6',
  collapse: 'M14 10V4h6M4 14h6v6M10 4v6H4M20 14h-6v6',
  plus: 'M12 5v14M5 12h14',
  x: 'M6 6l12 12M18 6L6 18',
  db: 'M12 3c4.4 0 8 1.3 8 3s-3.6 3-8 3-8-1.3-8-3 3.6-3 8-3zM4 6v6c0 1.7 3.6 3 8 3s8-1.3 8-3V6M4 12v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6',
  spark: 'M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8z',
  history: 'M3 12a9 9 0 1 0 9-9 9 9 0 0 0-7 3.3M3 4v4h4M12 8v4l3 2',
  flag: 'M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1zM4 22v-7'
};
function Icon({ d, size = 16, color = 'currentColor', sw = 2, style }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color}
    strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round" style={style}>
      {String(d).split('M').filter(Boolean).map((seg, i) => <path key={i} d={'M' + seg} />)}
    </svg>);

}

// 코랄 스파이크 마크 (브랜드 글리프 homage)
function Spike({ size = 22, color = 'var(--accent)' }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill={color}>
      <path d="M12 2l1.3 7.2L20 6.5l-4.5 5.5L22 14l-7-0.5L12 22l-3-8.5L2 14l6.5-2L4 6.5l6.7 2.7z" opacity="0.92" />
    </svg>);

}

// ── 백엔드 연결(실 에이전트) ────────────────────────────────
// serve.ts 가 서빙하면 /api/chat 로 실 LLM+MCP 응답. 정적(file://)으로 열면 실패→mock 폴백.
const API_BASE = '';
// 스트리밍(NDJSON) 소비: 툴 호출마다 onTool(ev) 로 진행상태를 실시간 갱신하고, 마지막 done 이벤트를 반환.
async function askBackendStream(message, history, onTool) {
  // 현재 선택 업체(tenant_id)를 함께 보낸다 — 백엔드가 업체별 지역·LLM(관리자 설정)을 적용한다.
  const tenantId = (window.RibbonData && window.RibbonData.currentTenantId) || 'thesafe';
  const res = await fetch(API_BASE + '/api/chat', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ message, history: history || [], tenant_id: tenantId }),
  });
  if (!res.ok || !res.body) throw new Error('api ' + res.status);
  const reader = res.body.getReader();
  const dec = new TextDecoder();
  let buf = '';
  let done = null;
  for (;;) {
    const { value, done: rdone } = await reader.read();
    if (rdone) break;
    buf += dec.decode(value, { stream: true });
    let idx;
    while ((idx = buf.indexOf('\n')) >= 0) {
      const line = buf.slice(0, idx).trim();
      buf = buf.slice(idx + 1);
      if (!line) continue;
      let ev; try { ev = JSON.parse(line); } catch (e) { continue; }
      if (ev.type === 'tool') { onTool && onTool(ev); }
      else if (ev.type === 'done') { done = ev; }
    }
  }
  if (!done) throw new Error('no done event');
  return done; // { text, provider, model, stub, llm_turns, tools, charts }
}

// ── 대화 세션 저장소 (localStorage — 새 대화/닫기 경계로 세션 관리) ──────
const SESSION_KEY = 'ribbon.sessions.v1';
function loadSessions() { try { const a = JSON.parse(localStorage.getItem(SESSION_KEY) || '[]'); return Array.isArray(a) ? a : []; } catch { return []; } }
function persistSessions(list) { try { localStorage.setItem(SESSION_KEY, JSON.stringify(list.slice(0, 50))); } catch (e) {} }
function newSessionId() { return 's' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); }
function relTime(ts) {
  const s = Math.floor((Date.now() - (ts || 0)) / 1000);
  if (s < 60) return '방금'; if (s < 3600) return Math.floor(s / 60) + '분 전';
  if (s < 86400) return Math.floor(s / 3600) + '시간 전'; return Math.floor(s / 86400) + '일 전';
}
function assistantText(m) {
  if (m.text) return m.text;
  const blocks = (m.scenario && m.scenario.answer) || [];
  return blocks.filter((b) => b.type === 'markdown' || b.type === 'text').map((b) => b.md).join('\n');
}
function firstUserText(messages) { const u = messages.find((m) => m.role === 'user'); return u ? u.text : ''; }
// LLM 에 넘길 이전 대화(맥락) — user/assistant 최종 텍스트만.
function historyFrom(messages) {
  return messages
    .map((m) => (m.role === 'user' ? { role: 'user', content: (m.text || '').trim() } : { role: 'assistant', content: assistantText(m).trim() }))
    .filter((x) => x.content);
}

// 실 응답 → 기존 렌더러가 소비하는 scenario 형태로 변환.
// 텍스트(마크다운) 뒤에 백엔드가 실데이터로 만든 차트 블록(kpi/line/bar/table…)을 이어 붙인다.
function backendToScenario(data) {
  const toolNames = [...new Set((data.tools || []).map((t) => t.name).filter(Boolean))];
  const charts = Array.isArray(data.charts) ? data.charts : [];
  const answer = [{ type: 'markdown', md: data.text || '(빈 응답)' }, ...charts];
  return {
    real: true,
    answer,
    sources: toolNames.length ? toolNames : undefined,
    followups: window.RibbonData.STARTER_CHIPS.slice(0, 3),
    meta: { provider: data.provider, model: data.model, stub: data.stub, llm_turns: data.llm_turns },
  };
}

// ── 대화 오케스트레이션 훅 ──────────────────────────────────
function useConversation() {
  const [messages, setMessages] = useStateC([]);
  const [busy, setBusy] = useStateC(false);
  const [status, setStatus] = useStateC(null); // {steps, idx, label, source, external}
  const [spotlight, setSpotlight] = useStateC(null);
  const [reportMsg, setReportMsg] = useStateC(null); // 문제제기 모달 대상 메시지(null=닫힘)
  const [sessions, setSessions] = useStateC(loadSessions);
  const [activeId, setActiveId] = useStateC(newSessionId());
  const sid = useRefC(activeId); // 저장/삭제 로직이 참조하는 canonical id
  const timers = useRefC([]);

  function clearTimers() {timers.current.forEach(clearTimeout);timers.current = [];}
  useEffectC(() => clearTimers, []);

  // 자동 저장: 메시지가 바뀔 때마다 현재 세션을 '최근 대화'에 upsert(비어있으면 스킵).
  useEffectC(() => {
    if (messages.length === 0) return;
    const sess = { id: sid.current, title: firstUserText(messages) || '새 대화', ts: Date.now(), messages };
    setSessions((prev) => {
      const next = [sess, ...prev.filter((s) => s.id !== sess.id)].slice(0, 50);
      persistSessions(next);
      return next;
    });
  }, [messages]);

  // 실 백엔드 경로(기본, 스트리밍). 툴 호출마다 상태창을 실시간 갱신. 실패 시 mock 폴백.
  async function runReal(text, history) {
    setBusy(true);
    setSpotlight(null);
    setStatus({ steps: [{ label: '질문 이해 중…', source: '알파카', external: false }], idx: 0 });
    try {
      const steps = []; // 툴 호출을 누적 → 진행바가 단계별로 전진.
      const data = await askBackendStream(text, history, (ev) => {
        steps.push({ label: ev.label || (ev.name + ' 호출 중'), source: ev.server ? ev.server + ' MCP' : 'MCP', external: true });
        setStatus({ steps: steps.slice(), idx: steps.length - 1 });
      });
      const scenario = backendToScenario(data);
      setStatus(null);
      setBusy(false);
      setMessages((m) => [...m, { id: 'a' + Date.now(), role: 'assistant', scenario, text: data.text, feedback: null }]);
    } catch (e) {
      setStatus(null);
      setBusy(false);
      const scenario = window.RibbonData.matchScenario(text);
      const txt = (scenario.answer || []).filter((b) => b.type === 'text' || b.type === 'markdown').map((b) => b.md).join('\n');
      setMessages((m) => [...m, { id: 'a' + Date.now(), role: 'assistant', scenario, text: txt, feedback: null, fallback: true }]);
    }
  }

  function send(text) {
    const v = (text || '').trim();
    if (!v || busy) return;
    const hist = historyFrom(messages); // 현재 메시지 추가 전의 이전 턴 = 맥락
    setMessages((m) => [...m, { id: 'u' + Date.now(), role: 'user', text: v }]);
    runReal(v, hist);
  }

  function regenerate() {
    if (busy) return;
    let lui = -1;
    for (let i = messages.length - 1; i >= 0; i--) if (messages[i].role === 'user') { lui = i; break; }
    if (lui < 0) return;
    const lastUser = messages[lui];
    const hist = historyFrom(messages.slice(0, lui));
    setMessages((m) => {
      const copy = [...m];
      if (copy[copy.length - 1]?.role === 'assistant') copy.pop();
      return copy;
    });
    runReal(lastUser.text, hist);
  }

  function setFeedback(id, val) {
    setMessages((m) => m.map((x) => x.id === id ? { ...x, feedback: x.feedback === val ? null : val } : x));
  }

  // 새 대화: 현재 세션은 자동저장돼 있으니 새 id 로 비운다.
  function reset() {
    clearTimers();
    const nid = newSessionId();
    sid.current = nid; setActiveId(nid);
    setMessages([]); setBusy(false); setStatus(null); setSpotlight(null);
  }

  // 저장 세션 열기 — 메시지 복원 + 그 세션으로 이어서 대화(맥락 포함).
  function loadSession(sess) {
    if (!sess) return;
    clearTimers();
    sid.current = sess.id; setActiveId(sess.id);
    setMessages(Array.isArray(sess.messages) ? sess.messages : []);
    setBusy(false); setStatus(null); setSpotlight(null);
  }

  // 저장 세션 삭제. 현재 열려있는 세션이면 새 대화로 전환.
  function deleteSession(id) {
    setSessions((prev) => {
      const next = prev.filter((s) => s.id !== id);
      persistSessions(next);
      return next;
    });
    if (id === sid.current) reset();
  }

  return {
    messages, busy, status, spotlight, sessions, activeId,
    send, regenerate, setFeedback, reset, loadSession, deleteSession, setSpotlight,
    // 문제제기(피드백) 모달 — 대상 메시지를 넣으면 열리고 null 로 닫는다.
    reportMsg, openReport: setReportMsg, closeReport: () => setReportMsg(null),
  };
}

// ── MCP 미니 상태 표시줄 ────────────────────────────────────
function McpStatusBar({ status }) {
  if (!status) return null;
  const step = status.steps[status.idx];
  // 스트리밍이라 총 단계 수를 미리 알 수 없다 → "N/전체" 대신 진행한 단계 수만(카운트 업),
  // 진행바는 불확정(인디터미네이트) 애니메이션으로 표시(가짜 100% 방지). 사용자 지적 2026-07-05.
  const color = step.external ? 'var(--color-teal)' : 'var(--accent)';
  return (
    <div style={{ borderBottom: '1px solid var(--border-hairline)', background: 'var(--surface-soft)', padding: '8px 16px' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 9 }}>
        <span className="rb-spin" style={{ width: 13, height: 13, borderRadius: '50%', flexShrink: 0,
          border: '2px solid var(--border-hairline)', borderTopColor: color }} />
        <span style={{ fontSize: 12.5, fontWeight: 600, color: 'var(--text-strong)' }}>{step.label}</span>
        {step.external &&
        <span style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.6px', color: 'var(--color-teal)',
          background: 'rgba(93,184,114,0.14)', padding: '2px 7px', borderRadius: 'var(--radius-pill)', textTransform: 'uppercase' }}>MCP</span>
        }
        <span style={{ marginLeft: 'auto', fontSize: 11, color: 'var(--text-faint)', fontFamily: 'var(--font-mono)' }}>{step.source}</span>
        <span style={{ fontSize: 11, color: 'var(--text-muted)', fontWeight: 600 }}>{status.idx + 1}단계째</span>
      </div>
      <div style={{ position: 'relative', height: 2, background: 'var(--border-hairline)', borderRadius: 2, marginTop: 8, overflow: 'hidden' }}>
        <div className="rb-indet" style={{ background: color }} />
      </div>
    </div>);

}

// ── 사용자 / 어시스턴트 메시지 ──────────────────────────────
function UserMsg({ text }) {
  return (
    <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
      <div style={{ maxWidth: '82%', background: 'var(--surface-card-strong)', color: 'var(--text-heading)',
        borderRadius: '14px 14px 4px 14px', padding: '10px 14px', fontFamily: 'var(--font-sans)', fontSize: 14.5, lineHeight: 1.5 }}>{text}</div>
    </div>);

}

function ActionBtn({ icon, label, active, activeColor, onClick }) {
  const [hov, setHov] = useStateC(false);
  return (
    <button onClick={onClick} onMouseEnter={() => setHov(true)} onMouseLeave={() => setHov(false)} title={label}
    style={{ display: 'inline-flex', alignItems: 'center', gap: 5, height: 28, padding: '0 9px', borderRadius: 'var(--radius-sm)',
      border: 'none', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontSize: 12, fontWeight: 500,
      background: hov ? 'var(--surface-card)' : 'transparent',
      color: active ? activeColor || 'var(--accent)' : 'var(--text-muted)', transition: 'background .12s, color .12s' }}>
      <Icon d={icon} size={14} />{label && <span>{label}</span>}
    </button>);

}

function Sources({ items }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 7, flexWrap: 'wrap', marginTop: 4 }}>
      <Icon d={I.db} size={12} color="var(--text-faint)" />
      <span style={{ fontSize: 11.5, color: 'var(--text-faint)', fontWeight: 500 }}>출처</span>
      {items.map((s, i) =>
      <span key={i} style={{ fontSize: 11.5, fontWeight: 500, color: 'var(--text-muted)',
        background: 'var(--surface-soft)', border: '1px solid var(--border-hairline)', padding: '2px 9px', borderRadius: 'var(--radius-pill)' }}>{s}</span>
      )}
    </div>);

}

function AssistantMsg({ msg, conv, onChip, opts = {} }) {
  const { Block } = window.RibbonCharts;
  const sc = msg.scenario;
  const [toast, setToast] = useStateC(null);
  function copy() {
    const txt = sc.answer.filter((b) => b.type === 'text' || b.type === 'markdown').map((b) => b.md.replace(/\*\*/g, '')).join('\n\n');
    navigator.clipboard && navigator.clipboard.writeText(txt);
    setToast('복사했어요');setTimeout(() => setToast(null), 1400);
  }
  function exportChart() {setToast('차트를 PNG로 내보냈어요');setTimeout(() => setToast(null), 1600);}
  return (
    <div style={{ display: 'flex', gap: 11 }}>
      <div style={{ flexShrink: 0, width: 30, height: 30, borderRadius: 'var(--radius-full)', background: 'var(--surface-card)',
        display: 'flex', alignItems: 'center', justifyContent: 'center', marginTop: 2 }}><Spike size={17} /></div>
      <div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 14 }}>
        {sc.answer.map((b, i) => <Block key={i} block={b} />)}
        {sc.sources && opts.showSources !== false && <Sources items={sc.sources} />}
        {/* 메시지 액션 */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap', marginTop: -2 }}>
          <ActionBtn icon={I.copy} label="복사" onClick={copy} />
          <ActionBtn icon={I.refresh} label="재생성" onClick={() => conv.regenerate()} />
          <ActionBtn icon={I.download} label="내보내기" onClick={exportChart} />
          <div style={{ width: 1, height: 16, background: 'var(--border-hairline)', margin: '0 4px' }} />
          <ActionBtn icon={I.up} active={msg.feedback === 'up'} activeColor="var(--color-success)" onClick={() => conv.setFeedback(msg.id, 'up')} />
          <ActionBtn icon={I.down} active={msg.feedback === 'down'} activeColor="var(--color-error)" onClick={() => conv.setFeedback(msg.id, 'down')} />
          <ActionBtn icon={I.flag} label="문제제기" onClick={() => conv.openReport(msg)} />
          {toast && <span style={{ marginLeft: 8, fontSize: 11.5, color: 'var(--color-success)', fontWeight: 600 }}>{toast}</span>}
        </div>
        {/* 후속 질문 칩 */}
        {sc.followups && sc.followups.length > 0 &&
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 7 }}>
            {sc.followups.map((f, i) =>
          <button key={i} onClick={() => onChip(f)} className="rb-chip"
          style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '7px 12px', borderRadius: 'var(--radius-pill)',
            border: '1px solid var(--border-hairline)', background: 'var(--surface-page)', cursor: 'pointer',
            fontFamily: 'var(--font-sans)', fontSize: 12.5, fontWeight: 500, color: 'var(--text-body)' }}>
                <Icon d={I.spark} size={12} color="var(--accent)" />{f}</button>
          )}
          </div>
        }
      </div>
    </div>);

}

// ── 시작 화면 (인사 + 추천 질문) ────────────────────────────
function Welcome({ onChip }) {
  const chips = window.RibbonData.STARTER_CHIPS;
  return (
    <div style={{ padding: '8px 4px 4px' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
        <div style={{ width: 40, height: 40, borderRadius: 'var(--radius-full)', background: 'var(--surface-card)',
          display: 'flex', alignItems: 'center', justifyContent: 'center' }}><Spike size={24} /></div>
      </div>
      <h2 style={{ fontFamily: 'var(--font-display)', fontSize: 27, letterSpacing: '-0.5px', color: 'var(--text-heading)', margin: '0 0 6px', fontWeight: 400 }}>
        무엇을 도와드릴까요?</h2>
      <p style={{ fontFamily: 'var(--font-sans)', fontSize: 14, color: 'var(--text-muted)', margin: '0 0 22px', lineHeight: 1.55 }}>
        리본 ERP의 인벤토리·예약·매출 데이터를 분석해 차트와 표로 답해 드려요. 맥락은 대화 내내 이어집니다.</p>
      <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-faint)', marginBottom: 10, letterSpacing: '0.3px' }}>추천 질문</div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
        {chips.map((c, i) =>
        <button key={i} onClick={() => onChip(c)} className="rb-chip rb-starter"
        style={{ textAlign: 'left', display: 'flex', alignItems: 'center', gap: 9, padding: '12px 13px', borderRadius: 'var(--radius-md)',
          border: '1px solid var(--border-hairline)', background: 'var(--surface-page)', cursor: 'pointer',
          fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 500, color: 'var(--text-body)', lineHeight: 1.4 }}>
            <Icon d={I.spark} size={14} color="var(--accent)" style={{ flexShrink: 0 }} />{c}</button>
        )}
      </div>
    </div>);

}

// ── 입력 바 (음성 / 첨부 / 전송) ────────────────────────────
function ChatInput({ onSend, busy }) {
  const [val, setVal] = useStateC('');
  const [listening, setListening] = useStateC(false);
  const [attached, setAttached] = useStateC(false);
  const ref = useRefC(null);
  function submit() {if (!val.trim() || busy) return;onSend(val);setVal('');setAttached(false);if (ref.current) ref.current.style.height = 'auto';}
  function key(e) {if (e.key === 'Enter' && !e.shiftKey) {e.preventDefault();submit();}}
  function grow(e) {e.target.style.height = 'auto';e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px';setVal(e.target.value);}
  return (
    <div style={{ padding: '12px 16px 14px', borderTop: '1px solid var(--border-hairline)', background: 'var(--surface-page)' }}>
      {attached &&
      <div style={{ display: 'inline-flex', alignItems: 'center', gap: 7, marginBottom: 8, padding: '5px 10px', borderRadius: 'var(--radius-sm)',
        background: 'var(--surface-soft)', border: '1px solid var(--border-hairline)', fontSize: 12, color: 'var(--text-body)' }}>
          <Icon d={I.clip} size={13} color="var(--text-muted)" />6월_가동률_리포트.xlsx
          <button onClick={() => setAttached(false)} style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 0, marginLeft: 2 }}><Icon d={I.x} size={12} color="var(--text-faint)" /></button>
        </div>
      }
      <div style={{ display: 'flex', alignItems: 'flex-end', gap: 8, background: 'var(--surface-page)', border: '1px solid var(--border-hairline)',
        borderRadius: 'var(--radius-lg)', padding: '8px 8px 8px 12px' }}>
        <button onClick={() => setAttached(true)} title="첨부" style={{ flexShrink: 0, width: 32, height: 32, borderRadius: 'var(--radius-full)',
          border: 'none', background: 'transparent', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <Icon d={I.clip} size={18} color="var(--text-muted)" /></button>
        <textarea ref={ref} rows={1} value={val} onChange={grow} onKeyDown={key} placeholder="차종·기간·지표를 포함해 질문해 보세요"
        style={{ flex: 1, resize: 'none', border: 'none', outline: 'none', background: 'transparent', fontFamily: 'var(--font-sans)',
          fontSize: 14.5, lineHeight: 1.5, color: 'var(--text-heading)', maxHeight: 120, padding: '5px 0' }} />
        <button onClick={() => setListening((l) => !l)} title="음성 입력" className={listening ? 'rb-pulse' : ''}
        style={{ flexShrink: 0, width: 32, height: 32, borderRadius: 'var(--radius-full)', border: 'none', cursor: 'pointer',
          background: listening ? 'var(--accent)' : 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <Icon d={I.mic} size={18} color={listening ? '#fff' : 'var(--text-muted)'} /></button>
        <button onClick={submit} disabled={!val.trim() || busy} title="전송"
        style={{ flexShrink: 0, width: 32, height: 32, borderRadius: 'var(--radius-full)', border: 'none',
          cursor: val.trim() && !busy ? 'pointer' : 'default', display: 'flex', alignItems: 'center', justifyContent: 'center',
          background: val.trim() && !busy ? 'var(--accent)' : 'var(--surface-card-strong)', transition: 'background .15s' }}>
          <Icon d={I.arrowUp} size={17} color={val.trim() && !busy ? '#fff' : 'var(--text-faint)'} /></button>
      </div>
      <div style={{ textAlign: 'center', fontSize: 10.5, color: 'var(--text-faint)', marginTop: 8 }}>리본 AI는 실수할 수 있어요. 중요한 수치는 원본 데이터를 확인하세요.</div>
    </div>);

}

// ── 메시지 스레드 (스크롤 영역) ─────────────────────────────
function ChatThread({ conv, onChip, opts = {} }) {
  const endRef = useRefC(null);
  useEffectC(() => {if (endRef.current) endRef.current.scrollTop = endRef.current.scrollHeight;}, [conv.messages.length, conv.status]);
  return (
    <div ref={endRef} className="rb-thread" style={{ flex: 1, overflowY: 'auto', padding: '20px 18px' }}>
      {conv.messages.length === 0 && !conv.busy ?
      <Welcome onChip={onChip} /> :

      <div className="rb-msgs" style={{ display: 'flex', flexDirection: 'column', gap: 22, maxWidth: 720, margin: '0 auto', fontWeight: "600" }}>
          {conv.messages.map((m) => m.role === 'user' ?
        <UserMsg key={m.id} text={m.text} /> :
        <AssistantMsg key={m.id} msg={m} conv={conv} onChip={onChip} opts={opts} />)}
          {conv.busy && conv.status &&
        <div style={{ display: 'flex', gap: 11 }}>
              <div style={{ flexShrink: 0, width: 30, height: 30, borderRadius: 'var(--radius-full)', background: 'var(--surface-card)',
            display: 'flex', alignItems: 'center', justifyContent: 'center' }} className="rb-breathe"><Spike size={17} /></div>
              <div style={{ fontSize: 13.5, color: 'var(--text-muted)', paddingTop: 6, fontStyle: 'italic' }}>분석 중…</div>
            </div>
        }
        </div>
      }
    </div>);

}

window.RibbonChat = { useConversation, McpStatusBar, ChatThread, ChatInput, Icon, Spike, I, relTime, historyFrom };