- 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.
219 lines
9.5 KiB
TypeScript
219 lines
9.5 KiB
TypeScript
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>
|
||
)
|
||
}
|