- Fix `simple_test_analysis` template in E2E test setup to align with Orchestrator's data fetch logic.
- Implement and verify additional E2E scenarios:
- Scenario C: Partial Provider Failure (verified error propagation fix in Orchestrator).
- Scenario D: Invalid Symbol input.
- Scenario E: Analysis Module failure.
- Update `WorkflowStateMachine::handle_report_failed` to correctly scope error broadcasting to the specific task instead of failing effectively silently or broadly.
- Update testing strategy documentation to reflect completed Phase 4 testing.
- Skip Scenario B (Orchestrator Restart) as persistence is not yet implemented (decision made to defer persistence).
143 lines
6.1 KiB
TypeScript
143 lines
6.1 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import ReactMarkdown from 'react-markdown';
|
|
import remarkGfm from 'remark-gfm';
|
|
import { TaskStatus } from '@/types/workflow';
|
|
import { AnalysisModuleConfig } from '@/types/index';
|
|
import { BrainCircuit, Terminal } from 'lucide-react';
|
|
|
|
interface AnalysisModulesViewProps {
|
|
taskStates: Record<string, TaskStatus>;
|
|
taskOutputs: Record<string, string>;
|
|
modulesConfig: Record<string, AnalysisModuleConfig>;
|
|
}
|
|
|
|
export function AnalysisModulesView({
|
|
taskStates,
|
|
taskOutputs,
|
|
modulesConfig
|
|
}: AnalysisModulesViewProps) {
|
|
// Identify analysis tasks based on the template config
|
|
// We assume task IDs in the DAG correspond to module IDs or follow a pattern
|
|
// For now, let's try to match tasks that are NOT fetch tasks
|
|
|
|
// If we have config, use it to drive tabs
|
|
const moduleIds = Object.keys(modulesConfig);
|
|
|
|
const [activeModuleId, setActiveModuleId] = useState<string>(moduleIds[0] || '');
|
|
|
|
useEffect(() => {
|
|
// If no active module and we have modules, select first
|
|
if (!activeModuleId && moduleIds.length > 0) {
|
|
setActiveModuleId(moduleIds[0]);
|
|
}
|
|
}, [moduleIds, activeModuleId]);
|
|
|
|
if (moduleIds.length === 0) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-[300px] border-dashed border-2 rounded-lg text-muted-foreground">
|
|
<BrainCircuit className="w-10 h-10 mb-2 opacity-50" />
|
|
<p>No analysis modules defined in this template.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<Tabs value={activeModuleId} onValueChange={setActiveModuleId} className="w-full">
|
|
<div className="overflow-x-auto pb-2">
|
|
<TabsList className="w-full justify-start h-auto p-1 bg-transparent gap-2">
|
|
{moduleIds.map(moduleId => {
|
|
const config = modulesConfig[moduleId];
|
|
// Task ID might match module ID directly or be prefixed
|
|
// Heuristic: check exact match first
|
|
const taskId = moduleId;
|
|
const status = taskStates[taskId] || 'pending';
|
|
|
|
return (
|
|
<TabsTrigger
|
|
key={moduleId}
|
|
value={moduleId}
|
|
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground px-4 py-2 rounded-md border bg-card hover:bg-accent/50 transition-all"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<span>{config.name}</span>
|
|
<StatusDot status={status} />
|
|
</div>
|
|
</TabsTrigger>
|
|
);
|
|
})}
|
|
</TabsList>
|
|
</div>
|
|
|
|
{moduleIds.map(moduleId => {
|
|
const taskId = moduleId;
|
|
const output = taskOutputs[taskId] || '';
|
|
const status = taskStates[taskId] || 'pending';
|
|
const config = modulesConfig[moduleId];
|
|
|
|
return (
|
|
<TabsContent key={moduleId} value={moduleId} className="mt-0">
|
|
<Card className="h-[600px] flex flex-col">
|
|
<CardHeader className="py-4 border-b bg-muted/5">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<CardTitle className="text-lg">{config.name}</CardTitle>
|
|
<Badge variant="outline" className="font-mono text-xs">
|
|
{config.model_id}
|
|
</Badge>
|
|
</div>
|
|
<StatusBadge status={status} />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="flex-1 p-0 min-h-0 relative">
|
|
<ScrollArea className="h-full p-6">
|
|
{output ? (
|
|
<div className="prose dark:prose-invert max-w-none pb-10">
|
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
|
{output}
|
|
</ReactMarkdown>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col items-center justify-center h-full text-muted-foreground gap-2 opacity-50">
|
|
<Terminal className="w-8 h-8" />
|
|
<p>{status === 'running' ? 'Generating analysis...' : 'Waiting for input...'}</p>
|
|
</div>
|
|
)}
|
|
</ScrollArea>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
);
|
|
})}
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StatusDot({ status }: { status: TaskStatus }) {
|
|
let colorClass = "bg-muted";
|
|
if (status === 'completed') colorClass = "bg-green-500";
|
|
if (status === 'failed') colorClass = "bg-red-500";
|
|
if (status === 'running') colorClass = "bg-blue-500 animate-pulse";
|
|
|
|
return <div className={`w-2 h-2 rounded-full ${colorClass}`} />;
|
|
}
|
|
|
|
function StatusBadge({ status }: { status: TaskStatus }) {
|
|
switch (status) {
|
|
case 'completed':
|
|
return <Badge variant="outline" className="text-green-600 border-green-200 bg-green-50">Completed</Badge>;
|
|
case 'failed':
|
|
return <Badge variant="destructive">Failed</Badge>;
|
|
case 'running':
|
|
return <Badge variant="secondary" className="text-blue-600 bg-blue-50 animate-pulse">Generating...</Badge>;
|
|
default:
|
|
return <Badge variant="outline">Pending</Badge>;
|
|
}
|
|
}
|
|
|