import React, { useState, useRef, useEffect, useCallback } from 'react'; import { useAuth } from '../context/AuthContext'; import type { Message, Conversation, DocumentInfo, GraphNode } from '../types/api'; import { MessageSquare, Send, Bot, User as UserIcon, Zap, Menu, Info, X, ChevronDown, FileText, Plus } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api'; /* ── confidence colour helper ─────────────────────────────────────────────── */ const confColor = (c: number) => c >= 0.75 ? '#16a34a' : c >= 0.5 ? '#d97706' : '#dc2626'; const riskColor = (r: string) => r.toLowerCase() === 'high' ? '#dc2626' : r.toLowerCase() === 'medium' ? '#d97706' : '#16a34a'; /* ─────────────────────────────────────────────────────────────────────────── */ const InteractionView: React.FC = () => { const { token, logout } = useAuth(); // ── Core chat state ────────────────────────────────────────────────────── const [query, setQuery] = useState(''); const [conversation, setConversation] = useState([]); const [loading, setLoading] = useState(false); // ── Document / mode state ──────────────────────────────────────────────── const [documents, setDocuments] = useState([]); const [selectedDocId, setSelectedDocId] = useState(''); const [mode, setMode] = useState('auto'); const [agentId, setAgentId] = useState(''); const [agentNodes, setAgentNodes] = useState([]); // ── Thread history ──────────────────────────────────────────────────────── const [pastConversations, setPastConversations] = useState([]); const [currentConversationId, setCurrentConversationId] = useState(null); // ── UI state ────────────────────────────────────────────────────────────── const [sidebarOpen, setSidebarOpen] = useState(true); const [drawerSource, setDrawerSource] = useState(null); const endOfChatRef = useRef(null); const inputRef = useRef(null); /* ── auto-scroll ─────────────────────────────────────────────────────── */ useEffect(() => { endOfChatRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [conversation]); /* ── initial data fetch ──────────────────────────────────────────────── */ useEffect(() => { const fetchDocs = async () => { try { const res = await fetch(`${API_BASE}/documents`, { headers: { Authorization: `Bearer ${token}` } }); if (res.ok) setDocuments((await res.json()).documents); } catch {} }; const fetchConvs = async () => { try { const res = await fetch(`${API_BASE}/conversations`, { headers: { Authorization: `Bearer ${token}` } }); if (res.ok) setPastConversations((await res.json()).conversations); } catch {} }; const fetchAgents = async () => { try { const res = await fetch(`${API_BASE}/graph/visualization?limit=500`, { headers: { Authorization: `Bearer ${token}` } }); if (res.ok) setAgentNodes((await res.json()).nodes); } catch {} }; fetchDocs(); fetchConvs(); fetchAgents(); }, [token]); /* ── load an archived thread ─────────────────────────────────────────── */ const loadConversation = useCallback(async (convId: string) => { try { const res = await fetch(`${API_BASE}/conversations/${convId}`, { headers: { Authorization: `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setCurrentConversationId(data.id); setConversation( data.messages.map((m: any) => ({ role: m.role, content: m.content, reasoning: m.reasoning || [], sources: m.sources || [] })) ); // On mobile: close sidebar after selecting if (window.innerWidth < 768) setSidebarOpen(false); } } catch {} }, [token]); const startNewConversation = useCallback(() => { setCurrentConversationId(null); setConversation([]); if (window.innerWidth < 768) setSidebarOpen(false); setTimeout(() => inputRef.current?.focus(), 100); }, []); /* ── submit query ────────────────────────────────────────────────────── */ const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!query.trim() || loading) return; const userMessage: Message = { role: 'user', content: query }; const assistantPlaceholder: Message = { role: 'assistant', content: '', sources: [], reasoning: [], confidence: null, drift_expanded: false }; setConversation(prev => [...prev, userMessage, assistantPlaceholder]); setQuery(''); setLoading(true); try { /* ── Simulation mode ──────────────────────────────────────────── */ if (mode === 'simulation') { const res = await fetch(`${API_BASE}/v1/simulation/interview`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ agent_id: agentId, user_query: userMessage.content }) }); if (!res.ok) throw new Error('Simulation endpoint failed.'); const data = await res.json(); setConversation(prev => { const next = [...prev]; next[next.length - 1] = { role: 'assistant', content: data.response, sources: [], reasoning: [`Simulated persona response for agent: ${data.agent_name || agentId}`], confidence: null }; return next; }); setLoading(false); return; } /* ── RAG streaming mode ─────────────────────────────────────────── */ const reqBody: any = { query: userMessage.content, streaming: true, mode: mode, }; if (selectedDocId) reqBody.document_id = selectedDocId; if (currentConversationId) reqBody.conversation_id = currentConversationId; const res = await fetch(`${API_BASE}/query`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify(reqBody) }); if (res.status === 401) { logout(); return; } if (!res.body) throw new Error('ReadableStream not supported.'); const reader = res.body.getReader(); const decoder = new TextDecoder('utf-8'); while (true) { const { done, value } = await reader.read(); if (done) break; const raw = decoder.decode(value); const chunks = raw.split('\n\n'); for (const chunk of chunks) { if (chunk.trim() === 'data: [DONE]') { setLoading(false); break; } if (!chunk.startsWith('data: ')) continue; try { const data = JSON.parse(chunk.replace('data: ', '')); if (data.type === 'meta') { setCurrentConversationId(data.conversation_id); // Refresh thread list so it shows up in sidebar const convRes = await fetch(`${API_BASE}/conversations`, { headers: { Authorization: `Bearer ${token}` } }); if (convRes.ok) setPastConversations((await convRes.json()).conversations); continue; } setConversation(prev => { const next = [...prev]; const last = next.length - 1; if (data.type === 'step') { next[last] = { ...next[last], reasoning: [...(next[last].reasoning || []), data.content] }; } else if (data.type === 'answer') { next[last] = { ...next[last], content: data.answer, sources: data.sources, confidence: data.confidence, drift_expanded: data.drift_expanded || false, hallucination_risk: data.hallucination_risk, confidence_reasoning: data.confidence_reasoning }; } else if (data.type === 'confidence_update') { next[last] = { ...next[last], confidence: data.confidence, hallucination_risk: data.hallucination_risk, confidence_reasoning: data.confidence_reasoning }; } return next; }); } catch {} } } } catch (err) { console.error('Query error:', err); // Show error in the placeholder message setConversation(prev => { const next = [...prev]; next[next.length - 1] = { ...next[next.length - 1], content: '⚠ An error occurred. Please check your connection or try again.' }; return next; }); } finally { setLoading(false); } }; /* ── inline eval ─────────────────────────────────────────────────────── */ const runInlineEval = async (msgIndex: number) => { const astMsg = conversation[msgIndex]; if (astMsg.role !== 'assistant') return; let question = 'Contextual Query'; for (let i = msgIndex - 1; i >= 0; i--) { if (conversation[i].role === 'user') { question = conversation[i].content; break; } } setConversation(prev => { const next = [...prev]; next[msgIndex] = { ...next[msgIndex], evaluating: true }; return next; }); try { const contexts = (astMsg.sources || []).map((s: any) => s.text || JSON.stringify(s)); const res = await fetch(`${API_BASE}/eval/score`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ question, answer: astMsg.content, contexts }) }); const evalData = res.ok ? await res.json() : null; setConversation(prev => { const next = [...prev]; next[msgIndex] = { ...next[msgIndex], evaluating: false, ...(evalData ? { eval_result: evalData } : {}) }; return next; }); } catch { setConversation(prev => { const next = [...prev]; next[msgIndex] = { ...next[msgIndex], evaluating: false }; return next; }); } }; /* ── keyboard shortcut: Ctrl+/ or Cmd+/ to focus input ─────────────── */ useEffect(() => { const handler = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === '/') { e.preventDefault(); inputRef.current?.focus(); } if (e.key === 'Escape' && drawerSource) setDrawerSource(null); }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, [drawerSource]); /* ────────────────────────────────────────────────────────────────────── */ return (
{/* ── Top header bar ──────────────────────────────────────────────── */}
{/* left: title + breadcrumb */}

