From 167c4707705b53b7a6016f6b01a3e6800eea7761 Mon Sep 17 00:00:00 2001 From: "makayo(Mark)" Date: Thu, 27 Nov 2025 22:41:38 -0800 Subject: [PATCH 1/7] Replace Next.js config with next.config.mjs and update .gitignore - Removed next.config.ts and added next.config.mjs using ESM export style - Defined rewrites to proxy /api/* requests from Next.js frontend to FastAPI backend - Updated .gitignore to exclude Python cache files (__pycache__, *.pyc) and virtual environments - Ensured node_modules and .next remain ignored - Keeps repository clean by preventing build artifacts and compiled files from being tracked --- .gitignore | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 1380c2e..d43b7d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,35 @@ -node_modules -.next \ No newline at end of file +# 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 From d6b1a46eac857cd218ccbebcf6f0273b1b1b492e Mon Sep 17 00:00:00 2001 From: "makayo(Mark)" Date: Sat, 29 Nov 2025 22:54:32 -0800 Subject: [PATCH 2/7] chore: remove pycache files from main --- backend/__pycache__/__init__.cpython-313.pyc | Bin 178 -> 0 bytes backend/__pycache__/llm_model.cpython-313.pyc | Bin 3755 -> 0 bytes backend/__pycache__/main.cpython-313.pyc | Bin 11991 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 backend/__pycache__/__init__.cpython-313.pyc delete mode 100644 backend/__pycache__/llm_model.cpython-313.pyc delete mode 100644 backend/__pycache__/main.cpython-313.pyc diff --git a/backend/__pycache__/__init__.cpython-313.pyc b/backend/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 89e1d8883c98e5c35f968a874068dce119590d53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 178 zcmey&%ge<81kY~BXJ!ED#~=<2FhUuhS%8eG4CxG-jD9N_ikN`B&mgH=f%+NwxvBcO zi8=YnrN#O#Kr%NqucTPtJ+s84G)X@=HMz7XRo64GB(*3nF-O-qBe5hYzeLw1wKy|9 zPd_O!IXg8kML#}1GcU6wK3=b&@)n0pZhlH>PO4oIE6@gz3yMLEkIamWj77{q764QX BF2Vo+ diff --git a/backend/__pycache__/llm_model.cpython-313.pyc b/backend/__pycache__/llm_model.cpython-313.pyc deleted file mode 100644 index acfb970d1d05ff7fa465e325ca45b0b397900382..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3755 zcmb7GUrZax8K3p~?|N;(7!wS}8c;HOxAXnxn{Q^m-}kZQ@o)&*m*sHw4{n71O+U=mSQED9ScE=8A`+R4h|u10k-1Swr{2rX1)RyZ+*qg18a*e;{EpXkJao*4X|&H)QN7e zAsk$P22;`r+WFQ_rh)BZ;fu`r6CK-^M&Cw+L zI_rH*4@x$~m;ok!!bI=($vV?R5=;i;-`mxgQh!YlQ2RH2VyoDYY!iLSkl2`P7ya-w zCBtF>`ex`4fsW~N!0omq+xmQBFo>>MuQ5cJWg=}asLe|ei{Tk6=;~$lI##lam>7Qg zIj&}~d`>09QbCjCvGK*$NmW%ik-!(iZ>0R@^(H_G}XBzIR3#p90kjDm_%WDRk z&KJJvWkSm;vqA>XXVZ94xEx^&XF1sZeH(VJtrAfKGj13>bd8}u3*mf$96-m|?4EGHp0c_izThmE4pOR*DxF0*g zU`I(i&ESBZhldhyi$LQ02qkuCn1N(FSxaaT28;@U#Rh)6sd~@6 zrd_-#U3H62x?#~JawjbczvqmP!>BvKp8~D)9XMk?zLPf0*%Tr2=2{|8ErAXGO}B9r zB^*2V7)0pDKd-I;zP);dgyY+{&Pt<}S5iCf;9G3Gl1-VnWuBbO;oSDUxrNYptAZc1w(=y;W_R{HOL^h=;F9- zD}#n5uLDWF+pJ0Hz(@8j>#9%F9_j;vqn2^jhBu)G0)0L>oRx(vWsiSp~c( zr0LNaZa8|`Oe*%Qk}v3*!Knou1lX%)xT*jh-_$jFGQ!?w#JlIz9PZ9ZvYIYv-7$FQ zu%c_-=d=0`3e(*p6a<31MiuZKl2|pnPOExX3~Sk0rF&XRLo&>C@6?fKe!=jVyM>Fn zsgq7vl$Kwu8xC{{z4RmA_uPAEwWV*(+rR4U|I*>BSQ*bSQ(=*-wZbD$xaLFlRI#JFwoM1?Y1;&(zv4yBp$GDP`8Ue4`wLIt-p`)&6+Q2kJg17D zQ-3+P<{4f(_l3)QH}uQUhvDZvOZ7srURVh}8hRM|YizY%Sgjvj8hvR;&c=HSE2C@8 zU1fjh!NmQEmC;8ZtoeJN94`9%mR%Jm;+yWCy>oWW-L`6Nd+9)2)5ooAExjiC;**9o z|A{9p5zZwnbzr7oc9b^A~jEfC8B90fQVQBVuzoqTz+1Y>o zgSP*iy@3y|cPdq@ E*sENd0HNLkb8{GIrY-B8TX=r?egsIvwBde0$J7DnJnu}M5 z;^#%f0ahhlCx){YIvSyM!l;{pOaoDj!R4%iGbBVuT|2BbTv*k%8A{qgMLMY7isvO6 zwgUE8x)ED-QNuc|s&X~%2&7N61rH*StdZe74TLPn`1|A>d{TmI&){8Q8HU-iBgXSN zI{gf}o+0jE$hK-fvLb#m^>At(^*yDZk+R)+``WE*x0PGUYSZyGd*@d$Fg-ubg$+3E$Z zx`eW?z1n{+bUd`I4E&x0Ix}`K!q{(fx42*M6%MtA$}Mf>#y~j`EDJ}=huX^R9h(ik zOsLG)RaodZ{LK{`^f1lWTyandNYqeqQHewK{)(GQJZfpLc&Ow>zCfjpO7+OsbWgt@ TtWbLuAF{h%HiAS(;FA9bz#3bj diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc deleted file mode 100644 index ba9aa62dbf01d843e47a82a3877efe42746c3708..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11991 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 IXk)zdza`BZZ~y=R From 52642d7e9bfae1c7cc88862888c71b1dcfb02ba8 Mon Sep 17 00:00:00 2001 From: "makayo(Mark)" Date: Thu, 4 Dec 2025 13:21:51 -0800 Subject: [PATCH 3/7] Implement admin endpoints for stats, users, and settings - Introduced /admin/stats route with placeholder JSON for system metrics - Added /admin/users route returning empty user list - Added /admin/settings route returning empty settings object - Ensures admin endpoints are visible in FastAPI docs for future expansion --- backend/main.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/backend/main.py b/backend/main.py index 6ae2d83..67c7f18 100644 --- a/backend/main.py +++ b/backend/main.py @@ -220,6 +220,28 @@ def get_conversation(conversation_id: str, request: Request): ) +# --- Admin Endpoints --- + +@app.get("/admin/stats") +def get_admin_stats(): + # Placeholder until real stats logic is wired in + return { + "totalUsers": 1, + "totalConversations": 0, + "totalMessages": 0, + "activeUsers": 0, + } + +@app.get("/admin/users") +def get_admin_users(): + return {"users": []} + +@app.get("/admin/settings") +def get_admin_settings(): + return {"settings": {}} + + + @app.post("/api/chat/message", response_model=SendMessageResponse) def chat_with_llm(req: SendMessageRequest, request: Request): user_id = _get_user_id(request) From 05379993e09b00b979d3366e18b35d29a04081f2 Mon Sep 17 00:00:00 2001 From: "makayo(Mark)" Date: Wed, 17 Dec 2025 12:20:08 -0800 Subject: [PATCH 4/7] feat: Complete admin dashboard with LLM control settings, API key management, and user management - Add Overview, Users, LLM Settings, and API Keys tabs to admin dashboard - Implement LLM Control Settings: model dropdown, temperature slider, max tokens, rate limit, system prompt - Add API Key Management: secure key generation (sk_ prefixed), key hashing, masked display (last 4 chars) - Create API endpoints: /admin/stats, /admin/settings, /admin/users, /admin/api-keys - Implement mock auth bypass for development to prevent 401 errors - Fix race condition in ProtectedRoute to wait for auth initialization - Add recovery flows (password reset, username recovery) - Wire frontend API client to backend with proper error handling and confirmation messages - All settings persist in-memory on backend with optional usage logging --- app/admin/page.tsx | 393 ++++++---- app/chat/page.tsx | 190 ++--- app/login/page.tsx | 52 +- app/recovery/page.tsx | 138 ++++ app/recovery/reset-password/[token]/page.tsx | 307 ++++++++ app/recovery/reset-password/page.tsx | 182 +++++ app/recovery/username/page.tsx | 185 +++++ backend/main.py | 641 +++++++++++++++- components/admin/ApiKeyManagement.tsx | 246 ++++++ components/admin/UserManagement.tsx | 759 +++++++++++++++++++ components/auth/ProtectedRoute.tsx | 31 +- components/layout/Header.tsx | 128 ++++ conda.bat | 0 contexts/AuthContext.tsx | 88 ++- deactivate | 0 lib/api/admin.ts | 55 +- lib/api/auth.ts | 21 +- package-lock.json | 12 + 18 files changed, 3101 insertions(+), 327 deletions(-) create mode 100644 app/recovery/page.tsx create mode 100644 app/recovery/reset-password/[token]/page.tsx create mode 100644 app/recovery/reset-password/page.tsx create mode 100644 app/recovery/username/page.tsx create mode 100644 components/admin/ApiKeyManagement.tsx create mode 100644 components/admin/UserManagement.tsx create mode 100644 components/layout/Header.tsx create mode 100644 conda.bat create mode 100644 deactivate diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 03a41b7..59b9f42 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -1,21 +1,70 @@ -'use client'; +"use client"; -import { useState, useEffect } from 'react'; -import { useRouter } 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 { 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 { user } = useAuth(); + const { logout } = useAuth(); const router = useRouter(); - const [activeTab, setActiveTab] = useState<'stats' | 'users' | 'settings'>('stats'); + const searchParams = useSearchParams(); + const [activeTab, setActiveTab] = useState("stats"); const [stats, setStats] = useState(null); const [users, setUsers] = useState([]); const [settings, setSettings] = useState(null); + const [modelOptions, setModelOptions] = useState(DEFAULT_MODELS); + const [apiKeys, setApiKeys] = useState([]); const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); + const [error, setError] = useState(""); + + const navButtons = useMemo(() => TAB_CONFIG, []); + + useEffect(() => { + const tabParam = searchParams.get("tab") as TabKey | null; + if ( + tabParam && + ["stats", "users", "settings", "api-keys"].includes(tabParam) + ) { + setActiveTab(tabParam); + } + }, [searchParams]); useEffect(() => { loadData(); @@ -23,87 +72,131 @@ function AdminPageContent() { const loadData = async () => { setLoading(true); - setError(''); + setError(""); try { - if (activeTab === 'stats') { + if (activeTab === "stats") { const data = await adminApi.getStats(); setStats(data); - } else if (activeTab === 'users') { + } else if (activeTab === "users") { const data = await adminApi.getUsers(); setUsers(data); - } else if (activeTab === 'settings') { - const data = await adminApi.getSystemSettings(); - setSettings(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 (err) { if (err instanceof ApiError) { - setError(err.message || 'Failed to load data'); + setError(err.message || "Failed to load data"); } else { - setError('An unexpected error occurred'); + setError("An unexpected error occurred"); } } finally { setLoading(false); } }; - const handleUpdateSettings = async (updatedSettings: Partial) => { + const handleTabChange = (tab: TabKey) => { + setActiveTab(tab); + router.replace(`/admin?tab=${tab}`); + }; + + const handleUpdateSettings = async ( + updatedSettings: Partial + ) => { if (!settings) return; try { const updated = await adminApi.updateSystemSettings(updatedSettings); setSettings(updated); } catch (err) { if (err instanceof ApiError) { - setError(err.message || 'Failed to update settings'); + setError(err.message || "Failed to update settings"); } else { - setError('An unexpected error occurred'); + setError("An unexpected error occurred"); } } }; + const handleLogout = async () => { + await logout(); + router.push("/login"); + }; + return (

+
- {/* Header */} -
- -

