diff --git a/.gitignore b/.gitignore index 6ccfd2f..d43b7d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,35 @@ -node_modules -.next -.venv +# Node / Next.js +node_modules/ +.next/ +out/ +dist/ + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# FastAPI / Python +__pycache__/ +*.pyc +*.pyo +*.pyd +*.sqlite3 +*.db + +# Virtual environments +.venv/ +venv/ +env/ + +# IDE / Editor +.vscode/ +.idea/ +*.swp diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 662ac69..62568f5 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -1,492 +1,483 @@ -'use client'; - -import React, { useEffect, useMemo, useState } from 'react'; -import { useRouter } from 'next/navigation'; - -type SystemSettings = { - model: string; - systemPrompt: string; - temperature: number; // 0–2 - maxTokens: number; // >=1 - retrievalDepth: number; // >=0 - rateLimit: number; // >=1 (requests/min) -}; - -const DEFAULT_SETTINGS: SystemSettings = { - model: 'gpt-4o-mini', - systemPrompt: - 'You are an internal assistant. Answer concisely and follow safety and privacy policies. Do not reveal secrets.', - temperature: 0.2, - maxTokens: 1024, - retrievalDepth: 5, - rateLimit: 60, -}; - -const HARD_CODED_USERNAME = 'admin'; -const HARD_CODED_PASSWORD = 'secret'; -const ADMIN_TOKEN_KEY = 'admin_token'; -const LOCAL_STORAGE_KEY = 'admin_settings_v1'; - -// Safe normalization + bounds -function normalizeSettings(obj: Partial | null): SystemSettings { - const s = obj ?? {}; - const temperature = - typeof s.temperature === 'number' ? Math.min(2, Math.max(0, s.temperature)) : DEFAULT_SETTINGS.temperature; - const maxTokens = - typeof s.maxTokens === 'number' ? Math.max(1, Math.floor(s.maxTokens)) : DEFAULT_SETTINGS.maxTokens; - const retrievalDepth = - typeof s.retrievalDepth === 'number' ? Math.max(0, Math.floor(s.retrievalDepth)) : DEFAULT_SETTINGS.retrievalDepth; - const rateLimit = - typeof s.rateLimit === 'number' ? Math.max(1, Math.floor(s.rateLimit)) : DEFAULT_SETTINGS.rateLimit; - - return { - model: typeof s.model === 'string' && s.model.trim() ? s.model.trim() : DEFAULT_SETTINGS.model, - systemPrompt: - typeof s.systemPrompt === 'string' && s.systemPrompt.trim() - ? s.systemPrompt - : DEFAULT_SETTINGS.systemPrompt, - temperature, - maxTokens, - retrievalDepth, - rateLimit, - }; -} - -export default function AdminPageClient() { +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { ProtectedRoute } from "@/components/auth/ProtectedRoute"; +import { useAuth } from "@/contexts/AuthContext"; +import { adminApi, AdminStats, SystemSettings, User } from "@/lib/api/admin"; +import { ApiError } from "@/lib/api/client"; +import { UserManagement } from "@/components/admin/UserManagement"; +import { Header } from "@/components/layout/Header"; +import { ApiKeyManagement } from "@/components/admin/ApiKeyManagement"; + +type TabKey = "stats" | "users" | "settings" | "api-keys"; +const DEFAULT_MODELS = [ + "GPT-2 (Local)", + "GPT-3.5 Turbo", + "GPT-4", + "GPT-4o Mini", +]; + +const TAB_CONFIG: { key: TabKey; label: string; description: string }[] = [ + { + key: "stats", + label: "Overview", + description: "Metrics and usage", + }, + { + key: "users", + label: "Users", + description: "Manage accounts and roles", + }, + { + key: "settings", + label: "LLM Settings", + description: "Control model, tokens, limits", + }, + { + key: "api-keys", + label: "API Keys", + description: "Manage access keys", + }, +]; + +function AdminPageContent() { + const { logout } = useAuth(); const router = useRouter(); - - const [authed, setAuthed] = useState(() => { - try { - return sessionStorage.getItem(ADMIN_TOKEN_KEY) === 'ok'; - } catch { - return false; - } - }); - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - - const [loading, setLoading] = useState(false); + const searchParams = useSearchParams(); + const [activeTab, setActiveTab] = useState("stats"); + const [stats, setStats] = useState(null); + const [users, setUsers] = useState([]); const [settings, setSettings] = useState(null); - const [saving, setSaving] = useState(false); - const [message, setMessage] = useState(''); - const [error, setError] = useState(''); - - const [testPrompt, setTestPrompt] = useState(''); - const [testing, setTesting] = useState(false); - const [testResult, setTestResult] = useState(null); + const [modelOptions, setModelOptions] = useState(DEFAULT_MODELS); + const [apiKeys, setApiKeys] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); - // Derived validation state - const validation = useMemo(() => { - const issues: string[] = []; - if (!settings) return issues; - if (!settings.model.trim()) issues.push('Model is required.'); - if (settings.temperature < 0 || settings.temperature > 2) issues.push('Temperature must be between 0 and 2.'); - if (!Number.isFinite(settings.maxTokens) || settings.maxTokens < 1) issues.push('Max tokens must be >= 1.'); - if (!Number.isFinite(settings.retrievalDepth) || settings.retrievalDepth < 0) - issues.push('Retrieval depth must be >= 0.'); - if (!Number.isFinite(settings.rateLimit) || settings.rateLimit < 1) - issues.push('Rate limit must be >= 1 requests/min.'); - if (!settings.systemPrompt.trim()) issues.push('System prompt is required.'); - return issues; - }, [settings]); + const navButtons = useMemo(() => TAB_CONFIG, []); useEffect(() => { - if (authed) { - loadSettings(); + const tabParam = searchParams.get("tab") as TabKey | null; + if ( + tabParam && + ["stats", "users", "settings", "api-keys"].includes(tabParam) + ) { + setActiveTab(tabParam); } - }, [authed]); + }, [searchParams]); + + useEffect(() => { + loadData(); + }, [activeTab]); - async function loadSettings() { + const loadData = async () => { setLoading(true); - setError(''); + setError(""); try { - const res = await fetch('/api/admin/settings'); - if (res.ok) { - const json = await res.json(); - const normalized = normalizeSettings(json); - setSettings(normalized); - try { - localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(normalized)); - } catch {} - setLoading(false); - return; - } else { - setError(`Failed to load from server: ${res.status} ${res.statusText}`); + if (activeTab === "stats") { + const data = await adminApi.getStats(); + setStats(data); + } else if (activeTab === "users") { + const data = await adminApi.getUsers(); + setUsers(data); + } else if (activeTab === "settings") { + const [settingsData, modelsData] = await Promise.all([ + adminApi.getSystemSettings(), + adminApi.getAvailableModels().catch(() => DEFAULT_MODELS), + ]); + const mergedModels = Array.from( + new Set([...(modelsData || DEFAULT_MODELS), settingsData.model]) + ); + setSettings(settingsData); + setModelOptions(mergedModels); + } else if (activeTab === "api-keys") { + const keys = await adminApi.getApiKeys(); + setApiKeys(keys); } - } catch { - // ignore; fallback below - } - - try { - const local = localStorage.getItem(LOCAL_STORAGE_KEY); - if (local) { - setSettings(normalizeSettings(JSON.parse(local))); + } catch (err) { + if (err instanceof ApiError) { + setError(err.message || "Failed to load data"); } else { - setSettings(DEFAULT_SETTINGS); + setError("An unexpected error occurred"); } - } catch { - setSettings(DEFAULT_SETTINGS); } finally { setLoading(false); } - } + }; - function notify(msg: string) { - setMessage(msg); - setTimeout(() => setMessage(''), 3000); - } + const handleTabChange = (tab: TabKey) => { + setActiveTab(tab); + router.replace(`/admin?tab=${tab}`); + }; - async function handleSave(e?: React.FormEvent) { - if (e) e.preventDefault(); + const handleUpdateSettings = async ( + updatedSettings: Partial + ) => { if (!settings) return; - - // Block save if invalid - if (validation.length > 0) { - setError(validation.join(' ')); - return; - } - - setSaving(true); - setError(''); - const payload = normalizeSettings(settings); try { - const res = await fetch('/api/admin/settings', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }); - if (res.ok) { - const saved = await res.json().catch(() => null); - const normalized = normalizeSettings(saved || payload); - setSettings(normalized); - try { - localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(normalized)); - } catch {} - notify('Settings saved.'); + const updated = await adminApi.updateSystemSettings(updatedSettings); + setSettings(updated); + } catch (err) { + if (err instanceof ApiError) { + setError(err.message || "Failed to update settings"); } else { - // Persist locally on server error - try { - localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(payload)); - } catch {} - setError(`Save failed: ${res.status} ${res.statusText}`); - notify('Saved to localStorage (server error).'); + setError("An unexpected error occurred"); } - } catch { - // Persist locally on network error - try { - localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(payload)); - } catch {} - setError('Network error while saving settings.'); - notify('Saved to localStorage (network error).'); - } finally { - setSaving(false); } - } + }; - async function handleTest() { - if (!settings) return; - if (!testPrompt || testPrompt.trim().length === 0) { - setError('Test prompt cannot be empty.'); - return; - } - if (validation.length > 0) { - setError('Fix settings validation issues before running a test.'); - return; - } - setTesting(true); - setTestResult(null); - setError(''); - try { - const res = await fetch('/api/admin/test', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ prompt: testPrompt, settings: normalizeSettings(settings) }), - }); - if (res.ok) { - const json = await res.json().catch(() => null); - if (json && typeof json.output === 'string') { - setTestResult(json.output); - } else if (typeof json === 'string') { - setTestResult(json); - } else { - setTestResult(JSON.stringify(json, null, 2)); - } - } else { - setError(`Test failed: ${res.status} ${res.statusText}`); - } - } catch { - setError('Network error while running test.'); - } finally { - setTesting(false); - } - } + const handleLogout = async () => { + await logout(); + router.push("/login"); + }; - function handleLogin(e?: React.FormEvent) { - if (e) e.preventDefault(); - if (username === HARD_CODED_USERNAME && password === HARD_CODED_PASSWORD) { - try { - sessionStorage.setItem(ADMIN_TOKEN_KEY, 'ok'); - } catch {} - setAuthed(true); - notify('Logged in (development only).'); - } else { - setError('Invalid username or password.'); - } - } + return ( +
+
+
+ {/* Dashboard Header */} +
+
+