AGENTIC INTERACTION

TERMINAL // LOGIC QUERY INTERFACE

{/* right: controls */}
{/* Document filter */}
{/* ── Info bar ────────────────────────────────────────────────────── */}
Standard (Graph Logic): multi-hop retrieval over the knowledge graph.   GoT: runs all search strategies in parallel, best for complex questions.   God-Mode: interviews a simulated AI persona by agent ID.   Press Ctrl+/ to focus input.
{/* ── Main layout ─────────────────────────────────────────────────── */}
{/* ── Sidebar ────────────────────────────────────────────────────── */}
ARCHIVED THREADS
{pastConversations.length === 0 ? (
No prior sequences
) : ( pastConversations.map(conv => (
loadConversation(conv.id)} >
{conv.title || 'Untitled thread'}
{new Date(conv.created_at).toLocaleDateString()}
)) )}
{/* ── Chat panel ──────────────────────────────────────────────────── */}
{/* messages */}
{conversation.length === 0 ? (
INITIALIZE QUERY SEQUENCE TO BEGIN GRAPH ANALYSIS…
Try: "Summarize the main entities in this document" Try: "What relationships exist between X and Y?" Try: "Find all mentions of [topic] and their context"
) : ( conversation.map((msg, idx) => (
{msg.role === 'user' ? : }
{/* message header */}
{msg.role === 'user' ? 'YOU' : 'GRAPH REASONING SYSTEM'} {msg.role === 'assistant' && msg.confidence != null && (
{msg.drift_expanded && ( DRIFT EXPANDED )} {(msg.confidence * 100).toFixed(0)}% CONF {msg.hallucination_risk && ( RISK: {msg.hallucination_risk.toUpperCase()} )}
)}
{/* reasoning steps */} {msg.role === 'assistant' && msg.reasoning && msg.reasoning.length > 0 && (
{msg.reasoning.map((step: string, si: number) => (
{si + 1} {step}
))}
)} {/* content */}
{msg.role === 'assistant' && msg.content === '' && loading && idx === conversation.length - 1 ? ( ██ ) : (
{msg.content}
)}
{/* sources */} {msg.sources && msg.sources.length > 0 && (
SOURCES:
{msg.sources .filter((v: any, i: number, a: any[]) => a.findIndex(t => (t.metadata?.file_name || t.document_id) === (v.metadata?.file_name || v.document_id)) === i ) .map((s: any, si: number) => ( ))}
{msg.role === 'assistant' && !msg.eval_result && ( )}
{/* eval results */} {msg.eval_result && (
EVALUATION RESULTS
{[ { label: 'OVERALL', value: msg.eval_result.overall_score ?? (msg.eval_result.faithfulness * 0.5 + (msg.eval_result.answer_relevancy || msg.eval_result.relevancy || 0) * 0.3 + (msg.eval_result.context_precision || msg.eval_result.precision || 0) * 0.2) }, { label: 'FAITHFULNESS', value: msg.eval_result.faithfulness }, { label: 'RELEVANCY', value: msg.eval_result.answer_relevancy ?? msg.eval_result.relevancy }, { label: 'PRECISION', value: msg.eval_result.context_precision ?? msg.eval_result.precision } ].map((m, mi) => { const val = typeof m.value === 'number' ? m.value : 0; const pct = Math.round(val * 100); return (
{m.label}
{pct}%
); })}
)}
)}
)) )}
{/* ── Input area ─────────────────────────────────────────────── */}
{/* Mode + agent ID row */}
{mode === 'simulation' && (
OR setAgentId(e.target.value)} placeholder="Paste UUID..." className="iv-agent-input" style={{ width: '120px' }} />
)} {/* Document scope chip (shows when doc selected) */} {selectedDocId && (
{documents.find(d => d.id === selectedDocId)?.filename?.substring(0, 24) || 'Filtered'}
)}
{/* Text input row */}
> setQuery(e.target.value)} disabled={loading || (mode === 'simulation' && !agentId)} placeholder={ mode === 'simulation' ? agentId ? 'INTERVIEW AGENT…' : 'ENTER AGENT ID ABOVE FIRST…' : 'ENTER QUERY DIRECTIVE…' } className="iv-text-input" autoComplete="off" />
{/* ── Source detail drawer ─────────────────────────────────────────── */} {drawerSource && (
setDrawerSource(null)}>
e.stopPropagation()}>

SOURCE DETAIL

DOCUMENT {drawerSource.metadata?.file_name || drawerSource.document_id || '—'}
RELEVANCE {drawerSource.score != null ? (drawerSource.score * 100).toFixed(1) + '%' : 'N/A'}
CHUNK ID {drawerSource.id || '—'}

{drawerSource.text || 'No text available.'}
)} {/* ── Scoped styles ────────────────────────────────────────────────── */}
); }; export default InteractionView;