Spaces:
Paused
Paused
| import React, { useState, useRef, useEffect } from 'react'; | |
| import { | |
| Search, Bot, Loader2, FileText, ExternalLink, | |
| Sparkles, RefreshCw, BookOpen, ChevronRight, | |
| ArrowRight, Globe, Database, Brain, CheckCircle, | |
| AlertCircle | |
| } from 'lucide-react'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Input } from '@/components/ui/input'; | |
| import { ScrollArea } from '@/components/ui/scroll-area'; | |
| import { Badge } from '@/components/ui/badge'; | |
| import { cn } from '@/lib/utils'; | |
| import { API_URL } from '@/config/api'; | |
| import ReactMarkdown from 'react-markdown'; | |
| interface ResearchStep { | |
| id: string; | |
| description: string; | |
| status: 'pending' | 'active' | 'completed' | 'failed'; | |
| timestamp: Date; | |
| details?: string; | |
| } | |
| export default function ResearchAgentWidget() { | |
| const [query, setQuery] = useState(''); | |
| const [isResearching, setIsResearching] = useState(false); | |
| const [steps, setSteps] = useState<ResearchStep[]>([]); | |
| const [streamedContent, setStreamedContent] = useState(''); | |
| const scrollRef = useRef<HTMLDivElement>(null); | |
| // Auto-scroll output | |
| useEffect(() => { | |
| if (scrollRef.current) { | |
| // scrollRef.current.scrollTop = scrollRef.current.scrollHeight; | |
| } | |
| }, [streamedContent]); | |
| const addStep = (desc: string, status: ResearchStep['status'] = 'active', details?: string) => { | |
| setSteps(prev => { | |
| // Mark previous active step as completed if exists | |
| const newSteps = prev.map(s => | |
| s.status === 'active' ? { ...s, status: 'completed' } : s | |
| ); | |
| return [...newSteps, { | |
| id: Date.now().toString() + Math.random(), | |
| description: desc, | |
| status, | |
| timestamp: new Date(), | |
| details | |
| } as ResearchStep]; | |
| }); | |
| }; | |
| const completeLastStep = () => { | |
| setSteps(prev => prev.map(s => | |
| s.status === 'active' ? { ...s, status: 'completed' } : s | |
| )); | |
| }; | |
| const failLastStep = () => { | |
| setSteps(prev => prev.map(s => | |
| s.status === 'active' ? { ...s, status: 'failed' } : s | |
| )); | |
| }; | |
| const startResearch = async () => { | |
| if (!query.trim()) return; | |
| setIsResearching(true); | |
| setSteps([]); | |
| setStreamedContent(''); | |
| try { | |
| addStep('Initializing research agent...', 'completed'); | |
| addStep(`Analyzing topic: "${query}"`); | |
| // Use the autonomous hybrid search endpoint | |
| // We frame it as a 'research' request which the backend should handle intelligently | |
| // or we use the generic query endpoint and let the agent decide | |
| const response = await fetch(`${API_URL}/api/mcp/autonomous/query`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| type: 'research', // Hint to the router | |
| params: { | |
| query: query, // Common param | |
| topic: query, // Specific param | |
| depth: 'deep', | |
| includeSources: true | |
| } | |
| }) | |
| }); | |
| if (!response.ok) throw new Error(`API Error: ${response.status}`); | |
| // Simulate progression if not streaming (since our current API is request/response) | |
| // In a real websocket setup, these would be events | |
| setTimeout(() => { | |
| if (isResearching) addStep('Querying internal knowledge base...', 'active', 'Vector Search + Graph Traversal'); | |
| }, 1000); | |
| setTimeout(() => { | |
| if (isResearching) addStep('Synthesizing intelligence...', 'active', 'GPU-accelerated context fusion'); | |
| }, 2500); | |
| const data = await response.json(); | |
| if (data.success && data.data) { | |
| completeLastStep(); | |
| addStep('Research completed', 'completed'); | |
| // Format output | |
| let content = ''; | |
| if (typeof data.data === 'string') { | |
| content = data.data; | |
| } else { | |
| // Try to format object nicely | |
| if (data.data.answer) content += `### Answer\n${data.data.answer}\n\n`; | |
| if (data.data.summary) content += `### Summary\n${data.data.summary}\n\n`; | |
| if (data.data.results && Array.isArray(data.data.results)) { | |
| content += `### Key Findings\n`; | |
| data.data.results.forEach((r: any) => { | |
| content += `- **${r.score?.toFixed(2) || '?'}**: ${r.content?.substring(0, 150)}...\n`; | |
| }); | |
| } | |
| if (!content) content = JSON.stringify(data.data, null, 2); | |
| } | |
| setStreamedContent(content); | |
| } else { | |
| throw new Error(data.error || 'No data returned'); | |
| } | |
| } catch (error: any) { | |
| console.error('Research failed:', error); | |
| failLastStep(); | |
| addStep('Error: ' + error.message, 'failed'); | |
| setStreamedContent(`### Research Failed\n\n${error.message}\n\nPlease ensure the backend is running and the GPU bridge is active.`); | |
| } finally { | |
| setIsResearching(false); | |
| } | |
| }; | |
| return ( | |
| <div className="h-full flex flex-col bg-card/80 backdrop-blur-sm border border-border/50 rounded-lg overflow-hidden shadow-lg"> | |
| {/* Header */} | |
| <div className="p-4 border-b border-border/50 bg-gradient-to-r from-purple-500/10 to-blue-500/5 flex items-center justify-between"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2 bg-gradient-to-br from-purple-500 to-blue-600 rounded-lg shadow-md"> | |
| <Bot className="w-5 h-5 text-white" /> | |
| </div> | |
| <div> | |
| <h2 className="font-bold text-foreground tracking-tight">Deep Research Agent</h2> | |
| <div className="flex items-center gap-2 text-[10px] font-mono text-muted-foreground"> | |
| <span className={cn("w-1.5 h-1.5 rounded-full", isResearching ? "bg-green-400 animate-pulse" : "bg-purple-400")} /> | |
| {isResearching ? "PROCESSING" : "IDLE"} • GPU ENABLED | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Main Content */} | |
| <div className="flex-1 flex overflow-hidden"> | |
| {/* Left Panel: Input & Process */} | |
| <div className="w-[350px] border-r border-border/50 flex flex-col bg-secondary/10"> | |
| <div className="p-4 border-b border-border/30"> | |
| <label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2 block"> | |
| Research Topic | |
| </label> | |
| <div className="flex gap-2"> | |
| <Input | |
| value={query} | |
| onChange={e => setQuery(e.target.value)} | |
| onKeyDown={e => e.key === 'Enter' && startResearch()} | |
| placeholder="E.g. Advanced Persistent Threats in 2025..." | |
| disabled={isResearching} | |
| className="bg-background/50 border-border/50 focus:ring-purple-500/50" | |
| /> | |
| <Button | |
| size="icon" | |
| onClick={startResearch} | |
| disabled={isResearching || !query.trim()} | |
| className="bg-purple-600 hover:bg-purple-700 shadow-sm shrink-0" | |
| > | |
| {isResearching ? <Loader2 className="w-4 h-4 animate-spin" /> : <Sparkles className="w-4 h-4" />} | |
| </Button> | |
| </div> | |
| </div> | |
| {/* Agent Thought Process */} | |
| <div className="flex-1 overflow-hidden flex flex-col"> | |
| <div className="p-2 px-4 bg-secondary/20 text-[10px] font-mono text-muted-foreground border-b border-border/30"> | |
| AGENT PROCESS | |
| </div> | |
| <ScrollArea className="flex-1 p-4"> | |
| <div className="space-y-4"> | |
| {steps.map((step, i) => ( | |
| <div key={step.id} className="flex gap-3 animate-in fade-in slide-in-from-left-2 duration-300"> | |
| <div className="flex flex-col items-center pt-1"> | |
| <div className={cn( | |
| "w-5 h-5 rounded-full flex items-center justify-center text-[10px] border shadow-sm transition-colors duration-500", | |
| step.status === 'completed' ? "bg-green-500/20 border-green-500/50 text-green-500" : | |
| step.status === 'active' ? "bg-purple-500/20 border-purple-500/50 text-purple-500 animate-pulse" : | |
| step.status === 'failed' ? "bg-red-500/20 border-red-500/50 text-red-500" : | |
| "bg-secondary border-muted text-muted-foreground" | |
| )}> | |
| {step.status === 'completed' ? <CheckCircle className="w-3 h-3" /> : | |
| step.status === 'failed' ? <AlertCircle className="w-3 h-3" /> : | |
| i + 1} | |
| </div> | |
| {i < steps.length - 1 && <div className="w-px h-full bg-border/50 my-1" />} | |
| </div> | |
| <div className="pb-2"> | |
| <p className={cn( | |
| "text-xs font-medium transition-colors", | |
| step.status === 'active' ? "text-purple-400" : "text-foreground" | |
| )}>{step.description}</p> | |
| {step.details && ( | |
| <p className="text-[10px] text-muted-foreground mt-0.5">{step.details}</p> | |
| )} | |
| <span className="text-[9px] text-muted-foreground/50 font-mono mt-1 block"> | |
| {step.timestamp.toLocaleTimeString()} | |
| </span> | |
| </div> | |
| </div> | |
| ))} | |
| {steps.length === 0 && ( | |
| <div className="text-center py-8 text-muted-foreground/40 text-xs italic"> | |
| Waiting for mission... | |
| </div> | |
| )} | |
| </div> | |
| </ScrollArea> | |
| </div> | |
| </div> | |
| {/* Right Panel: Results */} | |
| <div className="flex-1 flex flex-col bg-background/30"> | |
| <div className="p-2 px-4 bg-secondary/20 text-[10px] font-mono text-muted-foreground border-b border-border/30 flex justify-between items-center"> | |
| <span>INTELLIGENCE REPORT</span> | |
| {streamedContent && ( | |
| <Badge variant="outline" className="text-[9px] h-4 border-green-500/30 text-green-500 bg-green-500/5"> | |
| GENERATED | |
| </Badge> | |
| )} | |
| </div> | |
| <ScrollArea className="flex-1 p-8"> | |
| {streamedContent ? ( | |
| <div className="prose prose-sm dark:prose-invert max-w-none prose-headings:text-purple-400 prose-a:text-blue-400"> | |
| <ReactMarkdown>{streamedContent}</ReactMarkdown> | |
| </div> | |
| ) : ( | |
| <div className="h-full flex flex-col items-center justify-center text-muted-foreground/30"> | |
| <div className="w-20 h-20 bg-secondary/30 rounded-full flex items-center justify-center mb-4"> | |
| <Brain className="w-10 h-10" /> | |
| </div> | |
| <p className="text-sm font-medium">No intelligence generated yet</p> | |
| <p className="text-xs mt-1">Enter a topic to begin deep research</p> | |
| </div> | |
| )} | |
| </ScrollArea> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |