| import React, { useState, useEffect, useCallback } from 'react'; |
| import { useAuth } from '../context/AuthContext'; |
| import { BarChart2, TrendingUp, AlertTriangle, CheckCircle, RefreshCw, Zap, FileText, GitCommit, MessageSquare } from 'lucide-react'; |
|
|
| const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api'; |
|
|
| interface EvalDashboard { |
| total_evaluations: number; |
| avg_overall_score: number; |
| avg_faithfulness: number; |
| avg_relevancy: number; |
| hallucination_rate: number; |
| trend_data: TrendPoint[]; |
| } |
|
|
| interface TrendPoint { |
| timestamp: string; |
| overall_score: number; |
| faithfulness: number; |
| answer_relevancy: number; |
| hallucination_detected: boolean; |
| document_id?: string; |
| } |
|
|
| interface EvalForm { |
| question: string; |
| answer: string; |
| contexts: string; |
| } |
|
|
| |
| function buildReportMarkdown(result: any, topic: string): string { |
| const lines: string[] = []; |
| lines.push(`# ${result.topic || topic}`); |
| lines.push(''); |
| if (result.confidence !== undefined) { |
| lines.push(`**Confidence:** ${(result.confidence * 100).toFixed(1)}%`); |
| } |
| if (result.tool_calls_made !== undefined) { |
| lines.push(`**Tool calls:** ${result.tool_calls_made}`); |
| } |
| lines.push(`**Generated:** ${new Date().toLocaleString()}`); |
| lines.push(''); |
|
|
| if (result.executive_summary) { |
| lines.push('## Executive Summary'); |
| lines.push(''); |
| lines.push(result.executive_summary); |
| lines.push(''); |
| } |
|
|
| if (result.sections && typeof result.sections === 'object' && !Array.isArray(result.sections)) { |
| Object.entries(result.sections).forEach(([title, content], i) => { |
| lines.push(`## ${i + 1}. ${title}`); |
| lines.push(''); |
| lines.push(String(content)); |
| lines.push(''); |
| }); |
| } else if (Array.isArray(result.sections)) { |
| result.sections.forEach((s: any, i: number) => { |
| lines.push(`## ${i + 1}. ${s.title || ''}`); |
| lines.push(''); |
| lines.push(s.content || ''); |
| lines.push(''); |
| }); |
| } else if (!result.sections) { |
| lines.push(result.report || result.content || result.markdown || JSON.stringify(result, null, 2)); |
| lines.push(''); |
| } |
|
|
| if (result.key_entities && result.key_entities.length > 0) { |
| lines.push('## Key Entities'); |
| lines.push(''); |
| lines.push(result.key_entities.join(', ')); |
| } |
|
|
| return lines.join('\n'); |
| } |
|
|
| const InsightsView: React.FC = () => { |
| const { token } = useAuth(); |
| const [dashboard, setDashboard] = useState<EvalDashboard | null>(null); |
| const [loading, setLoading] = useState(false); |
| const [evalForm, setEvalForm] = useState<EvalForm>({ question: '', answer: '', contexts: '' }); |
| const [evalResult, setEvalResult] = useState<any>(null); |
| const [evalLoading, setEvalLoading] = useState(false); |
| const [communities, setCommunities] = useState<any[]>([]); |
| const [communityLoading, setCommunityLoading] = useState(false); |
| const [activeTab, setActiveTab] = useState<'metrics' | 'evaluate' | 'communities' | 'export' | 'report' | 'graph-update' | 'entity-chat'>('metrics'); |
|
|
| |
| const [reportTopic, setReportTopic] = useState(''); |
| const [reportDepth, setReportDepth] = useState(3); |
| const [reportResult, setReportResult] = useState<any>(null); |
| const [reportLoading, setReportLoading] = useState(false); |
| const [reportTimestamp, setReportTimestamp] = useState<Date | null>(null); |
| const [copyDone, setCopyDone] = useState(false); |
|
|
| |
| const [updateText, setUpdateText] = useState(''); |
| const [updateResult, setUpdateResult] = useState<any>(null); |
| const [updateLoading, setUpdateLoading] = useState(false); |
|
|
| |
| const [entities, setEntities] = useState<any[]>([]); |
| const [selectedEntity, setSelectedEntity] = useState(''); |
| const [entityContext, setEntityContext] = useState(''); |
| const [chatMsg, setChatMsg] = useState(''); |
| const [chatHistory, setChatHistory] = useState<{role: string; content: string}[]>([]); |
| const [chatLoading, setChatLoading] = useState(false); |
|
|
| const fetchDashboard = useCallback(async () => { |
| setLoading(true); |
| try { |
| const res = await fetch(`${API_BASE}/eval/dashboard?limit=200`, { |
| headers: { Authorization: `Bearer ${token}` } |
| }); |
| if (res.ok) setDashboard(await res.json()); |
| } catch (e) { console.error(e); } |
| setLoading(false); |
| }, [token]); |
|
|
| const fetchCommunities = useCallback(async () => { |
| setCommunityLoading(true); |
| try { |
| const res = await fetch(`${API_BASE}/graph/communities?limit=30`, { |
| headers: { Authorization: `Bearer ${token}` } |
| }); |
| if (res.ok) { |
| const data = await res.json(); |
| setCommunities(data.communities || []); |
| } |
| } catch (e) { console.error(e); } |
| setCommunityLoading(false); |
| }, [token]); |
|
|
| const fetchEntities = useCallback(async () => { |
| try { |
| const res = await fetch(`${API_BASE}/admin/graph/nodes?query=&limit=200`, { |
| headers: { Authorization: `Bearer ${token}` } |
| }); |
| if (res.ok) { |
| const data = await res.json(); |
| setEntities((data.nodes || []).slice(0, 100)); |
| } |
| } catch {} |
| }, [token]); |
|
|
| useEffect(() => { |
| fetchDashboard(); |
| fetchCommunities(); |
| fetchEntities(); |
| }, [fetchDashboard, fetchCommunities, fetchEntities]); |
|
|
| const runEval = async () => { |
| if (!evalForm.question || !evalForm.answer || !evalForm.contexts) return; |
| setEvalLoading(true); |
| setEvalResult(null); |
| try { |
| const res = await fetch(`${API_BASE}/eval/score`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, |
| body: JSON.stringify({ |
| question: evalForm.question, |
| answer: evalForm.answer, |
| contexts: evalForm.contexts.split('\n---\n').map(c => c.trim()).filter(Boolean) |
| }) |
| }); |
| if (res.ok) { |
| setEvalResult(await res.json()); |
| fetchDashboard(); |
| } |
| } catch (e) { console.error(e); } |
| setEvalLoading(false); |
| }; |
|
|
| const runReport = async () => { |
| if (!reportTopic.trim()) return; |
| setReportLoading(true); setReportResult(null); |
| try { |
| const res = await fetch(`${API_BASE}/report`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, |
| body: JSON.stringify({ topic: reportTopic, depth: reportDepth }) |
| }); |
| if (res.ok) { |
| setReportResult(await res.json()); |
| setReportTimestamp(new Date()); |
| } |
| } catch (e) { console.error(e); } |
| setReportLoading(false); |
| }; |
|
|
| const runGraphUpdate = async () => { |
| if (!updateText.trim()) return; |
| setUpdateLoading(true); setUpdateResult(null); |
| try { |
| const res = await fetch(`${API_BASE}/graph/update`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, |
| body: JSON.stringify({ text: updateText }) |
| }); |
| if (res.ok) setUpdateResult(await res.json()); |
| } catch (e) { console.error(e); } |
| setUpdateLoading(false); |
| }; |
|
|
| const sendEntityChat = async () => { |
| if (!selectedEntity || !chatMsg.trim()) return; |
| const userMsg = { role: 'user', content: chatMsg }; |
| setChatHistory(h => [...h, userMsg]); |
| setChatMsg(''); |
| setChatLoading(true); |
| try { |
| const res = await fetch(`${API_BASE}/entities/${encodeURIComponent(selectedEntity)}/chat`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, |
| body: JSON.stringify({ message: userMsg.content }) |
| }); |
| if (res.ok) { |
| const data = await res.json(); |
| setChatHistory(h => [...h, { role: 'assistant', content: data.response || data.answer || JSON.stringify(data) }]); |
| } |
| } catch (e) { console.error(e); } |
| setChatLoading(false); |
| }; |
|
|
| const assignCommunities = async () => { |
| setCommunityLoading(true); |
| try { |
| const res = await fetch(`${API_BASE}/graph/communities/assign`, { |
| method: 'POST', |
| headers: { Authorization: `Bearer ${token}` } |
| }); |
| if (res.ok) { |
| const data = await res.json(); |
| alert(`✓ ${data.message}`); |
| fetchCommunities(); |
| } |
| } catch (e) { console.error(e); } |
| setCommunityLoading(false); |
| }; |
|
|
| const exportGraph = async (fmt: string) => { |
| const res = await fetch(`${API_BASE}/graph/export?format=${fmt}`, { |
| headers: { Authorization: `Bearer ${token}` } |
| }); |
| if (!res.ok) return; |
| const blob = await res.blob(); |
| const ext = fmt === 'json' ? 'json' : fmt === 'cypher' ? 'cypher' : 'graphml'; |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; a.download = `graph_export.${ext}`; a.click(); |
| URL.revokeObjectURL(url); |
| }; |
|
|
| const handleCopyReport = () => { |
| if (!reportResult) return; |
| const text = buildReportMarkdown(reportResult, reportTopic); |
| navigator.clipboard.writeText(text).then(() => { |
| setCopyDone(true); |
| setTimeout(() => setCopyDone(false), 2000); |
| }); |
| }; |
|
|
| const handleDownloadReport = () => { |
| if (!reportResult) return; |
| const text = buildReportMarkdown(reportResult, reportTopic); |
| const blob = new Blob([text], { type: 'text/markdown' }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = `report_${reportTopic.slice(0, 30).replace(/\s+/g, '_')}.md`; |
| a.click(); |
| URL.revokeObjectURL(url); |
| }; |
|
|
| const score2color = (s: number) => { |
| if (s >= 0.8) return '#16a34a'; |
| if (s >= 0.6) return '#d97706'; |
| return '#dc2626'; |
| }; |
| const score2label = (s: number) => s >= 0.8 ? 'HIGH' : s >= 0.6 ? 'MEDIUM' : 'LOW'; |
|
|
| const ScoreBar: React.FC<{ label: string; value: number }> = ({ label, value }) => ( |
| <div style={{ marginBottom: '1rem' }}> |
| <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.25rem', fontFamily: 'var(--font-mono)', fontSize: '0.8rem' }}> |
| <span>{label}</span> |
| <span style={{ color: score2color(value), fontWeight: 700 }}>{(value * 100).toFixed(1)}%</span> |
| </div> |
| <div style={{ height: '8px', background: '#e5e7eb', position: 'relative' }}> |
| <div style={{ position: 'absolute', left: 0, top: 0, height: '100%', width: `${value * 100}%`, background: score2color(value), transition: 'width 0.6s ease' }} /> |
| </div> |
| </div> |
| ); |
|
|
| const confidenceBadgeColor = (c: number) => |
| c >= 0.8 ? '#16a34a' : c >= 0.6 ? '#d97706' : '#dc2626'; |
|
|
| return ( |
| <div className="container" style={{ maxWidth: '1100px', paddingBottom: '3rem' }}> |
| <div className="page-header flex-between" style={{ marginBottom: '2rem' }}> |
| <div> |
| <h1>INSIGHTS HQ</h1> |
| <p className="mono-text">QUALITY METRICS // EVAL DASHBOARD // GRAPH INTELLIGENCE</p> |
| </div> |
| <div style={{ display: 'flex', gap: '0.5rem' }}> |
| <button className="app-btn mono-text" onClick={fetchDashboard} disabled={loading} style={{ padding: '0.5rem 1rem', fontSize: '0.8rem' }}> |
| <RefreshCw size={14} style={{ display: 'inline', marginRight: '0.4rem' }} /> |
| REFRESH |
| </button> |
| </div> |
| </div> |
| |
| {/* Tab Nav */} |
| <div style={{ display: 'flex', borderBottom: '3px solid #000', marginBottom: '2rem', gap: 0, flexWrap: 'wrap' }}> |
| {([ |
| { id: 'metrics', label: 'METRICS' }, |
| { id: 'evaluate', label: 'EVALUATE' }, |
| { id: 'communities', label: 'COMMUNITIES' }, |
| { id: 'export', label: 'EXPORT' }, |
| { id: 'report', label: '⚡ REPORT' }, |
| { id: 'graph-update', label: '↑ LIVE UPDATE' }, |
| { id: 'entity-chat', label: '💬 ENTITY CHAT' }, |
| ] as const).map(t => ( |
| <button |
| key={t.id} |
| onClick={() => setActiveTab(t.id as any)} |
| className="mono-text" |
| style={{ |
| padding: '0.65rem 1.2rem', fontWeight: 700, fontSize: '0.78rem', letterSpacing: '0.8px', |
| border: 'none', borderBottom: activeTab === t.id ? '3px solid #000' : 'none', |
| background: activeTab === t.id ? '#000' : 'transparent', |
| color: activeTab === t.id ? '#fff' : '#000', cursor: 'pointer', |
| marginBottom: '-3px', textTransform: 'uppercase', whiteSpace: 'nowrap' |
| }} |
| > |
| {t.label} |
| </button> |
| ))} |
| </div> |
| |
| {/* ── METRICS TAB ── */} |
| {activeTab === 'metrics' && ( |
| <div> |
| {loading ? ( |
| <div className="mono-text" style={{ textAlign: 'center', padding: '3rem', color: '#aaa' }}>LOADING METRICS...</div> |
| ) : !dashboard || dashboard.total_evaluations === 0 ? ( |
| <div style={{ border: '3px solid #000', padding: '3rem', textAlign: 'center' }}> |
| <BarChart2 size={48} style={{ marginBottom: '1rem', opacity: 0.3 }} /> |
| <p className="mono-text" style={{ color: '#777' }}>NO EVALUATION DATA YET</p> |
| <p className="mono-text" style={{ color: '#999', fontSize: '0.85rem', marginTop: '0.5rem' }}> |
| Use the EVALUATE tab to score Q&A pairs and build your quality history. |
| </p> |
| </div> |
| ) : ( |
| <> |
| {/* KPI Cards */} |
| <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1rem', marginBottom: '2rem' }}> |
| {[ |
| { label: 'TOTAL EVALS', value: dashboard.total_evaluations, raw: true, icon: <BarChart2 size={20} /> }, |
| { label: 'AVG QUALITY', value: dashboard.avg_overall_score, icon: <TrendingUp size={20} /> }, |
| { label: 'FAITHFULNESS', value: dashboard.avg_faithfulness, icon: <CheckCircle size={20} /> }, |
| { label: 'HALLUCINATION RATE', value: dashboard.hallucination_rate, invert: true, icon: <AlertTriangle size={20} /> }, |
| ].map(card => ( |
| <div key={card.label} style={{ border: '3px solid #000', padding: '1.5rem', background: (card.invert ? dashboard.hallucination_rate > 0.3 : false) ? '#fff5f5' : '#fff' }}> |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '1rem' }}> |
| <span className="mono-text" style={{ fontSize: '0.7rem', fontWeight: 700, letterSpacing: '1px', color: '#666' }}>{card.label}</span> |
| {card.icon} |
| </div> |
| <div className="mono-text" style={{ |
| fontSize: '2rem', fontWeight: 900, |
| color: card.raw ? '#000' : score2color(card.invert ? 1 - (card.value as number) : (card.value as number)) |
| }}> |
| {card.raw ? card.value : `${((card.value as number) * 100).toFixed(1)}%`} |
| </div> |
| </div> |
| ))} |
| </div> |
| |
| {/* Score Breakdown */} |
| <div style={{ border: '3px solid #000', padding: '1.5rem', marginBottom: '2rem' }}> |
| <h3 className="mono-text" style={{ marginBottom: '1.5rem', borderBottom: '1px dotted #000', paddingBottom: '0.5rem' }}>METRIC BREAKDOWN</h3> |
| <ScoreBar label="Overall Quality Score" value={dashboard.avg_overall_score} /> |
| <ScoreBar label="Faithfulness (grounding)" value={dashboard.avg_faithfulness} /> |
| <ScoreBar label="Answer Relevancy" value={dashboard.avg_relevancy} /> |
| <ScoreBar label="Non-hallucination Rate" value={1 - dashboard.hallucination_rate} /> |
| </div> |
| |
| {/* Trend Table */} |
| {dashboard.trend_data.length > 0 && ( |
| <div style={{ border: '3px solid #000', padding: '1.5rem' }}> |
| <h3 className="mono-text" style={{ marginBottom: '1.5rem', borderBottom: '1px dotted #000', paddingBottom: '0.5rem' }}>EVALUATION HISTORY (LATEST {Math.min(dashboard.trend_data.length, 20)})</h3> |
| <div style={{ overflowX: 'auto' }}> |
| <table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-mono)', fontSize: '0.8rem' }}> |
| <thead> |
| <tr style={{ borderBottom: '2px solid #000' }}> |
| {['TIMESTAMP', 'QUALITY', 'FAITHFULNESS', 'RELEVANCY', 'HALLUCINATION'].map(h => ( |
| <th key={h} style={{ padding: '0.5rem', textAlign: 'left', fontWeight: 700 }}>{h}</th> |
| ))} |
| </tr> |
| </thead> |
| <tbody> |
| {dashboard.trend_data.slice(0, 20).map((p, i) => ( |
| <tr key={i} style={{ borderBottom: '1px dotted #ddd', background: i % 2 === 0 ? '#fafafa' : '#fff' }}> |
| <td style={{ padding: '0.5rem' }}>{p.timestamp}</td> |
| <td style={{ padding: '0.5rem', color: score2color(p.overall_score), fontWeight: 700 }}>{(p.overall_score * 100).toFixed(1)}%</td> |
| <td style={{ padding: '0.5rem', color: score2color(p.faithfulness) }}>{(p.faithfulness * 100).toFixed(1)}%</td> |
| <td style={{ padding: '0.5rem', color: score2color(p.answer_relevancy) }}>{(p.answer_relevancy * 100).toFixed(1)}%</td> |
| <td style={{ padding: '0.5rem' }}> |
| <span style={{ background: p.hallucination_detected ? '#dc2626' : '#16a34a', color: '#fff', padding: '0.1rem 0.5rem', fontSize: '0.7rem', fontWeight: 700 }}> |
| {p.hallucination_detected ? 'YES' : 'NO'} |
| </span> |
| </td> |
| </tr> |
| ))} |
| </tbody> |
| </table> |
| </div> |
| </div> |
| )} |
| </> |
| )} |
| </div> |
| )} |
|
|
| {} |
| {activeTab === 'evaluate' && ( |
| <div> |
| <div style={{ border: '3px solid #000', padding: '2rem', marginBottom: '2rem' }}> |
| <h3 className="mono-text" style={{ marginBottom: '1.5rem' }}> |
| <Zap size={16} style={{ display: 'inline', marginRight: '0.5rem' }} /> |
| SCORE A Q&A PAIR |
| </h3> |
| <p className="mono-text" style={{ fontSize: '0.8rem', color: '#666', marginBottom: '1.5rem' }}> |
| Paste a question, its generated answer, and the retrieved context chunks (separate chunks with "---"). |
| </p> |
| |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> |
| <div> |
| <label className="mono-text" style={{ fontSize: '0.8rem', fontWeight: 700, display: 'block', marginBottom: '0.5rem' }}>QUESTION</label> |
| <textarea value={evalForm.question} onChange={e => setEvalForm(f => ({ ...f, question: e.target.value }))} |
| rows={2} style={{ width: '100%', border: '2px solid #000', padding: '0.75rem', fontFamily: 'var(--font-mono)', fontSize: '0.9rem', resize: 'vertical', boxSizing: 'border-box' }} /> |
| </div> |
| <div> |
| <label className="mono-text" style={{ fontSize: '0.8rem', fontWeight: 700, display: 'block', marginBottom: '0.5rem' }}>GENERATED ANSWER</label> |
| <textarea value={evalForm.answer} onChange={e => setEvalForm(f => ({ ...f, answer: e.target.value }))} |
| rows={4} style={{ width: '100%', border: '2px solid #000', padding: '0.75rem', fontFamily: 'var(--font-mono)', fontSize: '0.9rem', resize: 'vertical', boxSizing: 'border-box' }} /> |
| </div> |
| <div> |
| <label className="mono-text" style={{ fontSize: '0.8rem', fontWeight: 700, display: 'block', marginBottom: '0.5rem' }}>CONTEXT CHUNKS (separate with "---" on its own line)</label> |
| <textarea value={evalForm.contexts} onChange={e => setEvalForm(f => ({ ...f, contexts: e.target.value }))} |
| rows={6} placeholder={'Context chunk 1 text...\n---\nContext chunk 2 text...'} style={{ width: '100%', border: '2px solid #000', padding: '0.75rem', fontFamily: 'var(--font-mono)', fontSize: '0.85rem', resize: 'vertical', boxSizing: 'border-box' }} /> |
| </div> |
| <button className="app-btn mono-text" onClick={runEval} disabled={evalLoading || !evalForm.question || !evalForm.answer || !evalForm.contexts} |
| style={{ alignSelf: 'flex-start', padding: '0.75rem 2rem' }}> |
| {evalLoading ? 'EVALUATING...' : 'RUN EVALUATION'} |
| </button> |
| </div> |
| </div> |
| |
| {evalResult && ( |
| <div style={{ border: '3px solid #000', padding: '2rem', background: evalResult.hallucination_detected ? '#fff5f5' : '#f0fdf4' }}> |
| <h3 className="mono-text" style={{ marginBottom: '1.5rem' }}> |
| {evalResult.hallucination_detected |
| ? <><AlertTriangle size={16} style={{ display: 'inline', marginRight: '0.5rem', color: '#dc2626' }} />HALLUCINATION RISK DETECTED</> |
| : <><CheckCircle size={16} style={{ display: 'inline', marginRight: '0.5rem', color: '#16a34a' }} />EVALUATION RESULTS</> |
| } |
| </h3> |
| <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '1rem', marginBottom: '1.5rem' }}> |
| {[ |
| { label: 'OVERALL', value: evalResult.overall_score }, |
| { label: 'FAITHFULNESS', value: evalResult.faithfulness }, |
| { label: 'RELEVANCY', value: evalResult.answer_relevancy }, |
| { label: 'PRECISION', value: evalResult.context_precision }, |
| ].map(m => ( |
| <div key={m.label} style={{ border: `2px solid ${score2color(m.value)}`, padding: '1rem', textAlign: 'center' }}> |
| <div className="mono-text" style={{ fontSize: '0.7rem', fontWeight: 700, color: '#666', marginBottom: '0.5rem' }}>{m.label}</div> |
| <div className="mono-text" style={{ fontSize: '1.8rem', fontWeight: 900, color: score2color(m.value) }}> |
| {(m.value * 100).toFixed(0)}% |
| </div> |
| <div className="mono-text" style={{ fontSize: '0.65rem', color: score2color(m.value), marginTop: '0.25rem' }}>{score2label(m.value)}</div> |
| </div> |
| ))} |
| </div> |
| <div className="mono-text" style={{ fontSize: '0.75rem', color: '#777', borderTop: '1px dotted #ccc', paddingTop: '0.75rem' }}> |
| Saved to Neo4j for trending. Eval ID: {evalResult.eval_id || 'N/A'} |
| </div> |
| </div> |
| )} |
| </div> |
| )} |
|
|
| {} |
| {activeTab === 'communities' && ( |
| <div> |
| <div style={{ display: 'flex', gap: '1rem', marginBottom: '2rem', alignItems: 'center', flexWrap: 'wrap' }}> |
| <button className="app-btn mono-text" onClick={assignCommunities} disabled={communityLoading} style={{ padding: '0.75rem 1.5rem' }}> |
| {communityLoading ? 'DETECTING...' : '⚡ DETECT COMMUNITIES'} |
| </button> |
| <span className="mono-text" style={{ fontSize: '0.8rem', color: '#666' }}> |
| Run after ingesting documents to cluster entities into related groups (enables community search). |
| </span> |
| {communities.length === 0 && !communityLoading && ( |
| <span style={{ |
| background: '#dc2626', |
| color: '#fff', |
| padding: '0.25rem 0.75rem', |
| borderRadius: '4px', |
| fontFamily: 'var(--font-mono)', |
| fontSize: '0.75rem', |
| fontWeight: 700, |
| display: 'flex', |
| alignItems: 'center', |
| gap: '0.35rem', |
| animation: 'pulse 2s infinite' |
| }}> |
| <AlertTriangle size={14} /> |
| REQUIRED: GENERATE REPORTS |
| </span> |
| )} |
| </div> |
| |
| {communities.length === 0 ? ( |
| <div style={{ border: '3px solid #000', padding: '3rem', textAlign: 'center' }}> |
| <p className="mono-text" style={{ color: '#777' }}>NO COMMUNITIES DETECTED YET</p> |
| <p className="mono-text" style={{ color: '#999', fontSize: '0.85rem', marginTop: '0.5rem' }}>Click "Detect Communities" above to cluster your knowledge graph entities.</p> |
| </div> |
| ) : ( |
| <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '1rem' }}> |
| {communities.map((c, i) => ( |
| <div key={i} style={{ border: '2px solid #000', padding: '1.25rem' }}> |
| <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.75rem' }}> |
| <span className="mono-text" style={{ fontWeight: 700, fontSize: '0.85rem' }}>CLUSTER #{c.community_id}</span> |
| <span className="mono-text" style={{ fontSize: '0.75rem', background: '#000', color: '#fff', padding: '0.15rem 0.5rem' }}> |
| {c.entity_count} entities |
| </span> |
| </div> |
| <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.35rem' }}> |
| {(c.sample_entities || []).map((e: string, ei: number) => ( |
| <span key={ei} style={{ background: '#f3f4f6', border: '1px solid #d1d5db', padding: '0.15rem 0.5rem', fontSize: '0.75rem', fontFamily: 'var(--font-mono)' }}>{e}</span> |
| ))} |
| {c.entity_count > (c.sample_entities || []).length && ( |
| <span style={{ color: '#666', fontSize: '0.75rem', fontFamily: 'var(--font-mono)', padding: '0.15rem 0' }}> |
| +{c.entity_count - (c.sample_entities || []).length} more |
| </span> |
| )} |
| </div> |
| </div> |
| ))} |
| </div> |
| )} |
| </div> |
| )} |
|
|
| {} |
| {activeTab === 'export' && ( |
| <div> |
| <div style={{ border: '3px solid #000', padding: '2rem', marginBottom: '2rem' }}> |
| <h3 className="mono-text" style={{ marginBottom: '1.5rem' }}>EXPORT KNOWLEDGE GRAPH</h3> |
| <p className="mono-text" style={{ fontSize: '0.85rem', color: '#555', marginBottom: '2rem', lineHeight: '1.6' }}> |
| Export your knowledge graph for use in external tools, backups, or further analysis. |
| </p> |
| <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: '1rem' }}> |
| {[ |
| { fmt: 'json', label: 'JSON', desc: 'Nodes & edges as JSON. For custom processing or visualization.' }, |
| { fmt: 'cypher', label: 'CYPHER', desc: 'Cypher CREATE statements. Re-import to any Neo4j instance.' }, |
| { fmt: 'graphml', label: 'GRAPHML', desc: 'GraphML XML. Compatible with Gephi, yEd, and most graph tools.' }, |
| ].map(e => ( |
| <div key={e.fmt} style={{ border: '2px solid #000', padding: '1.5rem' }}> |
| <div className="mono-text" style={{ fontWeight: 900, fontSize: '1.1rem', marginBottom: '0.5rem' }}>.{e.label}</div> |
| <p style={{ fontSize: '0.82rem', color: '#555', marginBottom: '1.25rem', fontFamily: 'var(--font-sans)', lineHeight: '1.5' }}>{e.desc}</p> |
| <button className="app-btn mono-text" onClick={() => exportGraph(e.fmt)} style={{ width: '100%', textAlign: 'center' }}> |
| DOWNLOAD .{e.label} |
| </button> |
| </div> |
| ))} |
| </div> |
| </div> |
| </div> |
| )} |
|
|
| {} |
| {activeTab === 'report' && ( |
| <div> |
| <div className="page-info-bar"> |
| <FileText size={14}/> |
| <span><strong>REPORT AGENT</strong> — Enter any topic or question. The ReACT agent autonomously queries the knowledge graph using multiple retrieval strategies and synthesizes a deep multi-section report. More <strong>depth</strong> = more reasoning steps.</span> |
| </div> |
| |
| {/* Input form */} |
| <div style={{ border: '3px solid #000', padding: '2rem', marginBottom: '2rem' }}> |
| <h3 className="mono-text" style={{ marginBottom: '1.5rem' }}> |
| <Zap size={16} style={{ display: 'inline', marginRight: '0.5rem' }}/>GENERATE ANALYTICAL REPORT |
| </h3> |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> |
| <div> |
| <label className="mono-text" style={{ fontSize: '0.8rem', fontWeight: 700, display: 'block', marginBottom: '0.5rem' }}>TOPIC OR QUESTION</label> |
| <textarea value={reportTopic} onChange={e => setReportTopic(e.target.value)} |
| rows={3} placeholder="e.g. 'Summarize all relationships between Company X and its investors'" |
| style={{ width: '100%', border: '2px solid #000', padding: '0.75rem', fontFamily: 'var(--font-mono)', fontSize: '0.9rem', resize: 'vertical', boxSizing: 'border-box' }}/> |
| </div> |
| <div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}> |
| <div> |
| <label className="mono-text" style={{ fontSize: '0.8rem', fontWeight: 700, display: 'block', marginBottom: '0.5rem' }}>DEPTH (REASONING STEPS)</label> |
| <select value={reportDepth} onChange={e => setReportDepth(Number(e.target.value))} |
| style={{ border: '2px solid #000', padding: '0.5rem 0.75rem', fontFamily: 'var(--font-mono)', fontSize: '0.9rem' }}> |
| {[1, 2, 3, 4, 5].map(n => <option key={n} value={n}>{n} {n === 1 ? '(fast)' : n === 5 ? '(deep)' : ''}</option>)} |
| </select> |
| </div> |
| <button className="app-btn mono-text" onClick={runReport} disabled={reportLoading || !reportTopic.trim()} |
| style={{ alignSelf: 'flex-end', padding: '0.75rem 2rem' }}> |
| {reportLoading ? 'GENERATING...' : 'GENERATE REPORT'} |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| {/* Empty state */} |
| {!reportResult && !reportLoading && ( |
| <div style={{ border: '3px dashed #e5e5e5', padding: '4rem 2rem', textAlign: 'center' }}> |
| <FileText size={44} style={{ opacity: 0.18, marginBottom: '1rem' }} /> |
| <p className="mono-text" style={{ color: '#aaa', fontSize: '0.85rem', letterSpacing: '1px' }}>NO REPORT GENERATED YET</p> |
| <p style={{ color: '#bbb', fontSize: '0.8rem', marginTop: '0.5rem', fontFamily: 'var(--font-sans)' }}> |
| Enter a topic above and click Generate to produce an AI-powered analytical report from the knowledge graph. |
| </p> |
| </div> |
| )} |
| |
| {/* Loading state */} |
| {reportLoading && ( |
| <div style={{ border: '3px solid #000', padding: '2.5rem', textAlign: 'center' }}> |
| <div style={{ fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: '0.9rem', marginBottom: '0.75rem' }}> |
| ⚡ AGENT REASONING… |
| </div> |
| <div style={{ fontFamily: 'var(--font-sans)', fontSize: '0.82rem', color: 'var(--muted-color)' }}> |
| Querying knowledge graph with depth {reportDepth}. This may take 20–60 seconds. |
| </div> |
| </div> |
| )} |
| |
| {/* Report output */} |
| {reportResult && ( |
| <div style={{ border: '3px solid #000' }}> |
| {/* Report header bar */} |
| <div style={{ background: '#000', color: '#fff', padding: '0.85rem 1.5rem', display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}> |
| <span style={{ fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: '0.82rem', flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> |
| 📄 {(reportResult.topic || reportTopic).toUpperCase()} |
| </span> |
| {/* Confidence badge */} |
| {reportResult.confidence !== undefined && ( |
| <span style={{ |
| background: confidenceBadgeColor(reportResult.confidence), |
| color: '#fff', |
| padding: '2px 10px', |
| fontFamily: 'var(--font-mono)', |
| fontSize: '0.72rem', |
| fontWeight: 700, |
| letterSpacing: '0.5px', |
| flexShrink: 0, |
| }}> |
| CONF: {(reportResult.confidence * 100).toFixed(0)}% |
| </span> |
| )} |
| {reportResult.tool_calls_made !== undefined && ( |
| <span style={{ background: '#333', color: '#fff', padding: '2px 10px', fontFamily: 'var(--font-mono)', fontSize: '0.72rem', fontWeight: 700, flexShrink: 0 }}> |
| {reportResult.tool_calls_made} CALLS |
| </span> |
| )} |
| {/* Actions */} |
| <button |
| onClick={handleCopyReport} |
| style={{ background: copyDone ? '#16a34a' : 'transparent', color: '#fff', border: '1px solid #555', padding: '3px 10px', fontFamily: 'var(--font-mono)', fontSize: '0.72rem', cursor: 'pointer', flexShrink: 0 }} |
| > |
| {copyDone ? '✓ COPIED' : '📋 COPY'} |
| </button> |
| <button |
| onClick={handleDownloadReport} |
| style={{ background: 'transparent', color: '#fff', border: '1px solid #555', padding: '3px 10px', fontFamily: 'var(--font-mono)', fontSize: '0.72rem', cursor: 'pointer', flexShrink: 0 }} |
| > |
| ⬇ .MD |
| </button> |
| </div> |
| |
| <div style={{ padding: '2rem' }}> |
| {/* Meta info row */} |
| <div style={{ display: 'flex', gap: '1.5rem', flexWrap: 'wrap', marginBottom: '2rem', paddingBottom: '1rem', borderBottom: '1px dotted #ddd', fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: '#888' }}> |
| {reportTimestamp && <div>Generated: {reportTimestamp.toLocaleString()}</div>} |
| {reportResult.tool_calls_made !== undefined && ( |
| <div>Depth: {reportDepth} · Tool calls: {reportResult.tool_calls_made}</div> |
| )} |
| {reportResult.confidence !== undefined && ( |
| <div style={{ color: confidenceBadgeColor(reportResult.confidence), fontWeight: 700 }}> |
| Confidence: {(reportResult.confidence * 100).toFixed(1)}% |
| </div> |
| )} |
| </div> |
| |
| {/* Executive summary */} |
| {reportResult.executive_summary && ( |
| <div style={{ marginBottom: '2.5rem', borderLeft: '4px solid #f59e0b', paddingLeft: '1.25rem' }}> |
| <h4 className="mono-text" style={{ fontSize: '0.72rem', marginBottom: '0.75rem', color: '#d97706', letterSpacing: '1.5px' }}> |
| EXECUTIVE SUMMARY |
| </h4> |
| <p style={{ fontFamily: 'var(--font-sans)', lineHeight: 1.9, fontSize: '1rem', fontWeight: 500, whiteSpace: 'pre-wrap', color: '#111' }}> |
| {reportResult.executive_summary} |
| </p> |
| </div> |
| )} |
| |
| {/* Sections */} |
| {reportResult.sections && typeof reportResult.sections === 'object' && !Array.isArray(reportResult.sections) ? ( |
| Object.entries(reportResult.sections).map(([title, content], i) => ( |
| <div key={i} style={{ marginBottom: '2rem', paddingBottom: '1.5rem', borderBottom: '1px dotted #e5e5e5' }}> |
| <h4 className="mono-text" style={{ fontSize: '0.8rem', marginBottom: '1rem', color: '#000', display: 'flex', alignItems: 'center', gap: '0.5rem' }}> |
| <span style={{ background: '#000', color: '#fff', padding: '2px 8px', fontSize: '0.65rem', flexShrink: 0 }}>{i + 1}</span> |
| {title.toUpperCase()} |
| </h4> |
| <p style={{ fontFamily: 'var(--font-sans)', lineHeight: 1.85, fontSize: '0.92rem', whiteSpace: 'pre-wrap', color: '#333' }}> |
| {String(content)} |
| </p> |
| </div> |
| )) |
| ) : ( |
| reportResult.sections?.map?.((s: any, i: number) => ( |
| <div key={i} style={{ marginBottom: '2rem', paddingBottom: '1.5rem', borderBottom: '1px dotted #e5e5e5' }}> |
| <h4 className="mono-text" style={{ fontSize: '0.8rem', marginBottom: '1rem', color: '#000', display: 'flex', alignItems: 'center', gap: '0.5rem' }}> |
| <span style={{ background: '#000', color: '#fff', padding: '2px 8px', fontSize: '0.65rem', flexShrink: 0 }}>{i + 1}</span> |
| {s.title?.toUpperCase()} |
| </h4> |
| <p style={{ fontFamily: 'var(--font-sans)', lineHeight: 1.85, fontSize: '0.92rem', whiteSpace: 'pre-wrap', color: '#333' }}> |
| {s.content} |
| </p> |
| </div> |
| )) |
| )} |
| |
| {!reportResult.sections && ( |
| <p style={{ fontFamily: 'var(--font-sans)', lineHeight: 1.85, whiteSpace: 'pre-wrap', color: '#333' }}> |
| {reportResult.report || reportResult.content || reportResult.markdown || JSON.stringify(reportResult, null, 2)} |
| </p> |
| )} |
| |
| {/* Key entities */} |
| {reportResult.key_entities && reportResult.key_entities.length > 0 && ( |
| <div style={{ marginTop: '2rem', paddingTop: '1rem', borderTop: '1px dotted #ccc' }}> |
| <h4 className="mono-text" style={{ fontSize: '0.68rem', color: '#888', marginBottom: '0.5rem', letterSpacing: '1.5px' }}> |
| KEY ENTITIES REFERENCED |
| </h4> |
| <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.4rem' }}> |
| {reportResult.key_entities.map((e: string, i: number) => ( |
| <span key={i} style={{ background: '#f3f4f6', border: '1.5px solid #e5e5e5', padding: '2px 8px', fontSize: '0.75rem', fontFamily: 'var(--font-mono)', fontWeight: 600 }}> |
| {e} |
| </span> |
| ))} |
| </div> |
| </div> |
| )} |
| </div> |
| </div> |
| )} |
| </div> |
| )} |
|
|
| {} |
| {activeTab === 'graph-update' && ( |
| <div> |
| <div className="page-info-bar"> |
| <GitCommit size={14}/> |
| <span><strong>LIVE GRAPH UPDATE</strong> — Paste any text (news article, note, policy). The system extracts entities and relationships and merges them into the live knowledge graph without re-running ingestion. Uses <strong>MERGE</strong> to prevent duplicates.</span> |
| </div> |
| <div style={{ border: '3px solid #000', padding: '2rem', marginBottom: '2rem' }}> |
| <h3 className="mono-text" style={{ marginBottom: '1.5rem' }}><GitCommit size={16} style={{ display: 'inline', marginRight: '0.5rem' }}/>INJECT KNOWLEDGE INTO GRAPH</h3> |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> |
| <div> |
| <label className="mono-text" style={{ fontSize: '0.8rem', fontWeight: 700, display: 'block', marginBottom: '0.5rem' }}>TEXT TO INJECT</label> |
| <textarea value={updateText} onChange={e => setUpdateText(e.target.value)} |
| rows={8} placeholder="Paste any text — news, reports, notes, or raw facts..." |
| style={{ width: '100%', border: '2px solid #000', padding: '0.75rem', fontFamily: 'var(--font-mono)', fontSize: '0.88rem', resize: 'vertical', boxSizing: 'border-box' }}/> |
| </div> |
| <button className="app-btn mono-text" onClick={runGraphUpdate} disabled={updateLoading || !updateText.trim()} |
| style={{ alignSelf: 'flex-start', padding: '0.75rem 2rem' }}> |
| {updateLoading ? 'INJECTING...' : 'INJECT INTO GRAPH'} |
| </button> |
| </div> |
| </div> |
| {updateResult && ( |
| <div style={{ border: '3px solid #000', padding: '1.5rem', background: '#f0fdf4' }}> |
| <h3 className="mono-text" style={{ marginBottom: '1rem', color: '#166534' }}>✓ GRAPH UPDATED</h3> |
| <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px,1fr))', gap: '0.75rem', marginBottom: '1rem' }}> |
| {[ |
| ['Entities Added', updateResult.entities_added ?? updateResult.nodes_created], |
| ['Relationships Added', updateResult.relationships_added ?? updateResult.edges_created], |
| ['Merged Existing', updateResult.entities_merged ?? '—'], |
| ].map(([label, val]) => ( |
| <div key={label as string} style={{ border: '2px solid #000', padding: '1rem', textAlign: 'center' }}> |
| <div className="mono-text" style={{ fontSize: '0.68rem', color: '#888', marginBottom: '0.35rem' }}>{label}</div> |
| <div className="mono-text" style={{ fontSize: '1.6rem', fontWeight: 900 }}>{val ?? '—'}</div> |
| </div> |
| ))} |
| </div> |
| {updateResult.message && ( |
| <p className="mono-text" style={{ fontSize: '0.82rem', color: '#555' }}>{updateResult.message}</p> |
| )} |
| </div> |
| )} |
| </div> |
| )} |
|
|
| {} |
| {activeTab === 'entity-chat' && ( |
| <div> |
| <div className="page-info-bar"> |
| <MessageSquare size={14}/> |
| <span><strong>ENTITY CHAT</strong> — Select an entity from the graph and interview it. The agent synthesizes answers from its neighborhood context, relationships, and LLM-generated profile.</span> |
| </div> |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: '1.5rem' }}> |
| {/* Config panel */} |
| <div style={{ border: '3px solid #000', padding: '1.5rem' }}> |
| <h3 className="mono-text" style={{ marginBottom: '1.25rem', fontSize: '0.9rem' }}>SELECT ENTITY</h3> |
| <div style={{ marginBottom: '1rem' }}> |
| <label className="mono-text" style={{ fontSize: '0.75rem', fontWeight: 700, color: '#888', display: 'block', marginBottom: '0.4rem' }}>ENTITY NAME</label> |
| <select value={selectedEntity} onChange={e => { setSelectedEntity(e.target.value); setChatHistory([]); }} |
| style={{ width: '100%', border: '2px solid #000', padding: '0.5rem', fontFamily: 'var(--font-mono)', fontSize: '0.85rem' }}> |
| <option value="">— Select entity —</option> |
| {entities.map((n: any) => ( |
| <option key={n.id} value={n.properties?.name || n.properties?.label || n.id}> |
| {n.labels?.[0]} · {n.properties?.name || n.properties?.label || n.id} |
| </option> |
| ))} |
| </select> |
| </div> |
| <div style={{ marginBottom: '1rem' }}> |
| <label className="mono-text" style={{ fontSize: '0.75rem', fontWeight: 700, color: '#888', display: 'block', marginBottom: '0.4rem' }}>EXTRA CONTEXT (OPTIONAL)</label> |
| <textarea value={entityContext} onChange={e => setEntityContext(e.target.value)} |
| rows={3} placeholder="Add any additional context or constraints..." |
| style={{ width: '100%', border: '2px solid #000', padding: '0.5rem', fontFamily: 'var(--font-mono)', fontSize: '0.82rem', resize: 'vertical' }}/> |
| </div> |
| <button className="app-btn mono-text" style={{ width: '100%', justifyContent: 'center' }} |
| onClick={() => setChatHistory([])}>CLEAR CHAT</button> |
| </div> |
| {/* Chat window */} |
| <div style={{ border: '3px solid #000', display: 'flex', flexDirection: 'column', minHeight: 400 }}> |
| <div style={{ background: '#000', color: '#fff', padding: '0.5rem 1rem', fontFamily: 'var(--font-mono)', fontSize: '0.78rem', fontWeight: 700 }}> |
| {selectedEntity ? `CHATTING WITH: ${selectedEntity.toUpperCase()}` : 'SELECT AN ENTITY TO BEGIN'} |
| </div> |
| <div style={{ flex: 1, overflowY: 'auto', padding: '1rem', display: 'flex', flexDirection: 'column', gap: '0.75rem', minHeight: 280, maxHeight: 400 }}> |
| {chatHistory.length === 0 && ( |
| <div style={{ textAlign: 'center', padding: '2rem', color: '#ccc', fontFamily: 'var(--font-mono)', fontSize: '0.82rem' }}> |
| {selectedEntity ? 'Ask this entity anything…' : 'Select an entity to start chatting'} |
| </div> |
| )} |
| {chatHistory.map((m, i) => ( |
| <div key={i} style={{ |
| alignSelf: m.role === 'user' ? 'flex-end' : 'flex-start', |
| background: m.role === 'user' ? '#000' : '#f3f4f6', |
| color: m.role === 'user' ? '#fff' : '#000', |
| padding: '0.6rem 1rem', |
| maxWidth: '85%', |
| fontFamily: 'var(--font-sans)', fontSize: '0.88rem', lineHeight: 1.6 |
| }}> |
| {m.content} |
| </div> |
| ))} |
| {chatLoading && ( |
| <div style={{ alignSelf: 'flex-start', background: '#f3f4f6', padding: '0.6rem 1rem', fontFamily: 'var(--font-mono)', fontSize: '0.82rem', color: '#888' }}> |
| Thinking… |
| </div> |
| )} |
| </div> |
| <div style={{ borderTop: '2px solid #000', display: 'flex', gap: 0 }}> |
| <input type="text" value={chatMsg} onChange={e => setChatMsg(e.target.value)} |
| onKeyDown={e => e.key === 'Enter' && !e.shiftKey && sendEntityChat()} |
| placeholder={selectedEntity ? 'Ask a question…' : 'Select an entity first'} |
| disabled={!selectedEntity || chatLoading} |
| style={{ flex: 1, border: 'none', padding: '0.75rem 1rem', fontFamily: 'var(--font-mono)', fontSize: '0.88rem', outline: 'none' }}/> |
| <button className="app-btn" onClick={sendEntityChat} disabled={!selectedEntity || !chatMsg.trim() || chatLoading} |
| style={{ border: 'none', borderLeft: '2px solid #000', borderRadius: 0, minWidth: 80 }}> |
| SEND |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| )} |
| </div> |
| ); |
| }; |
|
|
| export default InsightsView; |
|
|