"use client" import type React from "react" import { useState, useEffect } from "react" import { Upload, FileText, Languages, Loader2, Download, FileDown, DollarSign, Hash, TrendingUp, Play, Pause, Square, Volume2 } from "lucide-react" import { Button } from "@/components/ui/button" import { Card } from "@/components/ui/card" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Label } from "@/components/ui/label" import { Checkbox } from "@/components/ui/checkbox" import { costTracker } from "@/lib/cost-tracker" import type { CostSummary } from "@/lib/cost-tracker" const LANGUAGES = [ { code: "auto", name: "自動偵測" }, { code: "zh-TW", name: "繁體中文" }, { code: "zh-CN", name: "簡體中文" }, { code: "en", name: "English" }, { code: "ja", name: "日本語" }, { code: "ko", name: "한국어" }, { code: "es", name: "Español" }, { code: "fr", name: "Français" }, { code: "de", name: "Deutsch" }, { code: "it", name: "Italiano" }, { code: "pt", name: "Português" }, { code: "ru", name: "Русский" }, { code: "ar", name: "العربية" }, { code: "hi", name: "हिन्दी" }, { code: "th", name: "ไทย" }, { code: "vi", name: "Tiếng Việt" }, ] export function PDFTranslator() { const [file, setFile] = useState(null) const [sourceLanguage, setSourceLanguage] = useState("auto") const [targetLanguage, setTargetLanguage] = useState("") const [isTranslating, setIsTranslating] = useState(false) const [translatedText, setTranslatedText] = useState("") const [translatedPDFBase64, setTranslatedPDFBase64] = useState("") const [isDragging, setIsDragging] = useState(false) const [generatePDF, setGeneratePDF] = useState(true) const [tokenUsage, setTokenUsage] = useState(null) const [cost, setCost] = useState(null) const [model, setModel] = useState(null) const [costSummary, setCostSummary] = useState(null) // Speech synthesis states const [isPlaying, setIsPlaying] = useState(false) const [isPaused, setIsPaused] = useState(false) const [speechSupported, setSpeechSupported] = useState(false) const [selectedVoice, setSelectedVoice] = useState("") const [availableVoices, setAvailableVoices] = useState([]) const [speechRate, setSpeechRate] = useState(1.0) const [speechVolume, setSpeechVolume] = useState(1.0) // Load cost summary on component mount useEffect(() => { const summary = costTracker.getCostSummary() setCostSummary(summary) }, []) // Initialize speech synthesis useEffect(() => { if (typeof window !== 'undefined' && 'speechSynthesis' in window) { setSpeechSupported(true) // Load available voices const loadVoices = () => { const voices = speechSynthesis.getVoices() setAvailableVoices(voices) // Auto-select voice based on target language if (targetLanguage && voices.length > 0) { const preferredVoice = findPreferredVoice(voices, targetLanguage) if (preferredVoice) { setSelectedVoice(preferredVoice.name) } } } // Load voices immediately and on voiceschanged event loadVoices() speechSynthesis.addEventListener('voiceschanged', loadVoices) return () => { speechSynthesis.removeEventListener('voiceschanged', loadVoices) speechSynthesis.cancel() // Cancel any ongoing speech } } }, [targetLanguage]) // Helper function to find preferred voice for a language const findPreferredVoice = (voices: SpeechSynthesisVoice[], langCode: string) => { // Map language codes to speech synthesis language codes const langMap: Record = { 'zh-TW': ['zh-TW', 'zh-HK', 'zh'], 'zh-CN': ['zh-CN', 'zh'], 'en': ['en-US', 'en-GB', 'en'], 'ja': ['ja-JP', 'ja'], 'ko': ['ko-KR', 'ko'], 'es': ['es-ES', 'es-US', 'es'], 'fr': ['fr-FR', 'fr'], 'de': ['de-DE', 'de'], 'it': ['it-IT', 'it'], 'pt': ['pt-BR', 'pt-PT', 'pt'], 'ru': ['ru-RU', 'ru'], 'ar': ['ar-SA', 'ar'], 'hi': ['hi-IN', 'hi'], 'th': ['th-TH', 'th'], 'vi': ['vi-VN', 'vi'] } const targetLangs = langMap[langCode] || [langCode] for (const targetLang of targetLangs) { const voice = voices.find(v => v.lang.startsWith(targetLang)) if (voice) return voice } return voices[0] // Fallback to first available voice } const handleFileChange = (e: React.ChangeEvent) => { const selectedFile = e.target.files?.[0] if (selectedFile) { const isPDF = selectedFile.type === "application/pdf" if (isPDF) { setFile(selectedFile) setTranslatedText("") setTranslatedPDFBase64("") setTokenUsage(null) setCost(null) setModel(null) } else { alert("目前僅支援 PDF 文件") } } } const handleDragOver = (e: React.DragEvent) => { e.preventDefault() setIsDragging(true) } const handleDragLeave = () => { setIsDragging(false) } const handleDrop = (e: React.DragEvent) => { e.preventDefault() setIsDragging(false) const droppedFile = e.dataTransfer.files[0] if (droppedFile) { const isPDF = droppedFile.type === "application/pdf" if (isPDF) { setFile(droppedFile) setTranslatedText("") setTranslatedPDFBase64("") setTokenUsage(null) setCost(null) setModel(null) } else { alert("目前僅支援 PDF 文件") } } } const handleTranslate = async () => { if (!file || !targetLanguage) { alert("請選擇檔案和目標語言") return } setIsTranslating(true) setTranslatedText("") setTranslatedPDFBase64("") setTokenUsage(null) setCost(null) setModel(null) try { const formData = new FormData() formData.append("file", file) formData.append("sourceLanguage", sourceLanguage) formData.append("targetLanguage", targetLanguage) formData.append("returnPDF", generatePDF.toString()) const response = await fetch("/api/translate", { method: "POST", body: formData, }) const data = await response.json() if (!response.ok) { throw new Error(data.error || "翻譯失敗") } setTranslatedText(data.translatedText) if (data.pdfBase64) { setTranslatedPDFBase64(data.pdfBase64) } // Set token usage and cost information if (data.tokenUsage) { setTokenUsage(data.tokenUsage) } if (data.cost) { setCost(data.cost) } if (data.model) { setModel(data.model) } // Track cost in accumulator if (data.costSession) { const updatedSummary = costTracker.addCostSession(data.costSession) setCostSummary(updatedSummary) } } catch (error) { console.error("Translation error:", error) alert(error instanceof Error ? error.message : "翻譯過程中發生錯誤") } finally { setIsTranslating(false) } } const downloadTranslatedText = () => { if (!translatedText) return const blob = new Blob([translatedText], { type: "text/plain;charset=utf-8" }) const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = `translated_${file?.name?.replace(".pdf", ".txt") || "document.txt"}` document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) } // Speech synthesis functions const playText = () => { if (!speechSupported || !translatedText) return // If paused, resume if (isPaused) { speechSynthesis.resume() setIsPaused(false) setIsPlaying(true) return } // If already playing, do nothing if (isPlaying) return // Cancel any existing speech speechSynthesis.cancel() const utterance = new SpeechSynthesisUtterance(translatedText) // Configure voice if (selectedVoice) { const voice = availableVoices.find(v => v.name === selectedVoice) if (voice) { utterance.voice = voice } } // Configure speech parameters utterance.rate = speechRate utterance.volume = speechVolume utterance.pitch = 1.0 // Event handlers utterance.onstart = () => { setIsPlaying(true) setIsPaused(false) } utterance.onend = () => { setIsPlaying(false) setIsPaused(false) } utterance.onerror = (event) => { console.error('Speech synthesis error:', event.error) setIsPlaying(false) setIsPaused(false) } utterance.onpause = () => { setIsPaused(true) setIsPlaying(false) } utterance.onresume = () => { setIsPaused(false) setIsPlaying(true) } speechSynthesis.speak(utterance) } const pauseText = () => { if (speechSupported && isPlaying) { speechSynthesis.pause() } } const stopText = () => { if (speechSupported) { speechSynthesis.cancel() setIsPlaying(false) setIsPaused(false) } } const downloadTranslatedPDF = () => { if (!translatedPDFBase64) return // Convert base64 to blob const byteCharacters = atob(translatedPDFBase64) const byteNumbers = new Array(byteCharacters.length) for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i) } const byteArray = new Uint8Array(byteNumbers) const blob = new Blob([byteArray], { type: "application/pdf" }) const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = `translated_${file?.name || "document.pdf"}` document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) } const resetCostTracking = () => { if (confirm("確定要重置費用追蹤記錄嗎?這個操作無法復原。")) { const resetSummary = costTracker.resetCostTracking() setCostSummary(resetSummary) } } return (
{/* 文清楓背景裝飾 */}

文清楓翻譯

🍃 清雅 · 自然 · 智慧 🍂

以文雅之心,譯天下之言
上傳 PDF 文件,體驗清新典雅的多語翻譯之旅

{/* Upload Section */}

📄 上傳文件

🍃 拖曳 PDF 文件到此處,或點擊選擇文件 🍃

支援包含文字的 PDF 文件

{file && (
{file.type.startsWith("image/") ? ( ) : ( )} ✅ 已選擇: {file.name}

📄 檔案類型: PDF 檔案

📏 大小: {(file.size / 1024 / 1024).toFixed(2)} MB

🔧 處理方式: 文字提取

)}
setGeneratePDF(checked as boolean)} />
{/* Result Section */}

