Initialized repository for project AI scoring application

Co-authored-by: 吳佩庭 <190080258+WuPeiTing0919@users.noreply.github.com>
This commit is contained in:
v0
2025-09-21 15:28:35 +00:00
commit 22a5727920
38 changed files with 6515 additions and 0 deletions

373
app/upload/page.tsx Normal file
View 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> 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={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>
)
}