// AIPanel.jsx — 전역 AI 어시스턴트 (우측 사이드 패널). 화면 컨텍스트 기반 채팅. // ---- 컨텍스트 스토어 ---- let _AICTX = { screen: 'IBK 리스크 인사이트', summary: '', facts: [], suggestions: [] }; function setAIContext(c){ _AICTX = { screen:'IBK 리스크 인사이트', summary:'', facts:[], suggestions:[], ...c }; window.dispatchEvent(new CustomEvent('aictx', { detail:_AICTX })); } function getAIContext(){ return _AICTX; } Object.assign(window, { setAIContext, getAIContext }); // ---- 스크립트형 폴백 응답 (LLM 미가용 시) ---- function fallbackAnswer(q, ctx){ const f = (ctx.facts||[]).slice(0,4).map(x=>'· '+x).join('\n'); const lead = ctx.summary ? ctx.summary+' ' : ''; return `현재 보고 계신 **${ctx.screen}** 화면을 기준으로 말씀드릴게요.\n\n${lead}주요 수치는 다음과 같아요.\n${f}\n\n질문하신 내용은 이 데이터를 토대로 살펴봤을 때, 위험액 추이와 한도 소진율을 함께 모니터링하시는 것이 좋아요. 더 구체적으로 알고 싶은 항목을 골라주시면 자세히 설명드릴게요.`; } async function askAI(q, ctx, history){ try { const res = await fetch('/api/ai', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question: q, context: ctx, history: (history||[]).slice(-6) }) }); if (res.ok) { const data = await res.json(); if (data.answer && data.answer.trim()) return data.answer.trim(); } } catch(e){ /* fall through to fallback */ } await new Promise(r=>setTimeout(r, 650)); return fallbackAnswer(q, ctx); } // 간단 마크다운(**굵게**, 줄바꿈) 렌더 function RichText({ text }){ const parts = text.split(/(\*\*[^*]+\*\*)/g); return {parts.map((p,i)=> p.startsWith('**') && p.endsWith('**') ? {p.slice(2,-2)} : {p})}; } function Bubble({ m }){ const me = m.role==='user'; return (
{!me &&
}
{me ? m.text : }
); } function AIPanel({ open, onClose }){ const [ctx, setCtx] = React.useState(getAIContext()); const [msgs, setMsgs] = React.useState([]); const [input, setInput] = React.useState(''); const [busy, setBusy] = React.useState(false); const scrollRef = React.useRef(null); const inputRef = React.useRef(null); React.useEffect(()=>{ const h=e=>setCtx(e.detail); window.addEventListener('aictx',h); return ()=>window.removeEventListener('aictx',h); },[]); React.useEffect(()=>{ if(scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; },[msgs, busy]); React.useEffect(()=>{ if(open && inputRef.current) setTimeout(()=>inputRef.current.focus(),250); },[open]); const send = React.useCallback(async (text)=>{ const q = (text||'').trim(); if(!q || busy) return; setInput(''); const c = getAIContext(); setMsgs(m=>[...m, { role:'user', text:q }]); setBusy(true); const hist = msgs; const ans = await askAI(q, c, hist); setMsgs(m=>[...m, { role:'ai', text:ans }]); setBusy(false); },[busy, msgs]); return (