kv-app/app/settings/page.tsx

415 lines
No EOL
17 KiB
TypeScript

'use client'
import { useState } from 'react'
import Header from '@/components/Header'
import { Eye, EyeOff, Loader2, CheckCircle, XCircle, Save, Trash2 } from 'lucide-react'
import { useSettings } from '@/lib/context/SettingsContext'
import { testOpenCodeConnection } from '@/lib/api/opencode'
import { testConnection } from '@/lib/api/api34ai'
export default function SettingsPage() {
const { settings, updateSettings, isMounted } = useSettings()
const [showOpenCodeKey, setShowOpenCodeKey] = useState(false)
const [show34aiKey, setShow34aiKey] = useState(false)
const [testingOpenCode, setTestingOpenCode] = useState(false)
const [testing34ai, setTesting34ai] = useState(false)
const [openCodeStatus, setOpenCodeStatus] = useState<'untested' | 'success' | 'failed'>('untested')
const [status34ai, setStatus34ai] = useState<'untested' | 'success' | 'failed'>('untested')
const [saved, setSaved] = useState(false)
const [openCodeError, setOpenCodeError] = useState<string | null>(null)
const [api34aiError, setApi34aiError] = useState<string | null>(null)
const [testingTikTok, setTestingTikTok] = useState(false)
const [tiktokStatus, setTiktokStatus] = useState<'untested' | 'success' | 'failed'>('untested')
const [tiktokError, setTiktokError] = useState<string | null>(null)
if (!isMounted) {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-white">Loading...</div>
</div>
)
}
const handleTestOpenCode = async () => {
if (!settings.opencodeApiKey) {
setOpenCodeError('Please enter an API key first')
return
}
setTestingOpenCode(true)
setOpenCodeError(null)
try {
const success = await testOpenCodeConnection(settings.opencodeApiKey, settings.opencodeModel)
setOpenCodeStatus(success ? 'success' : 'failed')
if (!success) {
setOpenCodeError('Connection failed. Please check your API key.')
}
} catch (e: any) {
setOpenCodeStatus('failed')
setOpenCodeError(e.message || 'Failed to connect to OpenCode API')
} finally {
setTestingOpenCode(false)
}
}
const handleTest34ai = async () => {
if (!settings.api34aiKey) {
setApi34aiError('Please enter an API key first')
return
}
setTesting34ai(true)
setApi34aiError(null)
try {
const success = await testConnection(settings.api34aiKey)
setStatus34ai(success ? 'success' : 'failed')
if (!success) {
setApi34aiError('Connection failed. Please check your API key.')
}
} catch (e: any) {
setStatus34ai('failed')
setApi34aiError(e.message || 'Failed to connect to 34ai API')
} finally {
setTesting34ai(false)
}
}
const handleTestTikTokCookies = async () => {
if (!settings.tiktokCookies) {
setTiktokError('Please enter TikTok cookies first')
return
}
setTestingTikTok(true)
setTiktokError(null)
try {
const cookies = JSON.parse(settings.tiktokCookies)
if (!Array.isArray(cookies) || cookies.length === 0) {
throw new Error('Invalid cookie format')
}
const response = await fetch('/api/tiktok', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cookies }),
})
if (response.ok || response.status === 401) {
setTiktokStatus('success')
} else {
setTiktokStatus('failed')
setTiktokError('Cookies may be invalid. Try refreshing.')
}
} catch (e: any) {
setTiktokStatus('failed')
setTiktokError(e.message || 'Invalid JSON format in cookies')
} finally {
setTestingTikTok(false)
}
}
const handleSave = () => {
setSaved(true)
setTimeout(() => setSaved(false), 2000)
}
const handleClear = () => {
if (confirm('Are you sure you want to clear all settings?')) {
localStorage.removeItem('ai-video-flow-settings')
window.location.reload()
}
}
const getStatusIcon = (status: 'untested' | 'success' | 'failed') => {
switch (status) {
case 'success':
return <CheckCircle className="text-green-500" size={18} />
case 'failed':
return <XCircle className="text-red-500" size={18} />
default:
return <div className="w-[18px] h-[18px] rounded-full bg-slate-500" />
}
}
return (
<div className="min-h-screen bg-background">
<Header />
<main className="pt-20 pb-8 px-4">
<div className="max-w-3xl mx-auto space-y-6">
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-white mb-2">Settings</h2>
<p className="text-slate-400">Configure your API keys and TikTok cookies</p>
</div>
{saved && (
<div className="bg-green-500/10 border border-green-500 rounded-lg p-4 text-green-400 flex items-center gap-2">
<CheckCircle size={18} />
Settings saved successfully!
</div>
)}
<div className="bg-surface rounded-xl p-6 border border-slate-700">
<h3 className="text-lg font-semibold text-white mb-4">OpenCode Configuration</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
API Key
</label>
<div className="relative">
<input
type={showOpenCodeKey ? 'text' : 'password'}
value={settings.opencodeApiKey}
onChange={(e) => updateSettings({ opencodeApiKey: e.target.value })}
placeholder="sk-..."
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-4 py-2.5 pr-10 text-white placeholder-slate-500 focus:outline-none focus:border-primary"
/>
<button
type="button"
onClick={() => setShowOpenCodeKey(!showOpenCodeKey)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white"
>
{showOpenCodeKey ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Model
</label>
<select
value={settings.opencodeModel}
onChange={(e) => updateSettings({ opencodeModel: e.target.value })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary"
>
<option value="minimax-m2.5-free">MiniMax M2.5 Free</option>
<option value="big-pickle">Big Pickle Free</option>
<option value="hy3-preview-free">Hy3 Preview Free</option>
<option value="nemotron-3-super-free">Nemotron 3 Super Free</option>
</select>
</div>
<div className="flex items-center gap-3">
<button
onClick={handleTestOpenCode}
disabled={!settings.opencodeApiKey || testingOpenCode}
className="flex items-center gap-2 px-4 py-2 bg-slate-700 hover:bg-slate-600 disabled:bg-slate-800 rounded-lg text-white text-sm font-medium"
>
{testingOpenCode ? <Loader2 className="animate-spin" size={16} /> : null}
{testingOpenCode ? 'Testing...' : 'Test Connection'}
</button>
{getStatusIcon(openCodeStatus)}
{openCodeStatus === 'success' && (
<span className="text-green-500 text-sm">Connected!</span>
)}
</div>
{openCodeError && (
<p className="text-red-400 text-sm mt-2">{openCodeError}</p>
)}
</div>
</div>
<div className="bg-surface rounded-xl p-6 border border-slate-700">
<h3 className="text-lg font-semibold text-white mb-4">34ai Configuration</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
API Key
</label>
<div className="relative">
<input
type={show34aiKey ? 'text' : 'password'}
value={settings.api34aiKey}
onChange={(e) => updateSettings({ api34aiKey: e.target.value })}
placeholder="public_..."
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-4 py-2.5 pr-10 text-white placeholder-slate-500 focus:outline-none focus:border-primary"
/>
<button
type="button"
onClick={() => setShow34aiKey(!show34aiKey)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white"
>
{show34aiKey ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Base URL
</label>
<input
type="text"
value={settings.api34aiBaseUrl}
onChange={(e) => updateSettings({ api34aiBaseUrl: e.target.value })}
placeholder="https://34ai.net"
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-4 py-2.5 text-white placeholder-slate-500 focus:outline-none focus:border-primary"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Image Model
</label>
<select
value={settings.imageModel}
onChange={(e) => updateSettings({ imageModel: e.target.value })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary"
>
<option value="banana_pro_2">Banana Pro 2 (Latest)</option>
<option value="banana_pro">Banana Pro (HD)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Video Model
</label>
<select
value={settings.videoModel}
onChange={(e) => updateSettings({ videoModel: e.target.value })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary"
>
<option value="veo3.1">Veo 3.1 (Google AI)</option>
</select>
</div>
</div>
<div className="flex items-center gap-3">
<button
onClick={handleTest34ai}
disabled={!settings.api34aiKey || testing34ai}
className="flex items-center gap-2 px-4 py-2 bg-slate-700 hover:bg-slate-600 disabled:bg-slate-800 rounded-lg text-white text-sm font-medium"
>
{testing34ai ? <Loader2 className="animate-spin" size={16} /> : null}
{testing34ai ? 'Testing...' : 'Test Connection'}
</button>
{getStatusIcon(status34ai)}
{status34ai === 'success' && (
<span className="text-green-500 text-sm">Connected!</span>
)}
</div>
{api34aiError && (
<p className="text-red-400 text-sm mt-2">{api34aiError}</p>
)}
</div>
</div>
<div className="bg-surface rounded-xl p-6 border border-slate-700">
<h3 className="text-lg font-semibold text-white mb-4">TikTok Cookies (Required for Trending)</h3>
<div className="space-y-4">
<div className="p-4 bg-slate-800 rounded-lg">
<h4 className="text-sm font-medium text-white mb-2">How to get TikTok cookies:</h4>
<ol className="text-sm text-slate-400 space-y-1 list-decimal list-inside">
<li>Install "Cookie-Editor" extension on Chrome</li>
<li>Go to tiktok.com and login</li>
<li>Click Cookie-Editor Export Copy</li>
<li>Paste the cookies below</li>
</ol>
</div>
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
TikTok Cookies (JSON format)
</label>
<textarea
value={settings.tiktokCookies}
onChange={(e) => updateSettings({ tiktokCookies: e.target.value })}
placeholder='[{"name":"sessionid","value":"..."}, ...]'
className="w-full h-32 bg-slate-800 border border-slate-600 rounded-lg px-4 py-2.5 text-white placeholder-slate-500 focus:outline-none focus:border-primary font-mono text-sm"
/>
</div>
<div className="flex items-center gap-3">
<button
onClick={handleTestTikTokCookies}
disabled={!settings.tiktokCookies || testingTikTok}
className="flex items-center gap-2 px-4 py-2 bg-slate-700 hover:bg-slate-600 disabled:bg-slate-800 rounded-lg text-white text-sm font-medium"
>
{testingTikTok ? <Loader2 className="animate-spin" size={16} /> : null}
{testingTikTok ? 'Testing...' : 'Test Cookies'}
</button>
{tiktokStatus === 'success' && (
<CheckCircle className="text-green-500" size={18} />
)}
{tiktokStatus === 'failed' && (
<XCircle className="text-red-500" size={18} />
)}
{tiktokStatus === 'success' && (
<span className="text-green-500 text-sm">Cookies valid!</span>
)}
</div>
{tiktokError && (
<p className="text-red-400 text-sm mt-2">{tiktokError}</p>
)}
<p className="text-xs text-slate-500">
Cookies are stored locally in your browser. They are used to access TikTok data.
</p>
</div>
</div>
<div className="bg-surface rounded-xl p-6 border border-slate-700">
<h3 className="text-lg font-semibold text-white mb-4">Default Settings</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Default Video Duration
</label>
<select
value={settings.defaultDuration}
onChange={(e) => updateSettings({ defaultDuration: parseInt(e.target.value) })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary"
>
<option value={4}>4 seconds</option>
<option value={8}>8 seconds</option>
<option value={12}>12 seconds</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Default Aspect Ratio
</label>
<select
value={settings.defaultAspectRatio}
onChange={(e) => updateSettings({ defaultAspectRatio: e.target.value })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary"
>
<option value="16:9">16:9 (Landscape)</option>
<option value="9:16">9:16 (Portrait)</option>
<option value="1:1">1:1 (Square)</option>
</select>
</div>
</div>
</div>
<div className="flex gap-3">
<button
onClick={handleSave}
className="flex items-center gap-2 px-6 py-2.5 bg-primary hover:bg-primary-dark rounded-lg text-white font-medium"
>
<Save size={18} />
Save Settings
</button>
<button
onClick={handleClear}
className="flex items-center gap-2 px-6 py-2.5 bg-red-600 hover:bg-red-700 rounded-lg text-white font-medium"
>
<Trash2 size={18} />
Clear All Settings
</button>
</div>
</div>
</main>
</div>
)
}