| import { useState, useRef, useEffect } from 'react' |
| import { Send, ChevronDown, ChevronRight } from 'lucide-react' |
| import { api } from '../api' |
| import type { AppSettings, ChatResponse, CausalChain } from '../types' |
|
|
| interface Props { |
| documentId: string |
| settings: AppSettings |
| } |
|
|
| interface Message { |
| role: 'user' | 'assistant' |
| content: string |
| response?: ChatResponse |
| } |
|
|
| function ChainCard({ chain, index }: { chain: CausalChain; index: number }) { |
| const [open, setOpen] = useState(index === 0) |
|
|
| return ( |
| <div style={{ |
| border: '1px solid var(--border)', borderRadius: 'var(--radius)', |
| marginBottom: 8, overflow: 'hidden', |
| }}> |
| <button |
| onClick={() => setOpen(!open)} |
| style={{ |
| width: '100%', display: 'flex', alignItems: 'center', gap: 8, |
| padding: '8px 12px', background: 'var(--bg3)', border: 'none', |
| color: 'var(--text)', cursor: 'pointer', textAlign: 'left', fontSize: 12, |
| }} |
| > |
| {open ? <ChevronDown size={14} /> : <ChevronRight size={14} />} |
| <span style={{ fontWeight: 600, color: chain.score > 0.6 ? 'var(--success)' : 'var(--muted)' }}> |
| Score: {chain.score.toFixed(2)} |
| </span> |
| <span style={{ |
| fontSize: 11, padding: '1px 6px', borderRadius: 99, |
| background: chain.direction === 'backward' ? 'rgba(239,68,68,0.15)' : 'rgba(96,165,250,0.15)', |
| color: chain.direction === 'backward' ? '#f87171' : '#60a5fa', |
| }}> |
| {chain.direction} |
| </span> |
| <span style={{ color: 'var(--muted)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> |
| {chain.path.join(' → ')} |
| </span> |
| </button> |
| |
| {open && ( |
| <div style={{ padding: 12, fontSize: 12 }}> |
| <div style={{ marginBottom: 10, fontFamily: 'monospace', color: 'var(--accent2)', wordBreak: 'break-word' }}> |
| {chain.path.map((node, i) => ( |
| <span key={i}> |
| <span style={{ padding: '2px 6px', background: 'rgba(99,102,241,0.15)', borderRadius: 4 }}>{node}</span> |
| {i < chain.path.length - 1 && <span style={{ color: 'var(--muted)', margin: '0 4px' }}>→</span>} |
| </span> |
| ))} |
| </div> |
| {chain.edges.map((edge, i) => ( |
| <div key={i} style={{ |
| borderLeft: '2px solid var(--border)', paddingLeft: 10, marginBottom: 8, |
| }}> |
| <div style={{ marginBottom: 2 }}> |
| <span style={{ color: '#f87171' }}>{edge.cause}</span> |
| <span style={{ margin: '0 6px' }}> |
| <span className={`badge ${edge.relation}`}>{edge.relation}</span> |
| </span> |
| <span style={{ color: '#60a5fa' }}>{edge.effect}</span> |
| <span style={{ color: 'var(--muted)', marginLeft: 8 }}>[{edge.confidence.toFixed(2)}]</span> |
| </div> |
| {edge.evidence && ( |
| <div style={{ color: 'var(--muted)', fontStyle: 'italic', fontSize: 11 }}> |
| "{edge.evidence}" |
| </div> |
| )} |
| </div> |
| ))} |
| </div> |
| )} |
| </div> |
| ) |
| } |
|
|
| export function ChatPanel({ documentId, settings }: Props) { |
| const [messages, setMessages] = useState<Message[]>([]) |
| const [input, setInput] = useState('') |
| const [loading, setLoading] = useState(false) |
| const [error, setError] = useState<string | null>(null) |
| const bottomRef = useRef<HTMLDivElement>(null) |
|
|
| useEffect(() => { |
| bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) |
| }, [messages]) |
|
|
| const send = async () => { |
| const msg = input.trim() |
| if (!msg || loading) return |
| setInput('') |
| setError(null) |
| setMessages(prev => [...prev, { role: 'user', content: msg }]) |
| setLoading(true) |
| try { |
| const res = await api.chat(documentId, msg, settings) |
| setMessages(prev => [...prev, { role: 'assistant', content: res.answer, response: res }]) |
| } catch (e: any) { |
| setError(String(e.message)) |
| } finally { |
| setLoading(false) |
| } |
| } |
|
|
| const SAMPLES = [ |
| 'Why did quarterly revenue decrease?', |
| 'What caused inventory shortages?', |
| 'What are the downstream effects of port congestion?', |
| ] |
|
|
| return ( |
| <div style={{ display: 'flex', flexDirection: 'column', height: '100%', maxWidth: 900 }}> |
| {messages.length === 0 && ( |
| <div style={{ marginBottom: 16 }}> |
| <div className="label" style={{ marginBottom: 8 }}>Sample Questions</div> |
| <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}> |
| {SAMPLES.map(q => ( |
| <button key={q} className="btn-secondary" style={{ fontSize: 12 }} onClick={() => setInput(q)}> |
| {q} |
| </button> |
| ))} |
| </div> |
| </div> |
| )} |
| |
| <div style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 16, marginBottom: 16 }}> |
| {messages.map((m, i) => ( |
| <div key={i}> |
| {m.role === 'user' ? ( |
| <div style={{ |
| background: 'var(--accent)', borderRadius: 'var(--radius)', |
| padding: '10px 14px', alignSelf: 'flex-end', fontSize: 14, |
| maxWidth: '70%', marginLeft: 'auto', |
| }}> |
| {m.content} |
| </div> |
| ) : ( |
| <div> |
| <div className="card" style={{ marginBottom: 12, fontSize: 14, lineHeight: 1.6 }}> |
| {m.content} |
| </div> |
| {m.response && m.response.chains.length > 0 && ( |
| <div> |
| <div className="label" style={{ marginBottom: 8 }}> |
| Retrieved Causal Chains ({m.response.chains.length}) |
| </div> |
| {m.response.chains.map((chain, ci) => ( |
| <ChainCard key={ci} chain={chain} index={ci} /> |
| ))} |
| </div> |
| )} |
| </div> |
| )} |
| </div> |
| ))} |
| {loading && ( |
| <div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--muted)', fontSize: 13 }}> |
| <span className="spinner" style={{ width: 16, height: 16 }} /> |
| Retrieving causal chains... |
| </div> |
| )} |
| {error && <div className="error-msg">{error}</div>} |
| <div ref={bottomRef} /> |
| </div> |
| |
| <div style={{ display: 'flex', gap: 8 }}> |
| <input |
| type="text" |
| placeholder="Ask about causal relationships..." |
| value={input} |
| onChange={e => setInput(e.target.value)} |
| onKeyDown={e => e.key === 'Enter' && send()} |
| disabled={loading} |
| style={{ flex: 1 }} |
| /> |
| <button className="btn-primary" onClick={send} disabled={loading || !input.trim()}> |
| <Send size={16} /> |
| </button> |
| </div> |
| </div> |
| ) |
| } |
|
|