Files
ai-scoring-application/app/results/page.tsx

678 lines
26 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.

"use client"
import { useState, useEffect } from "react"
import { useSearchParams } from "next/navigation"
import { Sidebar } from "@/components/sidebar"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell,
RadarChart,
PolarGrid,
PolarAngleAxis,
PolarRadiusAxis,
Radar,
} from "recharts"
import { Download, Share2, TrendingUp, AlertCircle, CheckCircle, Star } from "lucide-react"
import { useToast } from "@/hooks/use-toast"
import { ShareModal } from "@/components/share-modal"
// 模擬評分結果數據 - 使用您提到的例子8, 7, 6, 8, 4平均 = 6.6
const mockCriteria = [
{
name: "內容品質",
score: 8,
maxScore: 10,
weight: 25,
weightedScore: 20,
feedback: "內容結構清晰,資訊豐富且準確。建議增加更多實際案例來支撐論點。",
strengths: ["邏輯清晰", "資料準確", "結構完整"],
improvements: ["增加案例", "深化分析"],
},
{
name: "視覺設計",
score: 7,
maxScore: 10,
weight: 20,
weightedScore: 14,
feedback: "整體設計風格統一,色彩搭配合理。部分頁面文字密度過高,影響閱讀體驗。",
strengths: ["風格統一", "色彩協調", "版面整潔"],
improvements: ["減少文字密度", "增加視覺元素"],
},
{
name: "邏輯結構",
score: 6,
maxScore: 10,
weight: 20,
weightedScore: 12,
feedback: "邏輯架構清晰,各章節銜接自然,論述層次分明。",
strengths: ["邏輯清晰", "結構完整", "銜接自然"],
improvements: ["可增加總結回顧"],
},
{
name: "創新性",
score: 8,
maxScore: 10,
weight: 15,
weightedScore: 12,
feedback: "內容具有一定創新性,但可以更大膽地提出獨特觀點和解決方案。",
strengths: ["思路新穎", "角度獨特"],
improvements: ["增加創新元素", "提出獨特見解"],
},
{
name: "實用性",
score: 4,
maxScore: 10,
weight: 20,
weightedScore: 8,
feedback: "內容實用性有待提升,提供的解決方案需要更具可操作性。",
strengths: ["實用性強", "可操作性好", "價值明確"],
improvements: ["增加實施步驟"],
},
];
// 計算 mock 數據的 overview - 基於平均分作為閾值
const calculateMockOverview = (criteria: any[]) => {
if (!criteria || criteria.length === 0) {
return {
excellentItems: 0,
improvementItems: 0,
overallPerformance: 0
};
}
// 計算所有項目的平均分數(不考慮權重)
const totalScore = criteria.reduce((sum, item) => sum + item.score, 0);
const averageScore = totalScore / criteria.length;
// 以平均分作為閾值
// ≥ 平均分 = 優秀項目,< 平均分 = 待改進項目
const excellentItems = criteria.filter(item => item.score >= averageScore).length;
const improvementItems = criteria.filter(item => item.score < averageScore).length;
// 整體表現:基於權重的加權平均分數
const overallPerformance = Math.round(criteria.reduce((sum, item) => sum + (item.score / item.maxScore) * item.weight, 0));
return {
excellentItems,
improvementItems,
overallPerformance
};
};
const mockResults = {
projectTitle: "產品介紹簡報",
overallScore: 82,
totalPossible: 100,
grade: "B+",
analysisDate: "2024-01-15",
criteria: mockCriteria,
overview: calculateMockOverview(mockCriteria),
}
// 圖表數據將在組件內部動態生成
const COLORS = ["#0891b2", "#6366f1", "#f59e0b", "#dc2626", "#10b981"]
export default function ResultsPage() {
const [activeTab, setActiveTab] = useState("overview")
const [evaluationData, setEvaluationData] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [isShareModalOpen, setIsShareModalOpen] = useState(false)
const { toast } = useToast()
const searchParams = useSearchParams()
useEffect(() => {
const loadEvaluationData = async () => {
try {
setIsLoading(true)
setError(null)
// 檢查是否有 URL 參數中的評審 ID
const evaluationId = searchParams.get('id')
if (evaluationId) {
// 從 API 獲取評審數據
console.log(`📊 從 API 獲取評審數據: ID=${evaluationId}`)
const response = await fetch(`/api/evaluation/${evaluationId}`)
const result = await response.json()
if (result.success) {
setEvaluationData(result.data)
console.log('✅ 成功載入評審數據:', result.data.projectTitle)
} else {
throw new Error(result.error || '獲取評審數據失敗')
}
} else {
// 回退到從 localStorage 獲取評審結果
console.log('📊 從 localStorage 獲取評審結果')
const storedData = localStorage.getItem('evaluationResult')
if (storedData) {
const data = JSON.parse(storedData)
setEvaluationData(data)
console.log('✅ 成功載入 localStorage 數據')
} else {
throw new Error('沒有找到評審結果')
}
}
} catch (error) {
console.error('載入評審結果失敗:', error)
setError((error as Error).message)
toast({
title: "載入失敗",
description: (error as Error).message || "無法載入評審結果,請重新進行評審",
variant: "destructive",
})
} finally {
setIsLoading(false)
}
}
loadEvaluationData()
}, [searchParams, toast])
// 如果正在載入,顯示載入狀態
if (isLoading) {
return (
<div className="min-h-screen bg-background">
<Sidebar />
<main className="md:ml-64 p-6">
<div className="max-w-6xl mx-auto">
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground">...</p>
</div>
</div>
</div>
</main>
</div>
)
}
// 如果沒有數據或發生錯誤,顯示錯誤狀態
if (!evaluationData || error) {
return (
<div className="min-h-screen bg-background">
<Sidebar />
<main className="md:ml-64 p-6">
<div className="max-w-6xl mx-auto">
<div className="flex items-center justify-center h-64">
<div className="text-center">
<p className="text-muted-foreground mb-4">
{error ? `載入失敗: ${error}` : '沒有找到評審結果'}
</p>
<div className="space-x-2">
<Button onClick={() => window.location.href = '/upload'}>
</Button>
<Button variant="outline" onClick={() => window.location.href = '/history'}>
</Button>
</div>
</div>
</div>
</div>
</main>
</div>
)
}
// 使用真實數據或回退到模擬數據
const results = (evaluationData as any)?.evaluation?.fullData || evaluationData || mockResults
// 計算統計數據 - 基於 criteria_items 的平均分作為閾值
const calculateOverview = (criteria: any[]): any => {
if (!criteria || criteria.length === 0) {
return {
excellentItems: 0,
improvementItems: 0,
overallPerformance: 0
};
}
// 計算所有項目的平均分數(不考慮權重)
const totalScore = criteria.reduce((sum, item) => sum + item.score, 0);
const averageScore = totalScore / criteria.length;
// 以平均分作為閾值
// ≥ 平均分 = 優秀項目,< 平均分 = 待改進項目
const excellentItems = criteria.filter(item => item.score >= averageScore).length;
const improvementItems = criteria.filter(item => item.score < averageScore).length;
// 整體表現:基於權重的加權平均分數
const overallPerformance = Math.round(criteria.reduce((sum, item) => sum + (item.score / item.maxScore) * item.weight, 0));
return {
excellentItems,
improvementItems,
overallPerformance
};
};
// 確保所有必要的數據結構存在
const safeResults = {
...results,
overview: results.overview || calculateOverview(results.criteria || []),
chartData: results.chartData || {
barChart: [],
pieChart: [],
radarChart: []
},
improvementSuggestions: results.improvementSuggestions || {
overallSuggestions: '暫無改進建議',
maintainStrengths: [],
keyImprovements: [],
actionPlan: []
}
}
const downloadReport = async () => {
try {
// 顯示載入提示
toast({
title: "報告下載中",
description: "評審報告 PDF 正在生成,請稍候...",
});
// 獲取評審 ID
const evaluationId = searchParams.get('id');
if (!evaluationId) {
throw new Error('無法獲取評審 ID');
}
// 調用下載 API
const response = await fetch(`/api/evaluation/${evaluationId}/download`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || '下載失敗');
}
// 獲取 PDF Blob
const pdfBlob = await response.blob();
// 創建下載連結
const url = window.URL.createObjectURL(pdfBlob);
const link = document.createElement('a');
link.href = url;
// 從 Content-Disposition 標頭獲取檔案名稱,或使用預設名稱
const contentDisposition = response.headers.get('Content-Disposition');
let fileName = `評審報告_${safeResults.projectTitle}_${safeResults.analysisDate}.pdf`;
if (contentDisposition) {
const fileNameMatch = contentDisposition.match(/filename="(.+)"/);
if (fileNameMatch) {
fileName = decodeURIComponent(fileNameMatch[1]);
}
}
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
// 顯示成功提示
toast({
title: "下載完成",
description: "評審報告已成功下載",
});
} catch (error) {
console.error('下載失敗:', error);
toast({
title: "下載失敗",
description: error instanceof Error ? error.message : '下載過程中發生錯誤',
variant: "destructive",
});
}
}
const shareResults = () => {
setIsShareModalOpen(true)
}
const getScoreColor = (score: number, maxScore: number) => {
const percentage = (score / maxScore) * 100
if (percentage >= 90) return "text-green-600"
if (percentage >= 80) return "text-blue-600"
if (percentage >= 70) return "text-yellow-600"
if (percentage >= 60) return "text-orange-600"
return "text-red-600"
}
const getGradeColor = (grade: string) => {
if (grade.startsWith("A")) return "bg-green-100 text-green-800"
if (grade.startsWith("B")) return "bg-blue-100 text-blue-800"
if (grade.startsWith("C")) return "bg-yellow-100 text-yellow-800"
return "bg-red-100 text-red-800"
}
return (
<div className="min-h-screen bg-background">
<Sidebar />
<main className="md:ml-64 p-6">
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="mb-8 pt-8 md:pt-0">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-foreground mb-2 font-[var(--font-playfair)]"></h1>
<p className="text-muted-foreground">
{safeResults.projectTitle} - {safeResults.analysisDate}
</p>
</div>
<div className="flex gap-2">
<Button onClick={shareResults} variant="outline">
<Share2 className="h-4 w-4 mr-2" />
</Button>
<Button onClick={downloadReport}>
<Download className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
</div>
{/* Overall Score */}
<Card className="mb-8">
<CardContent className="pt-6">
<div className="grid md:grid-cols-4 gap-6">
<div className="text-center">
<div className="text-4xl font-bold text-primary mb-2">{safeResults.overallScore}</div>
<div className="text-sm text-muted-foreground"> / {safeResults.totalPossible}</div>
</div>
<div className="text-center">
<Badge className={`text-lg px-4 py-2 ${getGradeColor(safeResults.grade)}`}>{safeResults.grade}</Badge>
<div className="text-sm text-muted-foreground mt-2"></div>
</div>
<div className="text-center">
<div className="flex items-center justify-center mb-2">
<TrendingUp className="h-6 w-6 text-green-600" />
</div>
<div className="text-sm text-muted-foreground">{safeResults.performanceStatus}</div>
</div>
<div className="text-center">
<div className="flex items-center justify-center mb-2">
{Array.from({ length: safeResults.recommendedStars || 0 }, (_, i) => (
<Star key={i} className="h-6 w-6 text-yellow-500 fill-current" />
))}
</div>
<div className="text-sm text-muted-foreground"></div>
</div>
</div>
</CardContent>
</Card>
{/* Detailed Results */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="overview"></TabsTrigger>
<TabsTrigger value="detailed"></TabsTrigger>
<TabsTrigger value="charts"></TabsTrigger>
<TabsTrigger value="suggestions"></TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-6">
{/* Score Breakdown */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{safeResults.criteria.map((item: any, index: number) => (
<div key={index} className="flex items-center justify-between p-4 bg-muted rounded-lg">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h4 className="font-medium">{item.name}</h4>
<Badge variant="outline">{item.weight}% </Badge>
</div>
<Progress value={(item.score / item.maxScore) * 100} className="h-2" />
</div>
<div className="text-right ml-4">
<div className={`text-2xl font-bold ${getScoreColor(item.score, item.maxScore)}`}>
{item.score}
</div>
<div className="text-sm text-muted-foreground">/ {item.maxScore}</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* Quick Stats */}
<div className="grid md:grid-cols-3 gap-6">
<Card>
<CardContent className="pt-6 text-center">
<CheckCircle className="h-8 w-8 text-green-600 mx-auto mb-2" />
<div className="text-2xl font-bold text-green-600 mb-1">{safeResults.overview.excellentItems}</div>
<div className="text-sm text-muted-foreground"></div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6 text-center">
<AlertCircle className="h-8 w-8 text-yellow-600 mx-auto mb-2" />
<div className="text-2xl font-bold text-yellow-600 mb-1">{safeResults.overview.improvementItems}</div>
<div className="text-sm text-muted-foreground"></div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6 text-center">
<TrendingUp className="h-8 w-8 text-blue-600 mx-auto mb-2" />
<div className="text-2xl font-bold text-blue-600 mb-1">{safeResults.overview.overallPerformance}%</div>
<div className="text-sm text-muted-foreground"></div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="detailed" className="space-y-6">
{safeResults.criteria.map((item: any, index: number) => (
<Card key={index}>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
{item.name}
<Badge variant="outline">{item.weight}% </Badge>
</CardTitle>
<div className={`text-2xl font-bold ${getScoreColor(item.score, item.maxScore)}`}>
{item.score}/{item.maxScore}
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h4 className="font-medium mb-2">AI </h4>
<p className="text-muted-foreground leading-relaxed">{item.feedback}</p>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div>
<h4 className="font-medium mb-2 text-green-700"></h4>
<ul className="space-y-1">
{item.strengths.map((strength: any, i: number) => (
<li key={i} className="flex items-center gap-2 text-sm">
<CheckCircle className="h-4 w-4 text-green-600" />
{strength}
</li>
))}
</ul>
</div>
<div>
<h4 className="font-medium mb-2 text-orange-700"></h4>
<ul className="space-y-1">
{item.improvements.map((improvement: any, i: number) => (
<li key={i} className="flex items-center gap-2 text-sm">
<AlertCircle className="h-4 w-4 text-orange-600" />
{improvement}
</li>
))}
</ul>
</div>
</div>
</CardContent>
</Card>
))}
</TabsContent>
<TabsContent value="charts" className="space-y-6">
<div className="grid lg:grid-cols-2 gap-6">
{/* Bar Chart */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={safeResults.chartData.barChart}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" angle={-45} textAnchor="end" height={80} />
<YAxis domain={[0, 10]} />
<Tooltip />
<Bar dataKey="score" fill="#0891b2" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Pie Chart */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={safeResults.chartData.pieChart}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, weight }) => `${name} (${weight}%)`}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{safeResults.chartData.pieChart.map((entry: any, index: number) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
{/* Radar Chart */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={400}>
<RadarChart data={safeResults.chartData.radarChart}>
<PolarGrid />
<PolarAngleAxis dataKey="subject" />
<PolarRadiusAxis domain={[0, 10]} />
<Radar name="得分" dataKey="score" stroke="#0891b2" fill="#0891b2" fillOpacity={0.3} />
</RadarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="suggestions" className="space-y-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>{safeResults.improvementSuggestions.overallSuggestions}</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div>
<h4 className="font-medium mb-3 text-green-700"></h4>
<div className="grid md:grid-cols-2 gap-4">
{safeResults.improvementSuggestions.maintainStrengths.map((strength: any, index: number) => (
<div key={index} className="p-4 bg-green-50 rounded-lg">
<h5 className="font-medium mb-2">{strength.title}</h5>
<p className="text-sm text-muted-foreground">
{strength.description}
</p>
</div>
))}
</div>
</div>
<div>
<h4 className="font-medium mb-3 text-orange-700"></h4>
<div className="space-y-4">
{safeResults.improvementSuggestions.keyImprovements.map((improvement: any, index: number) => (
<div key={index} className="p-4 bg-orange-50 rounded-lg">
<h5 className="font-medium mb-2">{improvement.title}</h5>
<p className="text-sm text-muted-foreground mb-3">{improvement.description}</p>
<ul className="text-sm space-y-1">
{improvement.suggestions.map((suggestion: any, sIndex: number) => (
<li key={sIndex}> {suggestion}</li>
))}
</ul>
</div>
))}
</div>
</div>
<div>
<h4 className="font-medium mb-3 text-blue-700"></h4>
<div className="space-y-3">
{safeResults.improvementSuggestions.actionPlan.map((action: any, index: number) => (
<div key={index} className="flex items-start gap-3 p-3 bg-blue-50 rounded-lg">
<div className="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-bold">
{index + 1}
</div>
<div>
<h5 className="font-medium">{action.phase}</h5>
<p className="text-sm text-muted-foreground">{action.description}</p>
</div>
</div>
))}
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</main>
{/* Share Modal */}
<ShareModal
isOpen={isShareModalOpen}
onClose={() => setIsShareModalOpen(false)}
evaluationId={searchParams.get('id') || undefined}
projectTitle={safeResults.projectTitle}
/>
</div>
)
}