Files
ai-scoring-application/app/results/page.tsx
2025-09-23 18:15:25 +08:00

580 lines
23 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 { 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"
// 模擬評分結果數據 - 使用您提到的例子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 { toast } = useToast()
useEffect(() => {
// 從 localStorage 獲取評審結果
const storedData = localStorage.getItem('evaluationResult')
if (storedData) {
try {
const data = JSON.parse(storedData)
setEvaluationData(data)
} catch (error) {
console.error('解析評審結果失敗:', error)
toast({
title: "數據錯誤",
description: "無法載入評審結果,請重新進行評審",
variant: "destructive",
})
}
} else {
toast({
title: "無評審結果",
description: "請先進行評審以查看結果",
variant: "destructive",
})
}
setIsLoading(false)
}, [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) {
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"></p>
<Button onClick={() => window.location.href = '/upload'}>
</Button>
</div>
</div>
</div>
</main>
</div>
)
}
// 使用真實數據或回退到模擬數據
const results = evaluationData.evaluation?.fullData || mockResults
// 計算統計數據 - 基於 criteria_items 的平均分作為閾值
const calculateOverview = (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 safeResults = {
...results,
overview: results.overview || calculateOverview(results.criteria || []),
chartData: results.chartData || {
barChart: [],
pieChart: [],
radarChart: []
},
improvementSuggestions: results.improvementSuggestions || {
overallSuggestions: '暫無改進建議',
maintainStrengths: [],
keyImprovements: [],
actionPlan: []
}
}
const downloadReport = () => {
toast({
title: "報告下載中",
description: "評審報告 PDF 正在生成,請稍候...",
})
}
const shareResults = () => {
toast({
title: "分享連結已複製",
description: "評審結果分享連結已複製到剪貼板",
})
}
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, index) => (
<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, index) => (
<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, i) => (
<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, i) => (
<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, index) => (
<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, index) => (
<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, index) => (
<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, sIndex) => (
<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, index) => (
<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>
</div>
)
}