+ Admin Dashboard +

+

+ Central control center for the platform +

+
+
- function handleLogout() { - try { - sessionStorage.removeItem(ADMIN_TOKEN_KEY); - } catch {} - setAuthed(false); - setUsername(''); - setPassword(''); - setSettings(null); - setMessage(''); - setError(''); - setTestPrompt(''); - setTestResult(null); - } + {/* Dashboard Menu */} +
+ {navButtons.map((item) => { + const isActive = activeTab === item.key; + return ( + + ); + })} +
- if (!authed) { - return ( -
-
-

Admin Login (dev only)

- {error &&
{error}
} -
-
- - setUsername(e.target.value)} - className="w-full border rounded px-3 py-2" - autoFocus - /> -
-
- - setPassword(e.target.value)} - type="password" - className="w-full border rounded px-3 py-2" - /> -
-
-
username: admin / password: secret
- -
-
+ ))} +
-
- ); - } - return ( -
-
-
-

Admin — Prompt & Model Settings

-
- - + {/* Error Banner */} + {error && ( +
+ {error}
-
+ )} - {loading || !settings ? ( -
Loading settings...
+ {/* Content */} + {loading ? ( +
+
+
) : ( -
- {error &&
{error}
} - {message &&
{message}
} - -
-
-
- - setSettings({ ...settings, model: e.target.value })} - className="w-full border rounded px-3 py-2" - /> -
- +
+ {activeTab === "stats" && + (stats ? (
- - - setSettings({ - ...settings, - temperature: Math.min(2, Math.max(0, parseFloat(e.target.value) || 0)), - }) - } - className="w-full border rounded px-3 py-2" - /> +

+ Statistics +

+
+
+
+ Total Users +
+
+ {stats.totalUsers} +
+
+
+
+ Total Conversations +
+
+ {stats.totalConversations} +
+
+
+
+ Total Messages +
+
+ {stats.totalMessages} +
+
+
+
+ Active Users +
+
+ {stats.activeUsers} +
+
+
- -
- - - setSettings({ - ...settings, - maxTokens: Math.max(1, parseInt(e.target.value || '1')), - }) - } - className="w-full border rounded px-3 py-2" - /> + ) : ( +
+ No statistics data available
- -
- - - setSettings({ - ...settings, - retrievalDepth: Math.max(0, parseInt(e.target.value || '0')), - }) - } - className="w-full border rounded px-3 py-2" - /> + ))} + + {activeTab === "users" && ( + + )} + + {activeTab === "settings" && + (settings ? ( + + ) : ( +
+ No settings data available
+ ))} -
- - - setSettings({ - ...settings, - rateLimit: Math.max(1, parseInt(e.target.value || '1')), - }) - } - className="w-full border rounded px-3 py-2" - /> -
-
+ {activeTab === "api-keys" && ( + + )} +
+ )} +
+
+ ); +} -
- -