Introduces core backend and frontend infrastructure for a PDF translation interface. Adds API endpoints for translation, PDF testing, and AI provider testing; implements PDF text extraction, cost tracking, and pricing logic in the lib directory; adds reusable UI components; and provides comprehensive documentation (SDD, environment setup, Claude instructions). Updates Tailwind and global styles, and includes a sample test PDF and configuration files.
148 lines
5.4 KiB
TypeScript
148 lines
5.4 KiB
TypeScript
"use client"
|
||
|
||
import { useState } from "react"
|
||
import { Card } from "@/components/ui/card"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Label } from "@/components/ui/label"
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||
import { Settings, Key, AlertCircle, CheckCircle } from "lucide-react"
|
||
|
||
interface APIConfigProps {
|
||
onConfigUpdate?: () => void
|
||
}
|
||
|
||
export function APIConfig({ onConfigUpdate }: APIConfigProps) {
|
||
const [provider, setProvider] = useState("deepseek")
|
||
const [apiKey, setApiKey] = useState("")
|
||
const [isTestingAPI, setIsTestingAPI] = useState(false)
|
||
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null)
|
||
|
||
const handleTestAPI = async () => {
|
||
if (!apiKey) {
|
||
setTestResult({ success: false, message: "請輸入 API 金鑰" })
|
||
return
|
||
}
|
||
|
||
setIsTestingAPI(true)
|
||
setTestResult(null)
|
||
|
||
try {
|
||
const response = await fetch("/api/test-api", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({
|
||
provider,
|
||
apiKey,
|
||
}),
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (response.ok) {
|
||
setTestResult({ success: true, message: "API 連接成功!" })
|
||
if (onConfigUpdate) {
|
||
onConfigUpdate()
|
||
}
|
||
} else {
|
||
setTestResult({ success: false, message: data.error || "API 測試失敗" })
|
||
}
|
||
} catch (error) {
|
||
setTestResult({ success: false, message: "網路連接錯誤" })
|
||
} finally {
|
||
setIsTestingAPI(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Card className="p-6 shadow-lg">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<Settings className="h-5 w-5 text-blue-600" />
|
||
<h3 className="text-lg font-semibold">API 配置</h3>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<Label htmlFor="provider">選擇 AI 服務提供者</Label>
|
||
<Select value={provider} onValueChange={setProvider}>
|
||
<SelectTrigger className="mt-2">
|
||
<SelectValue placeholder="選擇提供者" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="deepseek">DeepSeek (推薦 - 成本低)</SelectItem>
|
||
<SelectItem value="openai">OpenAI</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="api-key">API 金鑰</Label>
|
||
<div className="mt-2 flex gap-2">
|
||
<div className="flex-1 relative">
|
||
<Key className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||
<input
|
||
id="api-key"
|
||
type="password"
|
||
value={apiKey}
|
||
onChange={(e) => setApiKey(e.target.value)}
|
||
placeholder={provider === "deepseek" ? "sk-xxxxxxxx" : "sk-proj-xxxxxxxx"}
|
||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
/>
|
||
</div>
|
||
<Button
|
||
onClick={handleTestAPI}
|
||
disabled={isTestingAPI || !apiKey}
|
||
variant="outline"
|
||
>
|
||
{isTestingAPI ? "測試中..." : "測試"}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{testResult && (
|
||
<div className={`flex items-center gap-2 p-3 rounded-lg ${
|
||
testResult.success
|
||
? "bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300"
|
||
: "bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300"
|
||
}`}>
|
||
{testResult.success ? (
|
||
<CheckCircle className="h-4 w-4" />
|
||
) : (
|
||
<AlertCircle className="h-4 w-4" />
|
||
)}
|
||
<span className="text-sm">{testResult.message}</span>
|
||
</div>
|
||
)}
|
||
|
||
<div className="text-sm text-gray-600 dark:text-gray-400 space-y-2">
|
||
<h4 className="font-medium">如何獲取 API 金鑰:</h4>
|
||
{provider === "deepseek" ? (
|
||
<div>
|
||
<p><strong>DeepSeek API:</strong></p>
|
||
<ol className="list-decimal list-inside space-y-1 ml-4">
|
||
<li>訪問 <a href="https://platform.deepseek.com" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">platform.deepseek.com</a></li>
|
||
<li>註冊或登入帳號</li>
|
||
<li>前往 API 金鑰頁面</li>
|
||
<li>創建新的 API 金鑰</li>
|
||
<li>複製金鑰並貼上</li>
|
||
</ol>
|
||
<p className="mt-2 text-xs text-green-600">💡 DeepSeek 提供更實惠的定價</p>
|
||
</div>
|
||
) : (
|
||
<div>
|
||
<p><strong>OpenAI API:</strong></p>
|
||
<ol className="list-decimal list-inside space-y-1 ml-4">
|
||
<li>訪問 <a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">platform.openai.com/api-keys</a></li>
|
||
<li>登入您的 OpenAI 帳號</li>
|
||
<li>點擊 "Create new secret key"</li>
|
||
<li>複製生成的金鑰</li>
|
||
<li>貼上到上方欄位</li>
|
||
</ol>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
)
|
||
} |