476 lines
18 KiB
TypeScript
476 lines
18 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useCallback, 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 { Input } from "@/components/ui/input"
|
||
import { Label } from "@/components/ui/label"
|
||
import { Textarea } from "@/components/ui/textarea"
|
||
import { Progress } from "@/components/ui/progress"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||
import { Upload, FileText, Video, LinkIcon, X, CheckCircle, AlertCircle, Play, ExternalLink } from "lucide-react"
|
||
import { useToast } from "@/hooks/use-toast"
|
||
import { useDropzone } from "react-dropzone"
|
||
|
||
interface UploadedFile {
|
||
id: string
|
||
name: string
|
||
size: number
|
||
type: string
|
||
status: "uploading" | "completed" | "error"
|
||
progress: number
|
||
url?: string
|
||
file?: File // 儲存原始文件對象
|
||
}
|
||
|
||
export default function UploadPage() {
|
||
const [files, setFiles] = useState<UploadedFile[]>([])
|
||
const [websiteUrl, setWebsiteUrl] = useState("")
|
||
const [projectTitle, setProjectTitle] = useState("")
|
||
const [projectDescription, setProjectDescription] = useState("")
|
||
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
||
const [analysisProgress, setAnalysisProgress] = useState(0)
|
||
const { toast } = useToast()
|
||
|
||
// 動態進度條效果
|
||
useEffect(() => {
|
||
let progressInterval: NodeJS.Timeout | null = null
|
||
|
||
if (isAnalyzing) {
|
||
setAnalysisProgress(0)
|
||
|
||
progressInterval = setInterval(() => {
|
||
setAnalysisProgress((prev) => {
|
||
if (prev >= 90) {
|
||
// 在90%時停止,等待實際完成
|
||
return prev
|
||
}
|
||
// 模擬不規則的進度增長
|
||
const increment = Math.random() * 8 + 2 // 2-10之間的隨機增量
|
||
return Math.min(prev + increment, 90)
|
||
})
|
||
}, 500) // 每500ms更新一次
|
||
} else {
|
||
setAnalysisProgress(0)
|
||
}
|
||
|
||
return () => {
|
||
if (progressInterval) {
|
||
clearInterval(progressInterval)
|
||
}
|
||
}
|
||
}, [isAnalyzing])
|
||
|
||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||
const newFiles: UploadedFile[] = acceptedFiles.map((file) => ({
|
||
id: Date.now().toString() + Math.random().toString(36).substr(2, 9),
|
||
name: file.name,
|
||
size: file.size,
|
||
type: file.type,
|
||
status: "completed", // 直接標記為完成,因為我們會在評審時處理文件
|
||
progress: 100,
|
||
file: file, // 儲存原始文件對象
|
||
}))
|
||
|
||
setFiles((prev) => [...prev, ...newFiles])
|
||
|
||
console.log('📁 文件已準備就緒:', newFiles.map(f => f.name).join(', '))
|
||
}, [])
|
||
|
||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||
onDrop,
|
||
accept: {
|
||
"application/vnd.ms-powerpoint": [".ppt"],
|
||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": [".pptx"],
|
||
"video/*": [".mp4", ".avi", ".mov", ".wmv", ".flv", ".webm"],
|
||
"application/pdf": [".pdf"],
|
||
},
|
||
maxSize: 100 * 1024 * 1024, // 100MB
|
||
})
|
||
|
||
|
||
const removeFile = (fileId: string) => {
|
||
setFiles((prev) => prev.filter((file) => file.id !== fileId))
|
||
}
|
||
|
||
const formatFileSize = (bytes: number) => {
|
||
if (bytes === 0) return "0 Bytes"
|
||
const k = 1024
|
||
const sizes = ["Bytes", "KB", "MB", "GB"]
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||
return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
|
||
}
|
||
|
||
const getFileIcon = (type: string) => {
|
||
if (type.includes("presentation") || type.includes("powerpoint")) {
|
||
return <FileText className="h-8 w-8 text-orange-500" />
|
||
}
|
||
if (type.includes("video")) {
|
||
return <Video className="h-8 w-8 text-blue-500" />
|
||
}
|
||
return <FileText className="h-8 w-8 text-gray-500" />
|
||
}
|
||
|
||
const startAnalysis = async () => {
|
||
if (files.length === 0 && !websiteUrl.trim()) {
|
||
toast({
|
||
title: "請上傳文件或提供網站連結",
|
||
description: "至少需要提供一種評審內容",
|
||
variant: "destructive",
|
||
})
|
||
return
|
||
}
|
||
|
||
if (!projectTitle.trim()) {
|
||
toast({
|
||
title: "請填寫專案標題",
|
||
description: "專案標題是必填欄位",
|
||
variant: "destructive",
|
||
})
|
||
return
|
||
}
|
||
|
||
setIsAnalyzing(true)
|
||
|
||
try {
|
||
console.log('🚀 開始上傳和評審流程...')
|
||
console.log('📝 專案標題:', projectTitle)
|
||
console.log('📋 專案描述:', projectDescription)
|
||
console.log('📁 上傳文件數量:', files.length)
|
||
console.log('🌐 網站連結:', websiteUrl)
|
||
|
||
let projectId = null;
|
||
|
||
// 如果有文件,先上傳到資料庫
|
||
if (files.length > 0) {
|
||
console.log('📤 上傳文件到資料庫...')
|
||
const firstFile = files[0]
|
||
if (firstFile.file) {
|
||
const uploadFormData = new FormData()
|
||
uploadFormData.append('projectTitle', projectTitle)
|
||
uploadFormData.append('projectDescription', projectDescription)
|
||
uploadFormData.append('file', firstFile.file)
|
||
|
||
const uploadResponse = await fetch('/api/upload', {
|
||
method: 'POST',
|
||
body: uploadFormData,
|
||
})
|
||
|
||
const uploadResult = await uploadResponse.json()
|
||
|
||
if (uploadResult.success) {
|
||
projectId = uploadResult.data.projectId
|
||
console.log('✅ 文件上傳成功,專案 ID:', projectId)
|
||
toast({
|
||
title: "文件上傳成功",
|
||
description: `專案已創建,ID: ${projectId}`,
|
||
})
|
||
} else {
|
||
throw new Error(uploadResult.error || '文件上傳失敗')
|
||
}
|
||
} else {
|
||
throw new Error('文件對象遺失,請重新上傳')
|
||
}
|
||
}
|
||
|
||
// 準備 AI 評審數據
|
||
const formData = new FormData()
|
||
formData.append('projectTitle', projectTitle)
|
||
formData.append('projectDescription', projectDescription)
|
||
|
||
if (files.length > 0) {
|
||
const firstFile = files[0]
|
||
if (firstFile.file) {
|
||
formData.append('file', firstFile.file)
|
||
console.log('📄 處理文件:', firstFile.name, '大小:', firstFile.size)
|
||
}
|
||
}
|
||
|
||
if (websiteUrl.trim()) {
|
||
formData.append('websiteUrl', websiteUrl)
|
||
console.log('🌐 處理網站連結:', websiteUrl)
|
||
}
|
||
|
||
// 發送 AI 評審請求
|
||
console.log('🤖 開始 AI 評審...')
|
||
const response = await fetch('/api/evaluate', {
|
||
method: 'POST',
|
||
body: formData,
|
||
})
|
||
|
||
const result = await response.json()
|
||
|
||
if (result.success) {
|
||
console.log('✅ AI 評審完成!')
|
||
console.log('📊 評審結果:', result.data)
|
||
|
||
// 設置進度為100%
|
||
setAnalysisProgress(100)
|
||
|
||
// 等待一下讓用戶看到100%的進度
|
||
await new Promise(resolve => setTimeout(resolve, 500))
|
||
|
||
toast({
|
||
title: "評審完成",
|
||
description: `總分: ${result.data.evaluation.totalScore}/${result.data.evaluation.maxTotalScore}`,
|
||
})
|
||
|
||
// 儲存結果到 localStorage 以便結果頁面使用
|
||
localStorage.setItem('evaluationResult', JSON.stringify(result.data))
|
||
|
||
// 導向到結果頁面
|
||
window.location.href = "/results"
|
||
} else {
|
||
throw new Error(result.error || '評審失敗')
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 上傳或評審失敗:', error)
|
||
toast({
|
||
title: "操作失敗",
|
||
description: error instanceof Error ? error.message : "請稍後再試",
|
||
variant: "destructive",
|
||
})
|
||
} finally {
|
||
setIsAnalyzing(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-background">
|
||
<Sidebar />
|
||
|
||
<main className="md:ml-64 p-6">
|
||
<div className="max-w-4xl mx-auto">
|
||
{/* Header */}
|
||
<div className="mb-8 pt-8 md:pt-0">
|
||
<h1 className="text-3xl font-bold text-foreground mb-2 font-[var(--font-playfair)]">上傳評審內容</h1>
|
||
<p className="text-muted-foreground">上傳您的 PPT、影片或提供網站連結,開始 AI 智能評審</p>
|
||
</div>
|
||
|
||
{/* Project Info */}
|
||
<Card className="mb-6">
|
||
<CardHeader>
|
||
<CardTitle>專案資訊</CardTitle>
|
||
<CardDescription>請填寫專案的基本資訊,這將幫助 AI 更好地理解評審內容</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<div>
|
||
<Label htmlFor="project-title" className="mb-2 block">專案標題 *</Label>
|
||
<Input
|
||
id="project-title"
|
||
value={projectTitle}
|
||
onChange={(e) => setProjectTitle(e.target.value)}
|
||
placeholder="輸入專案標題"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<Label htmlFor="project-description" className="mb-2 block">專案描述</Label>
|
||
<Textarea
|
||
id="project-description"
|
||
value={projectDescription}
|
||
onChange={(e) => setProjectDescription(e.target.value)}
|
||
placeholder="簡要描述專案內容、目標或背景(選填)"
|
||
rows={3}
|
||
/>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Upload Tabs */}
|
||
<Tabs defaultValue="files" className="mb-6">
|
||
<TabsList className="grid w-full grid-cols-2">
|
||
<TabsTrigger value="files">文件上傳</TabsTrigger>
|
||
<TabsTrigger value="website">網站連結</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="files">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Upload className="h-5 w-5" />
|
||
上傳文件
|
||
</CardTitle>
|
||
<CardDescription>支援 PPT、PPTX、MP4、AVI、MOV 等格式,單檔最大 100MB</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{/* Drop Zone */}
|
||
<div
|
||
{...getRootProps()}
|
||
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${
|
||
isDragActive
|
||
? "border-primary bg-primary/5"
|
||
: "border-muted-foreground/25 hover:border-primary/50"
|
||
}`}
|
||
>
|
||
<input {...getInputProps()} />
|
||
<Upload className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
|
||
{isDragActive ? (
|
||
<p className="text-primary font-medium">放開以上傳文件</p>
|
||
) : (
|
||
<div>
|
||
<p className="font-medium mb-2">拖拽文件到此處或點擊選擇</p>
|
||
<p className="text-sm text-muted-foreground">支援 PPT、影片等多種格式</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* File List */}
|
||
{files.length > 0 && (
|
||
<div className="mt-6 space-y-3">
|
||
<h4 className="font-medium">已上傳文件</h4>
|
||
{files.map((file) => (
|
||
<div key={file.id} className="flex items-center gap-4 p-4 bg-muted rounded-lg">
|
||
{getFileIcon(file.type)}
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 mb-1">
|
||
<p className="font-medium truncate">{file.name}</p>
|
||
<Badge
|
||
variant={
|
||
file.status === "completed"
|
||
? "default"
|
||
: file.status === "error"
|
||
? "destructive"
|
||
: "secondary"
|
||
}
|
||
>
|
||
{file.status === "completed" && <CheckCircle className="h-3 w-3 mr-1" />}
|
||
{file.status === "error" && <AlertCircle className="h-3 w-3 mr-1" />}
|
||
{file.status === "uploading" ? "上傳中" : file.status === "completed" ? "完成" : "錯誤"}
|
||
</Badge>
|
||
</div>
|
||
<div className="flex items-center gap-4">
|
||
<span className="text-sm text-muted-foreground">{formatFileSize(file.size)}</span>
|
||
{file.status === "uploading" && (
|
||
<div className="flex-1 max-w-xs">
|
||
<Progress value={file.progress} className="h-2" />
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => removeFile(file.id)}
|
||
className="text-muted-foreground hover:text-destructive"
|
||
>
|
||
<X className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="website">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<LinkIcon className="h-5 w-5" />
|
||
網站連結
|
||
</CardTitle>
|
||
<CardDescription>提供網站 URL,AI 將分析網站內容進行評審</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-4">
|
||
<div>
|
||
<Label htmlFor="website-url">網站 URL</Label>
|
||
<div className="flex gap-2">
|
||
<Input
|
||
id="website-url"
|
||
value={websiteUrl}
|
||
onChange={(e) => setWebsiteUrl(e.target.value)}
|
||
placeholder="https://example.com"
|
||
type="url"
|
||
/>
|
||
<Button variant="outline" size="icon">
|
||
<ExternalLink className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{websiteUrl && (
|
||
<div className="p-4 bg-muted rounded-lg">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<LinkIcon className="h-4 w-4 text-primary" />
|
||
<span className="font-medium">預覽網站</span>
|
||
</div>
|
||
<p className="text-sm text-muted-foreground truncate">{websiteUrl}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
</Tabs>
|
||
|
||
{/* Analysis Button */}
|
||
<Card>
|
||
<CardContent className="pt-6">
|
||
<div className="text-center">
|
||
<Button onClick={startAnalysis} disabled={isAnalyzing} size="lg" className="w-full sm:w-auto px-8">
|
||
{isAnalyzing ? (
|
||
<>
|
||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" />
|
||
分析中...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Play className="h-4 w-4 mr-2" />
|
||
開始 AI 評審
|
||
</>
|
||
)}
|
||
</Button>
|
||
|
||
{isAnalyzing && (
|
||
<div className="mt-4 max-w-md mx-auto">
|
||
<div className="flex items-center justify-between text-sm text-muted-foreground mb-2">
|
||
<span>正在分析內容...</span>
|
||
<span>預計 3-5 分鐘</span>
|
||
</div>
|
||
<Progress value={analysisProgress} className="h-2" />
|
||
<div className="flex items-center justify-between text-xs text-muted-foreground mt-2">
|
||
<span>AI 正在深度分析您的內容,請稍候</span>
|
||
<span>{Math.round(analysisProgress)}%</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Tips */}
|
||
<Card className="mt-6">
|
||
<CardHeader>
|
||
<CardTitle className="text-lg">上傳小貼士</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid md:grid-cols-2 gap-4 text-sm">
|
||
<div>
|
||
<h4 className="font-medium mb-2">文件格式建議</h4>
|
||
<ul className="space-y-1 text-muted-foreground">
|
||
<li>• PPT/PPTX:確保內容清晰完整</li>
|
||
<li>• 影片:建議 1080p 以上畫質</li>
|
||
<li>• 檔案大小:建議不超過 50MB</li>
|
||
</ul>
|
||
</div>
|
||
<div>
|
||
<h4 className="font-medium mb-2">網站連結注意事項</h4>
|
||
<ul className="space-y-1 text-muted-foreground">
|
||
<li>• 確保網站可正常訪問</li>
|
||
<li>• 避免需要登入的頁面</li>
|
||
<li>• 建議提供完整的 URL</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
)
|
||
}
|