🌿 翻譯結果

{translatedText ? (
{/* Token Usage and Cost Information */} {tokenUsage && cost && model && (
📊 Token 使用量與費用
AI 模型
{model.displayName}
({model.provider})
總 Token 數
{tokenUsage.formattedCounts.total}
輸入: {tokenUsage.formattedCounts.prompt} | 輸出: {tokenUsage.formattedCounts.completion}
翻譯費用
{cost.formattedCost}
費用分析
輸入: ${cost.inputCost.toFixed(6)}
輸出: ${cost.outputCost.toFixed(6)}
)} {/* Cumulative Cost Summary */} {costSummary && costSummary.totalSessions > 0 && (
📈 累積費用統計
📊 總翻譯次數
{costSummary.totalSessions}
💬 總 Token 數
{costTracker.formatNumber(costSummary.totalTokens)}
💰 累積費用
{costTracker.formatCost(costSummary.totalCost, costSummary.currency)}
💵 平均費用
{costTracker.formatCost(costSummary.totalCost / costSummary.totalSessions, costSummary.currency)}
{Object.keys(costSummary.byProvider).length > 0 && (
📉 使用統計:
{Object.entries(costSummary.byProvider).map(([provider, stats]) => (
{provider}: {stats.sessions}次 ({costTracker.formatCost(stats.cost, costSummary.currency)})
))}
)}
)} {/* Speech Synthesis Controls */} {speechSupported && (
🔊 語音播放
{/* Play/Pause/Stop Controls */}
{/* Voice Selection */} {availableVoices.length > 0 && (
)} {/* Speech Rate Control */}
setSpeechRate(Number(e.target.value))} className="flex-1 sm:w-16 lg:w-20 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" /> {speechRate.toFixed(1)}x
{/* Status Indicator */} {(isPlaying || isPaused) && (
{isPlaying ? '正在播放...' : isPaused ? '已暫停' : ''}
)}
)}
                    {translatedText}
                  
{translatedPDFBase64 && ( )}
) : (

🌿 翻譯結果將在此顯示

)}
) }