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

374 lines
14 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 } 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>
)
}