initial commit: rocket.new export of broadcastbeat
This commit is contained in:
190
src/app/dashboard/ai-console/health/page.tsx
Normal file
190
src/app/dashboard/ai-console/health/page.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
interface EndpointResult {
|
||||
loading: boolean;
|
||||
status: number | null;
|
||||
json: unknown;
|
||||
error: string | null;
|
||||
fetchedAt: string | null;
|
||||
}
|
||||
|
||||
const defaultResult = (): EndpointResult => ({
|
||||
loading: false,
|
||||
status: null,
|
||||
json: null,
|
||||
error: null,
|
||||
fetchedAt: null,
|
||||
});
|
||||
|
||||
export default function AIHealthPage() {
|
||||
const [ollamaHealth, setOllamaHealth] = useState<EndpointResult>(defaultResult());
|
||||
const [nodeHealth, setNodeHealth] = useState<EndpointResult>(defaultResult());
|
||||
|
||||
const fetchEndpoint = useCallback(
|
||||
async (
|
||||
url: string,
|
||||
setter: React.Dispatch<React.SetStateAction<EndpointResult>>
|
||||
) => {
|
||||
setter((prev) => ({ ...prev, loading: true, error: null }));
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
const json = await res.json().catch(() => null);
|
||||
setter({
|
||||
loading: false,
|
||||
status: res.status,
|
||||
json,
|
||||
error: null,
|
||||
fetchedAt: new Date().toISOString(),
|
||||
});
|
||||
} catch (err) {
|
||||
setter({
|
||||
loading: false,
|
||||
status: null,
|
||||
json: null,
|
||||
error: err instanceof Error ? err.message : 'Fetch failed',
|
||||
fetchedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const fetchAll = useCallback(() => {
|
||||
fetchEndpoint('/api/ai/ollama-health', setOllamaHealth);
|
||||
fetchEndpoint('/api/ai/node-health', setNodeHealth);
|
||||
}, [fetchEndpoint]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAll();
|
||||
}, [fetchAll]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 text-gray-100 p-6">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">AI Node Health</h1>
|
||||
<p className="text-gray-400 text-sm mt-1">
|
||||
Live responses from AI_001 health endpoints
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={fetchAll}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Endpoint Cards */}
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
<EndpointCard
|
||||
title="GET /api/ai/ollama-health"
|
||||
description="Pings AI_001 at /ping — no auth required"
|
||||
result={ollamaHealth}
|
||||
onRefresh={() => fetchEndpoint('/api/ai/ollama-health', setOllamaHealth)}
|
||||
/>
|
||||
<EndpointCard
|
||||
title="GET /api/ai/node-health"
|
||||
description="Calls AI_001 at /health — uses AI_NODE_API_KEY bearer token"
|
||||
result={nodeHealth}
|
||||
onRefresh={() => fetchEndpoint('/api/ai/node-health', setNodeHealth)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface EndpointCardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
result: EndpointResult;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
function EndpointCard({ title, description, result, onRefresh }: EndpointCardProps) {
|
||||
const isOk =
|
||||
result.status !== null && result.status >= 200 && result.status < 300;
|
||||
const hasData = result.json !== null || result.error !== null;
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
|
||||
{/* Card Header */}
|
||||
<div className="flex items-start justify-between px-5 py-4 border-b border-gray-800">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<code className="text-indigo-400 font-mono text-sm font-semibold">{title}</code>
|
||||
{result.status !== null && (
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded text-xs font-bold ${
|
||||
isOk
|
||||
? 'bg-emerald-900/60 text-emerald-400 border border-emerald-700' :'bg-red-900/60 text-red-400 border border-red-700'
|
||||
}`}
|
||||
>
|
||||
HTTP {result.status}
|
||||
</span>
|
||||
)}
|
||||
{result.loading && (
|
||||
<span className="px-2 py-0.5 rounded text-xs font-bold bg-yellow-900/60 text-yellow-400 border border-yellow-700 animate-pulse">
|
||||
Loading…
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-gray-500 text-xs mt-1">{description}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={result.loading}
|
||||
className="text-gray-500 hover:text-gray-300 transition-colors disabled:opacity-40 ml-4 mt-0.5"
|
||||
title="Refresh this endpoint"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Raw JSON Output */}
|
||||
<div className="px-5 py-4">
|
||||
{result.loading && !hasData && (
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm py-4">
|
||||
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
|
||||
</svg>
|
||||
Fetching…
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!result.loading && !hasData && (
|
||||
<p className="text-gray-600 text-sm py-4">No data yet.</p>
|
||||
)}
|
||||
|
||||
{result.error && result.json === null && (
|
||||
<div className="bg-red-950/40 border border-red-800 rounded-lg p-4">
|
||||
<p className="text-red-400 text-sm font-mono">{result.error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result.json !== null && (
|
||||
<pre className="bg-gray-950 border border-gray-800 rounded-lg p-4 text-sm font-mono text-green-300 overflow-x-auto whitespace-pre-wrap break-words leading-relaxed">
|
||||
{JSON.stringify(result.json, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
|
||||
{result.fetchedAt && (
|
||||
<p className="text-gray-600 text-xs mt-2">
|
||||
Last fetched: {new Date(result.fetchedAt).toLocaleTimeString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1355
src/app/dashboard/ai-console/page.tsx
Normal file
1355
src/app/dashboard/ai-console/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
444
src/app/dashboard/ai-console/settings/page.tsx
Normal file
444
src/app/dashboard/ai-console/settings/page.tsx
Normal file
@@ -0,0 +1,444 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { BB_PEN_NAMES, AV_PEN_NAMES } from '@/lib/auth/permissions';
|
||||
|
||||
interface Settings {
|
||||
ollama_endpoint: string;
|
||||
ollama_model_default: string;
|
||||
ollama_model_translation: string;
|
||||
ollama_model_background: string;
|
||||
auto_failover: boolean;
|
||||
failover_timeout_seconds: number;
|
||||
default_site_id: number;
|
||||
default_pen_name_bb: string;
|
||||
default_pen_name_av: string;
|
||||
save_session_history: boolean;
|
||||
history_retention_days: number;
|
||||
streaming_enabled: boolean;
|
||||
background_use_local: boolean;
|
||||
background_timeout_seconds: number;
|
||||
priority_queue_enabled: boolean;
|
||||
}
|
||||
|
||||
interface ConnectionTest {
|
||||
online: boolean;
|
||||
latencyMs: number;
|
||||
models: string[];
|
||||
}
|
||||
|
||||
export default function AIConsoleSettingsPage() {
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const [settings, setSettings] = useState<Settings>({
|
||||
ollama_endpoint: '',
|
||||
ollama_model_default: '',
|
||||
ollama_model_translation: '',
|
||||
ollama_model_background: '',
|
||||
auto_failover: true,
|
||||
failover_timeout_seconds: 8,
|
||||
default_site_id: 1,
|
||||
default_pen_name_bb: 'Michael Strand',
|
||||
default_pen_name_av: 'Rex Chandler',
|
||||
save_session_history: true,
|
||||
history_retention_days: 30,
|
||||
streaming_enabled: true,
|
||||
background_use_local: true,
|
||||
background_timeout_seconds: 120,
|
||||
priority_queue_enabled: true,
|
||||
});
|
||||
|
||||
const [loadingSettings, setLoadingSettings] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [connectionResult, setConnectionResult] = useState<ConnectionTest | null>(null);
|
||||
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
|
||||
|
||||
const showToast = (message: string, type: 'success' | 'error' = 'success') => {
|
||||
setToast({ message, type });
|
||||
setTimeout(() => setToast(null), 4000);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) router.push('/login');
|
||||
}, [user, loading, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
fetch('/api/ai/console-settings')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.settings) setSettings(data.settings);
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setLoadingSettings(false));
|
||||
}, [user]);
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
setTesting(true);
|
||||
setConnectionResult(null);
|
||||
try {
|
||||
const res = await fetch('/api/ai/console-settings?action=test_connection');
|
||||
const data = await res.json();
|
||||
setConnectionResult(data);
|
||||
} catch {
|
||||
setConnectionResult({ online: false, latencyMs: 0, models: [] });
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await fetch('/api/ai/console-settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'save_settings', ...settings }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) showToast('Settings saved');
|
||||
else showToast(data.error || 'Failed to save', 'error');
|
||||
} catch {
|
||||
showToast('Failed to save', 'error');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const update = (key: keyof Settings, value: any) => {
|
||||
setSettings(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
if (loading || loadingSettings) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center">
|
||||
<div className="w-6 h-6 border-2 border-[#3b82f6]/30 border-t-[#3b82f6] rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0d1117]">
|
||||
{/* Header */}
|
||||
<div className="bg-[#111827] border-b border-[#1e2a3a] px-6 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/dashboard/ai-console" className="text-[#555] hover:text-[#3b82f6] text-sm transition-colors">← AI Console</Link>
|
||||
<span className="text-[#333]">/</span>
|
||||
<h1 className="text-white font-bold text-sm">AI Console Settings</h1>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium"
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save Settings'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="max-w-3xl mx-auto px-6 py-8 space-y-8">
|
||||
{/* Local AI Server */}
|
||||
<section className="bg-[#111827] border border-[#1e2a3a] rounded-xl p-6">
|
||||
<h2 className="text-white font-bold text-base mb-1">Local AI Server (Ollama)</h2>
|
||||
<p className="text-[#555] text-xs mb-5">
|
||||
Connect to your local Ollama instance running on your Windows PC with 24GB GPU.
|
||||
</p>
|
||||
|
||||
{/* Windows sleep warning */}
|
||||
<div className="bg-yellow-900/20 border border-yellow-700/40 rounded-lg p-4 mb-5">
|
||||
<div className="flex gap-3">
|
||||
<span className="text-yellow-400 text-lg flex-shrink-0">⚠</span>
|
||||
<div>
|
||||
<p className="text-yellow-300 text-sm font-semibold mb-1">Windows Sleep / Hibernate Warning</p>
|
||||
<p className="text-yellow-200/70 text-xs leading-relaxed">
|
||||
Your Windows server may sleep or hibernate when inactive, causing the local AI to go offline.
|
||||
To prevent this:
|
||||
</p>
|
||||
<ul className="text-yellow-200/70 text-xs mt-2 space-y-1 ml-3">
|
||||
<li>• <strong className="text-yellow-200">Option 1:</strong> Windows Settings → System → Power & Sleep → Sleep → set to <strong className="text-yellow-200">Never</strong></li>
|
||||
<li>• <strong className="text-yellow-200">Option 2:</strong> Download the free <strong className="text-yellow-200">Caffeine</strong> app for Windows — keeps your PC awake automatically</li>
|
||||
</ul>
|
||||
<p className="text-yellow-200/50 text-[10px] mt-2">
|
||||
The Health Dashboard will alert you if the local server has been offline for more than 30 minutes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-[#aaa] text-xs font-medium mb-1.5">Endpoint URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.ollama_endpoint}
|
||||
onChange={e => update('ollama_endpoint', e.target.value)}
|
||||
placeholder="http://192.168.1.x:11434 or https://your-tunnel.ngrok.io"
|
||||
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6] placeholder-[#333]"
|
||||
/>
|
||||
<p className="text-[#444] text-[10px] mt-1">
|
||||
Use direct IP if on the same network, or a tunnel URL (ngrok, Cloudflare Tunnel, Tailscale) for remote access.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleTestConnection}
|
||||
disabled={testing || !settings.ollama_endpoint}
|
||||
className="bg-[#1e2a3a] hover:bg-[#253347] disabled:opacity-40 text-white text-sm px-4 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
{testing ? 'Testing...' : 'Test Connection'}
|
||||
</button>
|
||||
{connectionResult && (
|
||||
<div className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm ${
|
||||
connectionResult.online
|
||||
? 'bg-green-900/30 border border-green-700/40 text-green-400' :'bg-red-900/30 border border-red-700/40 text-red-400'
|
||||
}`}>
|
||||
<div className={`w-2 h-2 rounded-full ${connectionResult.online ? 'bg-green-500' : 'bg-red-500'}`} />
|
||||
{connectionResult.online
|
||||
? `Connected — ${connectionResult.latencyMs}ms — ${connectionResult.models.length} model(s)`
|
||||
: 'Unreachable'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{connectionResult?.models && connectionResult.models.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-[#aaa] text-xs font-medium mb-1.5">Default Model (Stories)</label>
|
||||
<select
|
||||
value={settings.ollama_model_default}
|
||||
onChange={e => update('ollama_model_default', e.target.value)}
|
||||
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]"
|
||||
>
|
||||
<option value="">Auto (largest available)</option>
|
||||
{connectionResult.models.map(m => <option key={m} value={m}>{m}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[#aaa] text-xs font-medium mb-1.5">Default Model (Translation)</label>
|
||||
<select
|
||||
value={settings.ollama_model_translation}
|
||||
onChange={e => update('ollama_model_translation', e.target.value)}
|
||||
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]"
|
||||
>
|
||||
<option value="">Same as default</option>
|
||||
{connectionResult.models.map(m => <option key={m} value={m}>{m}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[#aaa] text-xs font-medium mb-1.5">Default Model (Background Tasks)</label>
|
||||
<select
|
||||
value={settings.ollama_model_background}
|
||||
onChange={e => update('ollama_model_background', e.target.value)}
|
||||
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]"
|
||||
>
|
||||
<option value="">Same as default</option>
|
||||
{connectionResult.models.map(m => <option key={m} value={m}>{m}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[#aaa] text-xs font-medium">Auto-failover to Cloud AI</p>
|
||||
<p className="text-[#444] text-[10px]">Automatically use Anthropic when local is unavailable</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => update('auto_failover', !settings.auto_failover)}
|
||||
className={`w-10 h-5 rounded-full transition-colors relative ${settings.auto_failover ? 'bg-[#3b82f6]' : 'bg-[#2a3a50]'}`}
|
||||
>
|
||||
<div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.auto_failover ? 'translate-x-5' : 'translate-x-0.5'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[#aaa] text-xs font-medium mb-1.5">Failover Timeout (seconds)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.failover_timeout_seconds}
|
||||
onChange={e => update('failover_timeout_seconds', Number(e.target.value))}
|
||||
min={3} max={30}
|
||||
className="w-32 bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#3b82f6]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Cloud AI */}
|
||||
<section className="bg-[#111827] border border-[#1e2a3a] rounded-xl p-6">
|
||||
<h2 className="text-white font-bold text-base mb-1">Cloud AI (Anthropic Failover)</h2>
|
||||
<p className="text-[#555] text-xs mb-5">Used when local Ollama is unavailable.</p>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between py-2 border-b border-[#1e2a3a]">
|
||||
<span className="text-[#aaa] text-sm">Model</span>
|
||||
<span className="text-white text-sm font-mono">claude-sonnet-4-20250514</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-2 border-b border-[#1e2a3a]">
|
||||
<span className="text-[#aaa] text-sm">Fallback Model</span>
|
||||
<span className="text-[#555] text-sm font-mono">claude-3-5-sonnet-20241022</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-[#aaa] text-sm">API Key Status</span>
|
||||
<span className={`text-sm font-medium ${process.env.NEXT_PUBLIC_SITE_URL ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{/* Key status shown via ping */}
|
||||
<span className="text-green-400">Configured</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Console Preferences */}
|
||||
<section className="bg-[#111827] border border-[#1e2a3a] rounded-xl p-6">
|
||||
<h2 className="text-white font-bold text-base mb-5">AI Console Preferences</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-[#aaa] text-xs font-medium mb-1.5">Default Site</label>
|
||||
<select
|
||||
value={settings.default_site_id}
|
||||
onChange={e => update('default_site_id', Number(e.target.value))}
|
||||
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]"
|
||||
>
|
||||
<option value={1}>Broadcast Beat</option>
|
||||
<option value={2}>AV Beat</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[#aaa] text-xs font-medium mb-1.5">Default Pen Name (Broadcast Beat)</label>
|
||||
<select
|
||||
value={settings.default_pen_name_bb}
|
||||
onChange={e => update('default_pen_name_bb', e.target.value)}
|
||||
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]"
|
||||
>
|
||||
{BB_PEN_NAMES.map(n => <option key={n} value={n}>{n}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[#aaa] text-xs font-medium mb-1.5">Default Pen Name (AV Beat)</label>
|
||||
<select
|
||||
value={settings.default_pen_name_av}
|
||||
onChange={e => update('default_pen_name_av', e.target.value)}
|
||||
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]"
|
||||
>
|
||||
{AV_PEN_NAMES.map(n => <option key={n} value={n}>{n}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[#aaa] text-xs font-medium">Save Session History</p>
|
||||
<p className="text-[#444] text-[10px]">Store last 20 sessions</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => update('save_session_history', !settings.save_session_history)}
|
||||
className={`w-10 h-5 rounded-full transition-colors relative ${settings.save_session_history ? 'bg-[#3b82f6]' : 'bg-[#2a3a50]'}`}
|
||||
>
|
||||
<div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.save_session_history ? 'translate-x-5' : 'translate-x-0.5'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[#aaa] text-xs font-medium mb-1.5">History Retention</label>
|
||||
<select
|
||||
value={settings.history_retention_days}
|
||||
onChange={e => update('history_retention_days', Number(e.target.value))}
|
||||
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]"
|
||||
>
|
||||
<option value={30}>30 days</option>
|
||||
<option value={60}>60 days</option>
|
||||
<option value={90}>90 days</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[#aaa] text-xs font-medium">Streaming Responses</p>
|
||||
<p className="text-[#444] text-[10px]">Text appears as it generates</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => update('streaming_enabled', !settings.streaming_enabled)}
|
||||
className={`w-10 h-5 rounded-full transition-colors relative ${settings.streaming_enabled ? 'bg-[#3b82f6]' : 'bg-[#2a3a50]'}`}
|
||||
>
|
||||
<div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.streaming_enabled ? 'translate-x-5' : 'translate-x-0.5'}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Background Tasks */}
|
||||
<section className="bg-[#111827] border border-[#1e2a3a] rounded-xl p-6">
|
||||
<h2 className="text-white font-bold text-base mb-5">Background AI Tasks</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[#aaa] text-xs font-medium">Route Background Tasks to Local AI</p>
|
||||
<p className="text-[#444] text-[10px]">Story generation, translation, classification</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => update('background_use_local', !settings.background_use_local)}
|
||||
className={`w-10 h-5 rounded-full transition-colors relative ${settings.background_use_local ? 'bg-[#3b82f6]' : 'bg-[#2a3a50]'}`}
|
||||
>
|
||||
<div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.background_use_local ? 'translate-x-5' : 'translate-x-0.5'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[#aaa] text-xs font-medium mb-1.5">Background Task Timeout (seconds)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.background_timeout_seconds}
|
||||
onChange={e => update('background_timeout_seconds', Number(e.target.value))}
|
||||
min={30} max={300}
|
||||
className="w-32 bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#3b82f6]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[#aaa] text-xs font-medium">Priority Queue</p>
|
||||
<p className="text-[#444] text-[10px]">Interactive requests take priority over background tasks</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => update('priority_queue_enabled', !settings.priority_queue_enabled)}
|
||||
className={`w-10 h-5 rounded-full transition-colors relative ${settings.priority_queue_enabled ? 'bg-[#3b82f6]' : 'bg-[#2a3a50]'}`}
|
||||
>
|
||||
<div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.priority_queue_enabled ? 'translate-x-5' : 'translate-x-0.5'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#0d1117] border border-[#1e2a3a] rounded-lg p-4">
|
||||
<p className="text-[#555] text-xs font-semibold mb-2 uppercase tracking-wide">Priority Queue Order</p>
|
||||
<ol className="space-y-1">
|
||||
{[
|
||||
'Interactive AI Console requests (admin typing)',
|
||||
'Breaking news story generation',
|
||||
'Show coverage auto-stories during show week',
|
||||
'Standard background auto-stories',
|
||||
'Translation backfill',
|
||||
'Classification and extraction tasks',
|
||||
].map((item, i) => (
|
||||
<li key={i} className="flex items-center gap-2 text-xs text-[#555]">
|
||||
<span className="w-4 h-4 rounded-full bg-[#1e2a3a] text-[#3b82f6] text-[10px] flex items-center justify-center font-bold flex-shrink-0">{i + 1}</span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{toast && (
|
||||
<div className={`fixed bottom-6 right-6 px-4 py-3 rounded-lg text-sm font-medium shadow-lg z-50 ${
|
||||
toast.type === 'error' ? 'bg-red-900 text-red-200 border border-red-700' : 'bg-green-900 text-green-200 border border-green-700'
|
||||
}`}>
|
||||
{toast.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user