Admin Dashboard

-

- Manage system settings, users, and view statistics -

+ {/* Dashboard Header */} +
+
+

+ Admin Dashboard +

+

+ Central control center for the platform +

+
+
+ + {/* Dashboard Menu */} +
+ {navButtons.map((item) => { + const isActive = activeTab === item.key; + return ( + + ); + })}
{/* Tabs */}
@@ -123,7 +216,7 @@ function AdminPageContent() {
) : (
- {activeTab === 'stats' && stats && ( + {activeTab === "stats" && stats && (

Statistics @@ -165,66 +258,21 @@ function AdminPageContent() {

)} - {activeTab === 'users' && ( -
-

- Users -

-
- - - - - - - - - - - {users.map((u) => ( - - - - - - - ))} - -
- Name - - Email - - Role - - Created -
- {u.name} - - {u.email} - - - {u.role} - - - {new Date(u.createdAt).toLocaleDateString()} -
-
-
+ {activeTab === "users" && ( + )} - {activeTab === 'settings' && settings && ( + {activeTab === "settings" && settings && ( )} + + {activeTab === "api-keys" && ( + + )}
)}
@@ -234,13 +282,16 @@ function AdminPageContent() { function SystemSettingsForm({ settings, + availableModels, onUpdate, }: { settings: SystemSettings; + availableModels: string[]; onUpdate: (settings: Partial) => Promise; }) { const [formData, setFormData] = useState(settings); const [loading, setLoading] = useState(false); + const [saved, setSaved] = useState(" "); useEffect(() => { setFormData(settings); @@ -249,8 +300,15 @@ function SystemSettingsForm({ const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); + setSaved(" "); try { - await onUpdate(formData); + const payload = { + ...formData, + temperature: Math.min(2, Math.max(0, formData.temperature)), + maxTokens: Math.min(4096, Math.max(1, formData.maxTokens)), + }; + await onUpdate(payload); + setSaved("Settings saved"); } finally { setLoading(false); } @@ -258,38 +316,81 @@ function SystemSettingsForm({ return (
-

- System Settings -

+
+
+

+ LLM Control Settings +

+

+ Configure model, temperature, tokens, and limits +

+
+ {saved.trim() && ( + + {saved} + + )} +
+
- setFormData({ ...formData, model: e.target.value })} + onChange={(e) => + setFormData({ ...formData, model: e.target.value }) + } className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white" - /> + > + {Array.from( + new Set([ + ...(availableModels.length + ? availableModels + : DEFAULT_MODELS), + formData.model, + ]) + ).map((model) => ( + + ))} + +

+ Select the deployed LLM backend +

+
- - setFormData({ ...formData, temperature: parseFloat(e.target.value) }) - } - className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white" - /> +
+ + setFormData({ + ...formData, + temperature: parseFloat(e.target.value), + }) + } + className="flex-1" + /> + + {formData.temperature.toFixed(2)} + +
+

+ Controls randomness (0 = deterministic, 2 = creative) +

+
+
+