415 lines
No EOL
17 KiB
TypeScript
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>
|
|
)
|
|
} |