This commit is contained in:
MarkLo 2025-11-21 03:29:20 +08:00
parent 44fd92850d
commit cc47085cd4
5 changed files with 203 additions and 13 deletions

13
.gitignore vendored
View File

@ -1,11 +1,12 @@
.venv
results
env/
__pycache__/
.DS_Store
# Project specific
/results
*.csv
src/
*.json
*.log
*.db
# Evaluation results
eval_results/
eval_data/
*.egg-info/
.env

View File

@ -4,15 +4,20 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { AnalysisForm } from "@/components/analysis/AnalysisForm";
import { TradingDecision } from "@/components/analysis/TradingDecision";
import { AnalystReport } from "@/components/analysis/AnalystReport";
import { PriceChart } from "@/components/analysis/PriceChart";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { Button } from "@/components/ui/button";
import { useAnalysis } from "@/hooks/useAnalysis";
import { useAnalysisContext } from "@/context/AnalysisContext";
import type { AnalysisRequest } from "@/lib/types";
export default function AnalysisPage() {
const router = useRouter();
const { setAnalysisResult } = useAnalysisContext();
const { runAnalysis, loading, error, result } = useAnalysis();
const handleSubmit = async (request: AnalysisRequest) => {
@ -24,6 +29,13 @@ export default function AnalysisPage() {
}
};
const handleViewResults = () => {
if (result) {
setAnalysisResult(result);
router.push("/analysis/results");
}
};
return (
<div className="container mx-auto px-4 py-12">
<div className="max-w-6xl mx-auto space-y-8">
@ -49,6 +61,13 @@ export default function AnalysisPage() {
{result && !loading && (
<div className="space-y-8">
{/* 查看詳細結果按鈕 */}
<div className="flex justify-end">
<Button onClick={handleViewResults} size="lg" className="gap-2">
</Button>
</div>
{/* 價格圖表 */}
{result.price_data && result.price_stats && (
<PriceChart

View File

@ -0,0 +1,134 @@
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useAnalysisContext } from "@/context/AnalysisContext";
import { PriceChart } from "@/components/analysis/PriceChart";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { ArrowLeft } from "lucide-react";
const ANALYSTS = [
{ key: "market", label: "市場分析師", reportKey: "market_report" },
{ key: "social", label: "社群媒體分析師", reportKey: "sentiment_report" },
{ key: "news", label: "新聞分析師", reportKey: "news_report" },
{ key: "fundamentals", label: "基本面分析師", reportKey: "fundamentals_report" },
];
export default function AnalysisResultsPage() {
const router = useRouter();
const { analysisResult } = useAnalysisContext();
const [selectedAnalyst, setSelectedAnalyst] = useState("market");
// 如果沒有結果,重定向到分析頁面
useEffect(() => {
if (!analysisResult) {
router.push("/analysis");
}
}, [analysisResult, router]);
if (!analysisResult) {
return (
<div className="container mx-auto px-4 py-12">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4"></h1>
<p className="text-gray-600 mb-4"></p>
<Button onClick={() => router.push("/analysis")}>
</Button>
</div>
</div>
);
}
const currentAnalyst = ANALYSTS.find(a => a.key === selectedAnalyst);
const currentReport = analysisResult.reports?.[currentAnalyst?.reportKey || ""];
return (
<div className="container mx-auto px-4 py-12">
<div className="max-w-7xl mx-auto space-y-8">
{/* Header */}
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<div>
<h1 className="text-4xl font-bold mb-2">
{analysisResult.ticker}
</h1>
<p className="text-gray-600 dark:text-gray-400">
{analysisResult.analysis_date}
</p>
</div>
<Button
variant="outline"
onClick={() => router.push("/analysis")}
className="gap-2"
>
<ArrowLeft className="h-4 w-4" />
</Button>
</div>
{/* 分析師選擇 Tabs */}
<Tabs value={selectedAnalyst} onValueChange={setSelectedAnalyst} className="w-full">
<TabsList className="grid w-full grid-cols-2 md:grid-cols-4 h-auto gap-2">
{ANALYSTS.map(analyst => (
<TabsTrigger
key={analyst.key}
value={analyst.key}
className="text-sm md:text-base py-2"
>
{analyst.label}
</TabsTrigger>
))}
</TabsList>
{ANALYSTS.map(analyst => (
<TabsContent key={analyst.key} value={analyst.key} className="mt-6">
<div className="space-y-6">
{/* 價格圖表 - 每個分析師都有 */}
{analysisResult.price_data && analysisResult.price_stats && (
<PriceChart
priceData={analysisResult.price_data}
priceStats={analysisResult.price_stats}
ticker={analysisResult.ticker}
/>
)}
{/* 分析師報告 */}
<Card>
<CardHeader>
<CardTitle>{analyst.label} </CardTitle>
<CardDescription>
{analyst.key === "market" && "技術分析與市場趨勢評估"}
{analyst.key === "social" && "社群情緒與市場氛圍分析"}
{analyst.key === "news" && "新聞事件與影響分析"}
{analyst.key === "fundamentals" && "財務數據與基本面分析"}
</CardDescription>
</CardHeader>
<CardContent>
{currentReport ? (
<div className="prose prose-sm max-w-none dark:prose-invert">
<pre className="whitespace-pre-wrap font-sans text-sm leading-relaxed">
{currentReport}
</pre>
</div>
) : (
<div className="text-center py-8">
<p className="text-gray-500 dark:text-gray-400">
</p>
<p className="text-sm text-gray-400 dark:text-gray-500 mt-2">
</p>
</div>
)}
</CardContent>
</Card>
</div>
</TabsContent>
))}
</Tabs>
</div>
</div>
);
}

View File

@ -3,6 +3,7 @@ import { Inter } from "next/font/google";
import "./globals.css";
import { Header } from "@/components/layout/Header";
import { Footer } from "@/components/layout/Footer";
import { AnalysisProvider } from "@/context/AnalysisContext";
const inter = Inter({ subsets: ["latin"] });
@ -19,13 +20,15 @@ export default function RootLayout({
return (
<html lang="en">
<body className={inter.className}>
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-1">
{children}
</main>
<Footer />
</div>
<AnalysisProvider>
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-1">
{children}
</main>
<Footer />
</div>
</AnalysisProvider>
</body>
</html>
);

View File

@ -0,0 +1,33 @@
"use client";
import { createContext, useContext, useState, ReactNode } from "react";
import type { AnalysisResponse } from "@/lib/types";
interface AnalysisContextType {
analysisResult: AnalysisResponse | null;
setAnalysisResult: (result: AnalysisResponse | null) => void;
}
const AnalysisContext = createContext<AnalysisContextType | undefined>(
undefined
);
export function AnalysisProvider({ children }: { children: ReactNode }) {
const [analysisResult, setAnalysisResult] = useState<AnalysisResponse | null>(
null
);
return (
<AnalysisContext.Provider value={{ analysisResult, setAnalysisResult }}>
{children}
</AnalysisContext.Provider>
);
}
export function useAnalysisContext() {
const context = useContext(AnalysisContext);
if (!context) {
throw new Error("useAnalysisContext must be used within AnalysisProvider");
}
return context;
}