Fundamental_Analysis/frontend/src/app/report/[symbol]/components/AnalysisModulesView.tsx
Lv, Qi 0cb31e363e Refactor E2E tests and improve error handling in Orchestrator
- 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).
2025-11-21 20:44:32 +08:00

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>;
}
}