Initialized repository for project AI scoring application
Co-authored-by: 吳佩庭 <190080258+WuPeiTing0919@users.noreply.github.com>
This commit is contained in:
373
app/upload/page.tsx
Normal file
373
app/upload/page.tsx
Normal file
@@ -0,0 +1,373 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useCallback } 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
|
||||
}
|
||||
|
||||
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 { toast } = useToast()
|
||||
|
||||
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: "uploading",
|
||||
progress: 0,
|
||||
}))
|
||||
|
||||
setFiles((prev) => [...prev, ...newFiles])
|
||||
|
||||
// 模擬上傳進度
|
||||
newFiles.forEach((file) => {
|
||||
simulateUpload(file.id)
|
||||
})
|
||||
}, [])
|
||||
|
||||
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 simulateUpload = (fileId: string) => {
|
||||
const interval = setInterval(() => {
|
||||
setFiles((prev) =>
|
||||
prev.map((file) => {
|
||||
if (file.id === fileId) {
|
||||
const newProgress = Math.min(file.progress + Math.random() * 30, 100)
|
||||
const status = newProgress === 100 ? "completed" : "uploading"
|
||||
return { ...file, progress: newProgress, status }
|
||||
}
|
||||
return file
|
||||
}),
|
||||
)
|
||||
}, 500)
|
||||
|
||||
setTimeout(() => {
|
||||
clearInterval(interval)
|
||||
setFiles((prev) =>
|
||||
prev.map((file) => (file.id === fileId ? { ...file, progress: 100, status: "completed" } : file)),
|
||||
)
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
if (files.length === 0 && !websiteUrl.trim()) {
|
||||
toast({
|
||||
title: "請上傳文件或提供網站連結",
|
||||
description: "至少需要提供一種評審內容",
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!projectTitle.trim()) {
|
||||
toast({
|
||||
title: "請填寫專案標題",
|
||||
description: "專案標題是必填欄位",
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setIsAnalyzing(true)
|
||||
|
||||
// 模擬分析過程
|
||||
setTimeout(() => {
|
||||
setIsAnalyzing(false)
|
||||
toast({
|
||||
title: "分析完成",
|
||||
description: "評審結果已生成,請查看結果頁面",
|
||||
})
|
||||
// 這裡會導向到結果頁面
|
||||
window.location.href = "/results"
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
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>支援 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={33} className="h-2" />
|
||||
<p className="text-xs text-muted-foreground mt-2">AI 正在深度分析您的內容,請稍候</p>
|
||||
</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>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user