From 2d86c0fd618389cb9d2221fe5a9d4f08ddc82423 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 17 Oct 2025 14:05:26 +0300 Subject: [PATCH] state synchronization between backend agent and frontend --- examples/ag-ui-research/.env.example | 4 +- examples/ag-ui-research/backend/agent.py | 375 ++++++++- examples/ag-ui-research/backend/main.py | 85 +- examples/ag-ui-research/docker-compose.yml | 4 +- .../frontend/components/Agent.tsx | 80 +- .../frontend/components/StateDisplay.tsx | 740 ++++++++++++++---- 6 files changed, 1066 insertions(+), 222 deletions(-) diff --git a/examples/ag-ui-research/.env.example b/examples/ag-ui-research/.env.example index 20aca8f6..d5a1245f 100644 --- a/examples/ag-ui-research/.env.example +++ b/examples/ag-ui-research/.env.example @@ -11,8 +11,8 @@ OLLAMA_BASE_URL=http://host.docker.internal:11434 # Path to the LanceDB database # For Docker: /app/data/haiku_rag.lancedb -# For local development: ./haiku_rag.lancedb -DB_PATH=haiku_rag.lancedb +# For local development: Use absolute path to existing database +DB_PATH=~/SOME_FOLDER/haiku.rag.lancedb # API keys (set as needed for your QA provider) # OPENAI_API_KEY=your-key-here diff --git a/examples/ag-ui-research/backend/agent.py b/examples/ag-ui-research/backend/agent.py index b3991e77..c273a4a3 100644 --- a/examples/ag-ui-research/backend/agent.py +++ b/examples/ag-ui-research/backend/agent.py @@ -1,10 +1,15 @@ """Pydantic AI research agent for haiku.rag with AG-UI protocol.""" +from __future__ import annotations + +from dataclasses import dataclass + from ag_ui.core import EventType, StateSnapshotEvent from pydantic import BaseModel from pydantic_ai import Agent, RunContext from pydantic_ai.ag_ui import StateDeps +from haiku.rag.client import HaikuRAG from haiku.rag.config import Config from haiku.rag.graph.common import get_model @@ -13,23 +18,41 @@ class ResearchState(BaseModel): """Shared state between research agent and frontend.""" question: str = "" - status: str = "idle" - current_iteration: int = 0 - max_iterations: int = 2 + phase: str = "idle" # idle|planning|searching|analyzing|evaluating|done + status: str = "" # Human-readable message + + # Research plan + plan: list[dict] = [] # [{id, question, status: pending|searching|done}] + current_question_index: int = 0 + + # Search results (live updates) + current_search: dict | None = ( + None # {query, type, results: [{chunk, score, expanded}]} + ) + + # Accumulated findings + insights: list[dict] = [] # [{summary, confidence, sources}] + + # Final output confidence: float = 0.0 - plan: list[dict] = [] - findings: list[dict] = [] final_report: dict | None = None -def _as_state_snapshot(ctx: RunContext[StateDeps[ResearchState]]) -> StateSnapshotEvent: - """Helper to create a state snapshot event for AG-UI.""" +@dataclass +class ResearchDeps(StateDeps[ResearchState]): + """Dependencies for the research agent with HaikuRAG client.""" + + client: HaikuRAG + + +def _as_state_snapshot(ctx: RunContext[ResearchDeps]) -> StateSnapshotEvent: + """Helper to create state snapshot event for AG-UI synchronization.""" return StateSnapshotEvent(type=EventType.STATE_SNAPSHOT, snapshot=ctx.deps.state) def create_agent( qa_provider: str = Config.QA_PROVIDER, qa_model: str = Config.QA_MODEL -) -> Agent[StateDeps[ResearchState], str]: +) -> Agent[ResearchDeps, str]: """Create and configure the research agent. Args: @@ -38,33 +61,327 @@ def create_agent( """ agent = Agent( model=get_model(qa_provider, qa_model), - deps_type=StateDeps[ResearchState], - instructions="""You are a research assistant powered by haiku.rag. + deps_type=ResearchDeps, + instructions="""You are a research co-pilot powered by haiku.rag. -You help users conduct deep research on complex questions by: -- Breaking down questions into sub-questions -- Searching through a knowledge base -- Evaluating findings for completeness and confidence -- Synthesizing comprehensive reports with citations +You work step-by-step with the user to conduct deep research on complex questions. -The state is shared with the frontend application, showing research progress in real-time. +Your workflow: +1. When user asks a question, propose a research plan (3-5 sub-questions) +2. Wait for user approval before proceeding +3. For each sub-question: + - Announce what you're searching for + - Execute search and show results with scores + - Extract insights from the results + - Ask user if they want to continue to next question +4. Evaluate overall confidence in your findings +5. Ask user if confident enough or should search more +6. Synthesize final report with citations -Currently, tools are placeholder stubs. Full integration with haiku.rag research pipeline -will be implemented in the next phase.""", +Be transparent: always announce what you're doing before you do it. +Show search scores, explain your reasoning, and involve the user in decisions. +""", ) @agent.tool - async def get_research_status(ctx: RunContext[StateDeps[ResearchState]]) -> dict: - """Get the current research state and progress.""" - return { - "question": ctx.deps.state.question, - "status": ctx.deps.state.status, - "iteration": ctx.deps.state.current_iteration, - "max_iterations": ctx.deps.state.max_iterations, - "confidence": ctx.deps.state.confidence, - "has_plan": len(ctx.deps.state.plan) > 0, - "findings_count": len(ctx.deps.state.findings), - "has_report": ctx.deps.state.final_report is not None, + async def propose_research_plan( + ctx: RunContext[ResearchDeps], question: str + ) -> StateSnapshotEvent: + """Propose a research plan by decomposing the question into sub-questions. + + Args: + question: The main research question to decompose + """ + # Update state with the question + ctx.deps.state.question = question + ctx.deps.state.phase = "planning" + ctx.deps.state.status = "Decomposing question into sub-questions..." + print( + f"[AGENT] Updated state: phase={ctx.deps.state.phase}, question={question}" + ) + + # Use LLM to decompose the question + decompose_prompt = f"""Break down this research question into 3-5 specific sub-questions that would help answer it comprehensively. + +Research Question: {question} + +Return ONLY a JSON array of sub-questions, like: ["Question 1?", "Question 2?", ...]""" + + response = await ctx.deps.client.ask(decompose_prompt) + + # Parse the response (simplified - assume it returns reasonable sub-questions) + import json + + try: + sub_questions = json.loads(response) + except json.JSONDecodeError: + # Fallback: split by newlines and clean up + sub_questions = [ + q.strip().lstrip("0123456789.-) ") + for q in response.split("\n") + if q.strip() + ][:5] + + # Create plan + plan = [ + {"id": i, "question": q, "status": "pending"} + for i, q in enumerate(sub_questions) + ] + + ctx.deps.state.plan = plan + ctx.deps.state.current_question_index = 0 + ctx.deps.state.status = f"Proposed plan with {len(plan)} sub-questions" + print(f"[AGENT] Plan created with {len(plan)} sub-questions") + print("[AGENT] Sending state snapshot to frontend") + + return _as_state_snapshot(ctx) + + @agent.tool + async def search_question( + ctx: RunContext[ResearchDeps], + question_id: int, + search_type: str = "hybrid", + ) -> StateSnapshotEvent: + """Execute search for a specific sub-question. + + Args: + question_id: ID of the sub-question from the plan + search_type: Type of search (hybrid, vector, or fts) + """ + # Get the question from plan + plan = ctx.deps.state.plan + if question_id >= len(plan): + raise ValueError(f"Question ID {question_id} not found in plan") + + question = plan[question_id]["question"] + + # Update state + ctx.deps.state.phase = "searching" + ctx.deps.state.current_question_index = question_id + ctx.deps.state.status = f"Searching: {question}" + plan[question_id]["status"] = "searching" + + # Execute search + search_results = await ctx.deps.client.search( + question, limit=5, search_type=search_type + ) + + # Expand context for top 3 results + if len(search_results) > 0: + # Get top 3 for context expansion + top_results = search_results[:3] + expanded_results = await ctx.deps.client.expand_context( + top_results, radius=2 + ) + + # Create a map of expanded chunks + expanded_map = { + chunk.id: (chunk, score) for chunk, score in expanded_results + } + else: + expanded_map = {} + + # Process results + results = [] + for chunk, score in search_results: + # Check if this chunk was expanded + if chunk.id in expanded_map: + expanded_chunk, _ = expanded_map[chunk.id] + result_data = { + "chunk": expanded_chunk.content[:500], # Truncate for display + "score": round(score, 3), + "source": chunk.document_title or chunk.document_uri or "Unknown", + "expanded": True, + } + else: + result_data = { + "chunk": chunk.content[:500], # Truncate for display + "score": round(score, 3), + "source": chunk.document_title or chunk.document_uri or "Unknown", + "expanded": False, + } + + results.append(result_data) + + # Update state + ctx.deps.state.current_search = { + "query": question, + "type": search_type, + "results": results, } + plan[question_id]["status"] = "done" + ctx.deps.state.status = f"Found {len(results)} results" + print("[AGENT] Search complete, sending state snapshot") + + return _as_state_snapshot(ctx) + + @agent.tool + async def extract_insights_from_results( + ctx: RunContext[ResearchDeps], + ) -> StateSnapshotEvent: + """Extract key insights from current search results.""" + current_search = ctx.deps.state.current_search + if not current_search: + raise ValueError("No current search results to analyze") + + # Update state + ctx.deps.state.phase = "analyzing" + ctx.deps.state.status = "Extracting insights from results..." + + # Build context from results + context = "\n\n".join( + [f"[Source: {r['source']}] {r['chunk']}" for r in current_search["results"]] + ) + + # Use LLM to extract insights + extract_prompt = f"""Analyze these search results and extract 1-3 key insights that help answer the question: "{current_search["query"]}" + +Search Results: +{context} + +Return a JSON array of insights with format: +[{{"summary": "brief insight", "confidence": 0.0-1.0, "sources": ["source1", "source2"]}}]""" + + response = await ctx.deps.client.ask(extract_prompt) + + # Parse insights + import json + + try: + new_insights = json.loads(response) + except json.JSONDecodeError: + # Fallback: create simple insight + new_insights = [ + { + "summary": response[:200], + "confidence": 0.7, + "sources": [r["source"] for r in current_search["results"][:3]], + } + ] + + # Add to accumulated insights + ctx.deps.state.insights.extend(new_insights) + + # Clear current search + ctx.deps.state.current_search = None + ctx.deps.state.status = f"Extracted {len(new_insights)} insights" + print("[AGENT] Insights extracted, sending state snapshot") + + return _as_state_snapshot(ctx) + + @agent.tool + async def evaluate_research_confidence( + ctx: RunContext[ResearchDeps], + ) -> StateSnapshotEvent: + """Evaluate overall confidence in the research findings.""" + insights = ctx.deps.state.insights + if not insights: + raise ValueError("No insights collected yet") + + # Update state + ctx.deps.state.phase = "evaluating" + ctx.deps.state.status = "Evaluating research confidence..." + + # Calculate confidence (simple average of insight confidences) + confidences = [i.get("confidence", 0.5) for i in insights] + overall_confidence = sum(confidences) / len(confidences) if confidences else 0 + + # Use LLM to evaluate completeness + eval_prompt = f"""Evaluate if these insights provide a confident answer to: "{ctx.deps.state.question}" + +Insights collected: +{chr(10).join([f"- {i['summary']}" for i in insights])} + +Assess: +1. Do we have enough information to answer the question? +2. What gaps remain? +3. Overall confidence (0.0-1.0) + +Return JSON: {{"confidence": 0.0-1.0, "gaps": ["gap1", "gap2"], "recommendation": "continue" or "finalize"}}""" + + response = await ctx.deps.client.ask(eval_prompt) + + # Parse evaluation + import json + + try: + evaluation = json.loads(response) + overall_confidence = evaluation.get("confidence", overall_confidence) + except json.JSONDecodeError: + evaluation = { + "confidence": overall_confidence, + "gaps": [], + "recommendation": "finalize" + if overall_confidence > 0.7 + else "continue", + } + + # Update state + ctx.deps.state.confidence = overall_confidence + ctx.deps.state.status = f"Confidence: {overall_confidence:.0%}" + print("[AGENT] Confidence evaluated, sending state snapshot") + + return _as_state_snapshot(ctx) + + @agent.tool + async def synthesize_final_report( + ctx: RunContext[ResearchDeps], + ) -> StateSnapshotEvent: + """Generate final research report with citations.""" + insights = ctx.deps.state.insights + if not insights: + raise ValueError("No insights to synthesize") + + # Update state + ctx.deps.state.phase = "synthesizing" + ctx.deps.state.status = "Generating final report..." + + # Build report prompt + report_prompt = f"""Generate a comprehensive research report answering: "{ctx.deps.state.question}" + +Based on these insights: +{chr(10).join([f"- {i['summary']} (sources: {', '.join(i.get('sources', [])[:2])})" for i in insights])} + +Create a structured report with: +- Executive Summary (2-3 sentences) +- Main Findings (bullet points) +- Conclusions +- Sources + +Return JSON with format: +{{ + "title": "...", + "summary": "...", + "findings": ["finding1", "finding2", ...], + "conclusions": ["conclusion1", ...], + "sources": ["source1", "source2", ...] +}}""" + + response = await ctx.deps.client.ask(report_prompt) + + # Parse report + import json + + try: + report = json.loads(response) + except json.JSONDecodeError: + # Fallback report + report = { + "title": ctx.deps.state.question, + "summary": response[:300], + "findings": [i["summary"] for i in insights], + "conclusions": ["See findings above"], + "sources": list( + set([s for i in insights for s in i.get("sources", [])]) + ), + } + + # Update state + ctx.deps.state.final_report = report + ctx.deps.state.phase = "done" + ctx.deps.state.status = "Research complete" + print("[AGENT] Report complete, sending state snapshot") + + return _as_state_snapshot(ctx) return agent diff --git a/examples/ag-ui-research/backend/main.py b/examples/ag-ui-research/backend/main.py index 3b37c213..86ec6dad 100644 --- a/examples/ag-ui-research/backend/main.py +++ b/examples/ag-ui-research/backend/main.py @@ -1,21 +1,64 @@ """Main entry point for the haiku.rag AG-UI research assistant backend.""" -from agent import ResearchState, create_agent -from pydantic_ai.ag_ui import StateDeps +import logging +import os +from contextlib import asynccontextmanager +from pathlib import Path + +from agent import ResearchDeps, ResearchState, create_agent from starlette.applications import Starlette from starlette.middleware import Middleware from starlette.middleware.cors import CORSMiddleware from starlette.responses import JSONResponse from starlette.routing import Mount, Route +from haiku.rag.client import HaikuRAG from haiku.rag.config import Config -# Create research agent instance using haiku.rag config +logger = logging.getLogger(__name__) + +# Global client instance +client: HaikuRAG | None = None + + +@asynccontextmanager +async def lifespan(app): + """Manage HaikuRAG client lifecycle.""" + global client + + # Get database path from environment or use default + db_path_str = os.getenv("DB_PATH", "haiku_rag.lancedb") + db_path = Path(db_path_str) + + if not db_path.exists(): + logger.error( + f"Database not found at {db_path}. Please initialize haiku.rag first." + ) + logger.error("Run: haiku-rag add ") + raise RuntimeError(f"Database not found: {db_path}") + + logger.info(f"Initializing HaikuRAG client with database: {db_path}") + client = HaikuRAG(db_path) + + logger.info("Research assistant backend ready") + logger.info(f"QA Provider: {Config.QA_PROVIDER}") + logger.info(f"QA Model: {Config.QA_MODEL}") + + yield + + # Cleanup + if client: + logger.info("Closing HaikuRAG client") + client.close() + + +# Create research agent instance agent = create_agent() async def health(request): """Health check endpoint.""" + db_path_str = os.getenv("DB_PATH", "haiku_rag.lancedb") return JSONResponse( { "status": "healthy", @@ -23,18 +66,47 @@ async def health(request): "qa_provider": Config.QA_PROVIDER, "qa_model": Config.QA_MODEL, "ollama_base_url": Config.OLLAMA_BASE_URL, + "db_path": db_path_str, + "db_exists": Path(db_path_str).exists(), } ) -# Convert PydanticAI agent to AG-UI compatible ASGI app -ag_ui_app = agent.to_ag_ui(deps=StateDeps(ResearchState())) # type: ignore[arg-type] +# Create AG-UI app once with the client +# State will be managed per-session by AG-UI +ag_ui_app = None + + +def get_ag_ui_app(): + """Get or create AG-UI app.""" + global ag_ui_app + if ag_ui_app is None and client is not None: + if client is None: + raise RuntimeError("Client not initialized") + + # Create deps with shared client but new state per session + research_deps = ResearchDeps(client=client, state=ResearchState()) + logger.info("Creating AG-UI app with initial state") + ag_ui_app = agent.to_ag_ui(deps=research_deps) + return ag_ui_app + + +# Initialize AG-UI app after client is ready in lifespan +async def agent_endpoint(scope, receive, send): + """Proxy requests to AG-UI app.""" + app = get_ag_ui_app() + if app is None: + response = JSONResponse({"error": "Client not initialized"}, status_code=503) + await response(scope, receive, send) + return + await app(scope, receive, send) + # Mount the AG-UI app at /agent and add health endpoint app = Starlette( routes=[ Route("/health", health), - Mount("/agent", ag_ui_app), + Mount("/agent", agent_endpoint), ], middleware=[ Middleware( @@ -45,6 +117,7 @@ app = Starlette( allow_headers=["*"], ) ], + lifespan=lifespan, ) if __name__ == "__main__": diff --git a/examples/ag-ui-research/docker-compose.yml b/examples/ag-ui-research/docker-compose.yml index 96be11ad..ce45776f 100644 --- a/examples/ag-ui-research/docker-compose.yml +++ b/examples/ag-ui-research/docker-compose.yml @@ -8,14 +8,14 @@ services: environment: - QA_PROVIDER=${QA_PROVIDER:-ollama} - QA_MODEL=${QA_MODEL:-gpt-oss:latest} - - DB_PATH=${DB_PATH:-/app/data/haiku_rag.lancedb} + - DB_PATH=/app/data/haiku.rag.lancedb - OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://host.docker.internal:11434} - OPENAI_API_KEY=${OPENAI_API_KEY} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} volumes: - ./backend:/app - /app/.venv - - ./data:/app/data + - ${DB_PATH}:/app/data/haiku.rag.lancedb networks: - ag-ui-network extra_hosts: diff --git a/examples/ag-ui-research/frontend/components/Agent.tsx b/examples/ag-ui-research/frontend/components/Agent.tsx index 88160950..a7902872 100644 --- a/examples/ag-ui-research/frontend/components/Agent.tsx +++ b/examples/ag-ui-research/frontend/components/Agent.tsx @@ -11,13 +11,37 @@ import StateDisplay from "./StateDisplay"; interface ResearchState { question: string; + phase: string; // idle|planning|searching|analyzing|evaluating|done status: string; - current_iteration: number; - max_iterations: number; + plan: Array<{ + id: number; + question: string; + status: string; // pending|searching|done + }>; + current_question_index: number; + current_search: { + query: string; + type: string; + results?: Array<{ + chunk: string; + score: number; + source: string; + expanded: boolean; + }>; + } | null; + insights: Array<{ + summary: string; + confidence: number; + sources: string[]; + }>; confidence: number; - plan: Array>; - findings: Array>; - final_report: Record | null; + final_report: { + title: string; + summary: string; + findings: string[]; + conclusions: string[]; + sources: string[]; + } | null; } function AgentContent() { @@ -26,20 +50,52 @@ function AgentContent() { name: "research_agent", initialState: { question: "", - status: "idle", - current_iteration: 0, - max_iterations: 2, - confidence: 0.0, + phase: "idle", + status: "", plan: [], - findings: [], + current_question_index: 0, + current_search: null, + insights: [], + confidence: 0.0, final_report: null, }, }); + // Log state changes + console.log("[FRONTEND] Current state:", state); + // Render state updates from the research agent useCoAgentStateRender({ name: "research_agent", render: ({ state: newState }) => { + console.log("[FRONTEND] State render update:", newState); + // Show different messages based on phase + let phaseMessage = ""; + switch (newState.phase) { + case "planning": + phaseMessage = "Planning research..."; + break; + case "searching": + phaseMessage = newState.current_search + ? `Searching: ${newState.current_search.query}` + : "Searching..."; + break; + case "analyzing": + phaseMessage = "Extracting insights..."; + break; + case "evaluating": + phaseMessage = `Evaluating confidence: ${(newState.confidence * 100).toFixed(0)}%`; + break; + case "synthesizing": + phaseMessage = "Generating final report..."; + break; + case "done": + phaseMessage = "Research complete!"; + break; + default: + phaseMessage = newState.status || "Ready"; + } + return (
- Research Update: Status: {newState.status}, - Iteration: {newState.current_iteration}/{newState.max_iterations}, - Confidence: {(newState.confidence * 100).toFixed(0)}% + Research Update: {phaseMessage}
); }, diff --git a/examples/ag-ui-research/frontend/components/StateDisplay.tsx b/examples/ag-ui-research/frontend/components/StateDisplay.tsx index 33a1e36f..686ec3e8 100644 --- a/examples/ag-ui-research/frontend/components/StateDisplay.tsx +++ b/examples/ag-ui-research/frontend/components/StateDisplay.tsx @@ -1,14 +1,40 @@ "use client"; +import { useState } from "react"; + interface ResearchState { question: string; + phase: string; status: string; - current_iteration: number; - max_iterations: number; + plan: Array<{ + id: number; + question: string; + status: string; + }>; + current_question_index: number; + current_search: { + query: string; + type: string; + results?: Array<{ + chunk: string; + score: number; + source: string; + expanded: boolean; + }>; + } | null; + insights: Array<{ + summary: string; + confidence: number; + sources: string[]; + }>; confidence: number; - plan: Array>; - findings: Array>; - final_report: Record | null; + final_report: { + title: string; + summary: string; + findings: string[]; + conclusions: string[]; + sources: string[]; + } | null; } interface StateDisplayProps { @@ -16,6 +42,33 @@ interface StateDisplayProps { } export default function StateDisplay({ state }: StateDisplayProps) { + const [expandedSections, setExpandedSections] = useState< + Record + >({ + plan: true, + search: true, + insights: true, + report: true, + }); + + const toggleSection = (section: string) => { + setExpandedSections((prev) => ({ + ...prev, + [section]: !prev[section], + })); + }; + + // Phase indicator + const phases = [ + "idle", + "planning", + "searching", + "analyzing", + "evaluating", + "done", + ]; + const currentPhaseIndex = phases.indexOf(state.phase); + return (
Research State -

- This state is shared between the research agent and the frontend via the - AG-UI protocol. -

-
+ {/* Phase Progress */} +
+
+ Progress +
+
+ {phases.slice(1).map((phase, idx) => ( +
+
+ {phase} +
+ {idx < phases.length - 2 && ( +
+ )} +
+ ))} +
+
+ + {/* Question */} + {state.question && (
- {state.question || "No question yet"} + {state.question}
+ )} + {/* Confidence Meter */} + {state.confidence > 0 && (
-
- Status -
-
- {state.status} -
+ Confidence
- -
+
- Confidence +
0.8 + ? "#48bb78" + : state.confidence > 0.5 + ? "#ed8936" + : "#f56565", + transition: "width 0.3s ease", + }} + />
0.8 - ? "#38a169" + ? "#48bb78" : state.confidence > 0.5 - ? "#d69e2e" - : "#e53e3e", + ? "#ed8936" + : "#f56565", }} > {(state.confidence * 100).toFixed(0)}%
+ )} -
-
0 && ( +
+ + {expandedSections.plan && (
- Progress + {state.plan.map((item) => ( +
+
+ {item.status === "done" + ? "✓" + : item.status === "searching" + ? "🔍" + : "⏳"} +
+
+
+ {item.question} +
+
+
+ ))}
-
- {state.current_iteration} / {state.max_iterations} -
-
- -
-
- Plan Items -
-
- {state.plan.length} -
-
- -
-
- Findings -
-
- {state.findings.length} -
-
+ )}
+ )} -
-
+
-
- {state.final_report ? "Ready" : "Not ready"} -
+ + Search Results: {state.current_search.query.substring(0, 50)}... + + {expandedSections.search ? "▼" : "▶"} + + {expandedSections.search && ( +
+ {state.current_search.results && ( +
+
+ Type: {state.current_search.type} |{" "} + {state.current_search.results.length} results +
+ {state.current_search.results.map((result, idx) => ( +
+
+ + {result.source} + +
+ {result.expanded && ( + + Expanded + + )} + 0.8 + ? "#48bb78" + : result.score > 0.6 + ? "#ed8936" + : "#a0aec0", + }} + > + {result.score.toFixed(2)} + +
+
+
+ {result.chunk}... +
+
+ ))} +
+ )} +
+ )}
-
+ )} + + {/* Insights */} + {state.insights.length > 0 && ( +
+ + {expandedSections.insights && ( +
+ {state.insights.map((insight, idx) => ( +
+
+ + {(insight.confidence * 100).toFixed(0)}% confidence + + + {insight.sources.length} sources + +
+
+ {insight.summary} +
+
+ ))} +
+ )} +
+ )} + + {/* Final Report */} + {state.final_report && ( +
+ + {expandedSections.report && ( +
+

+ {state.final_report.title} +

+
+

+ Executive Summary +

+

+ {state.final_report.summary} +

+
+
+

+ Main Findings +

+
    + {state.final_report.findings.map((finding) => ( +
  • + {finding} +
  • + ))} +
+
+
+

+ Conclusions +

+
    + {state.final_report.conclusions.map((conclusion) => ( +
  • + {conclusion} +
  • + ))} +
+
+
+

+ Sources +

+
+ {state.final_report.sources.map((source) => ( +
+ {source} +
+ ))} +
+
+
+ )} +
+ )}
); }