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

445 lines
17 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, 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('🚀 開始 AI 評審流程...')
console.log('📝 專案標題:', projectTitle)
console.log('📋 專案描述:', projectDescription)
console.log('📁 上傳文件數量:', files.length)
console.log('🌐 網站連結:', websiteUrl)
// 準備表單數據
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)
} else {
throw new Error('文件對象遺失,請重新上傳')
}
}
if (websiteUrl.trim()) {
formData.append('websiteUrl', websiteUrl)
console.log('🌐 處理網站連結:', websiteUrl)
}
// 發送評審請求
console.log('📤 發送評審請求到 API...')
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('❌ AI 評審失敗:', 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"> *</Label>
<Input
id="project-title"
value={projectTitle}
onChange={(e) => setProjectTitle(e.target.value)}
placeholder="輸入專案標題"
/>
</div>
<div>
<Label htmlFor="project-description"></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> PPTPPTXMP4AVIMOV 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> URLAI </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>
)
}