This commit is contained in:
parent
44fd92850d
commit
cc47085cd4
|
|
@ -1,11 +1,12 @@
|
||||||
.venv
|
.venv
|
||||||
results
|
# Project specific
|
||||||
env/
|
/results
|
||||||
__pycache__/
|
|
||||||
.DS_Store
|
|
||||||
*.csv
|
*.csv
|
||||||
src/
|
*.json
|
||||||
|
*.log
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# Evaluation results
|
||||||
eval_results/
|
eval_results/
|
||||||
eval_data/
|
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.env
|
.env
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,20 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import { AnalysisForm } from "@/components/analysis/AnalysisForm";
|
import { AnalysisForm } from "@/components/analysis/AnalysisForm";
|
||||||
import { TradingDecision } from "@/components/analysis/TradingDecision";
|
import { TradingDecision } from "@/components/analysis/TradingDecision";
|
||||||
import { AnalystReport } from "@/components/analysis/AnalystReport";
|
import { AnalystReport } from "@/components/analysis/AnalystReport";
|
||||||
import { PriceChart } from "@/components/analysis/PriceChart";
|
import { PriceChart } from "@/components/analysis/PriceChart";
|
||||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { useAnalysis } from "@/hooks/useAnalysis";
|
import { useAnalysis } from "@/hooks/useAnalysis";
|
||||||
|
import { useAnalysisContext } from "@/context/AnalysisContext";
|
||||||
import type { AnalysisRequest } from "@/lib/types";
|
import type { AnalysisRequest } from "@/lib/types";
|
||||||
|
|
||||||
export default function AnalysisPage() {
|
export default function AnalysisPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { setAnalysisResult } = useAnalysisContext();
|
||||||
const { runAnalysis, loading, error, result } = useAnalysis();
|
const { runAnalysis, loading, error, result } = useAnalysis();
|
||||||
|
|
||||||
const handleSubmit = async (request: AnalysisRequest) => {
|
const handleSubmit = async (request: AnalysisRequest) => {
|
||||||
|
|
@ -24,6 +29,13 @@ export default function AnalysisPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleViewResults = () => {
|
||||||
|
if (result) {
|
||||||
|
setAnalysisResult(result);
|
||||||
|
router.push("/analysis/results");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-12">
|
<div className="container mx-auto px-4 py-12">
|
||||||
<div className="max-w-6xl mx-auto space-y-8">
|
<div className="max-w-6xl mx-auto space-y-8">
|
||||||
|
|
@ -49,6 +61,13 @@ export default function AnalysisPage() {
|
||||||
|
|
||||||
{result && !loading && (
|
{result && !loading && (
|
||||||
<div className="space-y-8">
|
<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 && (
|
{result.price_data && result.price_stats && (
|
||||||
<PriceChart
|
<PriceChart
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import { Inter } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { Header } from "@/components/layout/Header";
|
import { Header } from "@/components/layout/Header";
|
||||||
import { Footer } from "@/components/layout/Footer";
|
import { Footer } from "@/components/layout/Footer";
|
||||||
|
import { AnalysisProvider } from "@/context/AnalysisContext";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
|
@ -19,13 +20,15 @@ export default function RootLayout({
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
<div className="flex flex-col min-h-screen">
|
<AnalysisProvider>
|
||||||
<Header />
|
<div className="flex flex-col min-h-screen">
|
||||||
<main className="flex-1">
|
<Header />
|
||||||
{children}
|
<main className="flex-1">
|
||||||
</main>
|
{children}
|
||||||
<Footer />
|
</main>
|
||||||
</div>
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</AnalysisProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue