Fundamental_Analysis/frontend/src/pages/Dashboard.tsx
Lv, Qi 03b53aed71 feat: Refactor Analysis Context Mechanism and Generic Worker
- Implemented Unified Context Mechanism (Task 20251127):
  - Decoupled intent (Module) from resolution (Orchestrator).
  - Added ContextResolver for resolving input bindings (Manual Glob/Auto LLM).
  - Added IOBinder for managing physical paths.
  - Updated GenerateReportCommand to support explicit input bindings and output paths.

- Refactored Report Worker to Generic Execution (Task 20251128):
  - Removed hardcoded financial DTOs and specific formatting logic.
  - Implemented Generic YAML-based context assembly for better LLM readability.
  - Added detailed execution tracing (Sidecar logs).
  - Fixed input data collision bug by using full paths as context keys.

- Updated Tushare Provider to support dynamic output paths.
- Updated Common Contracts with new configuration models.
2025-11-28 20:11:17 +08:00

219 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useMutation } from '@tanstack/react-query';
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { BarChart3, Search, Sparkles, Loader2, AlertCircle } from "lucide-react"
import { useAnalysisTemplates, useLlmProviders } from "@/hooks/useConfig"
import { client } from '@/api/client';
import { DataRequest } from '@/api/schema.gen';
import { z } from 'zod';
import { useToast } from "@/hooks/use-toast"
type DataRequestDTO = z.infer<typeof DataRequest>;
export function Dashboard() {
const navigate = useNavigate();
const { toast } = useToast();
const [symbol, setSymbol] = useState("");
const [market, setMarket] = useState("CN");
const [templateId, setTemplateId] = useState("");
const { data: templates, isLoading: isTemplatesLoading } = useAnalysisTemplates();
const { data: llmProviders } = useLlmProviders();
const [validationError, setValidationError] = useState<string | null>(null);
// Auto-select first template when loaded
useEffect(() => {
if (templates && Object.keys(templates).length > 0 && !templateId) {
setTemplateId(Object.keys(templates)[0]);
}
}, [templates, templateId]);
// Validate template against providers
useEffect(() => {
if (!templateId || !templates || !templates[templateId] || !llmProviders) {
setValidationError(null);
return;
}
const selectedTemplate = templates[templateId];
const missingConfigs: string[] = [];
Object.values(selectedTemplate.modules).forEach(module => {
const modelId = module.llm_config?.model_id;
if (modelId && llmProviders) {
const modelExists = Object.values(llmProviders).some(provider =>
provider.models.some(m => m.model_id === modelId)
);
if (!modelExists) {
missingConfigs.push(`Module '${module.name}': Model '${modelId}' not found in any active provider`);
}
}
});
if (missingConfigs.length > 0) {
setValidationError(missingConfigs.join("; "));
} else {
setValidationError(null);
}
}, [templateId, templates, llmProviders]);
const startWorkflowMutation = useMutation({
mutationFn: async (payload: DataRequestDTO) => {
return await client.start_workflow(payload);
},
onSuccess: (data) => {
navigate(`/report/${data.request_id}?symbol=${data.symbol}&market=${data.market}&templateId=${templateId}`);
},
onError: (error) => {
toast({
title: "Start Failed",
description: "Failed to start analysis workflow. Please try again.",
type: "error"
});
console.error("Workflow start error:", error);
}
});
const handleStart = () => {
if (!symbol || !templateId) return;
startWorkflowMutation.mutate({
symbol,
market,
template_id: templateId
});
};
return (
<div className="flex flex-col items-center justify-center min-h-[calc(100vh-4rem)] bg-muted/5 px-4">
<div className="w-full max-w-3xl space-y-8 text-center">
<div className="space-y-2">
<h1 className="text-4xl font-extrabold tracking-tight lg:text-5xl bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
Fundamental Analysis
</h1>
<p className="text-xl text-muted-foreground">
AI Agent
</p>
</div>
<Card className="w-full max-w-xl mx-auto shadow-lg border-primary/10">
<CardHeader>
<CardTitle className="flex items-center justify-center gap-2">
<Sparkles className="h-5 w-5 text-yellow-500" />
</CardTitle>
<CardDescription>
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-4 gap-4">
<div className="col-span-3 space-y-2 text-left">
<Label htmlFor="symbol"> (Symbol)</Label>
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
id="symbol"
placeholder="e.g. 600519.SS or AAPL"
className="pl-9"
value={symbol}
onChange={(e) => setSymbol(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && symbol && templateId) {
handleStart();
}
}}
/>
</div>
</div>
<div className="col-span-1 space-y-2 text-left">
<Label htmlFor="market"> (Market)</Label>
<Select value={market} onValueChange={setMarket}>
<SelectTrigger id="market">
<SelectValue placeholder="Market" />
</SelectTrigger>
<SelectContent>
<SelectItem value="CN">CN (A股)</SelectItem>
<SelectItem value="US">US ()</SelectItem>
<SelectItem value="HK">HK ()</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2 text-left">
<Label htmlFor="template"> (Template)</Label>
<Select value={templateId} onValueChange={setTemplateId} disabled={isTemplatesLoading}>
<SelectTrigger id="template">
<SelectValue placeholder={isTemplatesLoading ? "Loading templates..." : "Select a template"} />
</SelectTrigger>
<SelectContent>
{templates && Object.keys(templates).length > 0 ? (
Object.entries(templates).map(([id, t]) => (
<SelectItem key={id} value={id}>
{t.name}
</SelectItem>
))
) : (
<SelectItem value="loading" disabled>
{isTemplatesLoading ? "Loading..." : "No templates found"}
</SelectItem>
)}
</SelectContent>
</Select>
</div>
{validationError && (
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-md flex items-start gap-2 text-left">
<AlertCircle className="h-5 w-5 shrink-0 mt-0.5" />
<span>
<strong>Configuration Error:</strong> The selected template has invalid configurations.<br/>
{validationError.split('; ').map((err, i) => (
<span key={i} className="block"> {err}</span>
))}
</span>
</div>
)}
</CardContent>
<CardFooter>
<Button
size="lg"
className="w-full text-base"
onClick={handleStart}
disabled={!symbol || !templateId || isTemplatesLoading || startWorkflowMutation.isPending || !!validationError}
>
{startWorkflowMutation.isPending || isTemplatesLoading ?
<Loader2 className="mr-2 h-5 w-5 animate-spin" /> :
<BarChart3 className="mr-2 h-5 w-5" />
}
{startWorkflowMutation.isPending ? "启动中..." : "生成分析报告"}
</Button>
</CardFooter>
</Card>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 max-w-4xl mx-auto text-left">
<FeatureCard title="多源数据聚合" desc="集成 Tushare, Finnhub 等多个专业金融数据源。" />
<FeatureCard title="AI 驱动分析" desc="使用 GPT-4o 等大模型进行深度财务指标解读。" />
<FeatureCard title="可视化工作流" desc="全流程透明化,实时查看每个分析步骤的状态。" />
</div>
</div>
</div>
)
}
function FeatureCard({ title, desc }: { title: string, desc: string }) {
return (
<div className="p-4 rounded-lg border bg-card text-card-foreground shadow-sm">
<h3 className="font-semibold mb-1">{title}</h3>
<p className="text-sm text-muted-foreground">{desc}</p>
</div>
)
}