From d748e9426275da85f79eee5df38a02a556630beb Mon Sep 17 00:00:00 2001 From: "dayuan.jiang" Date: Wed, 10 Jun 2026 20:47:52 +0900 Subject: [PATCH] polish: admin panel UI improvements - Provider logos in credential rows (shared ProviderLogo component, extracted from model-config-dialog) - Scroll-spy active state in the sidebar nav - Green success state in the save bar that clears after a few seconds - Wider content column (max-w-6xl) for less wasted space on desktop --- app/[lang]/admin/page.tsx | 73 +++++++++++++++++++++++++++--- components/model-config-dialog.tsx | 40 +--------------- components/provider-logo.tsx | 36 +++++++++++++++ lib/admin/settings-registry.ts | 24 ++++++++++ 4 files changed, 129 insertions(+), 44 deletions(-) create mode 100644 components/provider-logo.tsx diff --git a/app/[lang]/admin/page.tsx b/app/[lang]/admin/page.tsx index f6db0d9..cf77222 100644 --- a/app/[lang]/admin/page.tsx +++ b/app/[lang]/admin/page.tsx @@ -12,6 +12,7 @@ import { ShieldCheck, } from "lucide-react" import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { ProviderLogo } from "@/components/provider-logo" import { Button } from "@/components/ui/button" import { Collapsible, @@ -33,8 +34,10 @@ import { SETTING_GROUPS, SETTINGS_REGISTRY, type SettingDef, + SUBGROUP_PROVIDERS, } from "@/lib/admin/settings-registry" import { getApiEndpoint } from "@/lib/base-path" +import type { ProviderName } from "@/lib/types/model-config" import { cn } from "@/lib/utils" const SESSION_PASSWORD_KEY = "next-ai-draw-io-admin-password" @@ -327,6 +330,11 @@ function ProviderSubgroup({ )} aria-hidden="true" /> + {SUBGROUP_PROVIDERS[name] && ( + + )} {name} {configured && ( @@ -365,6 +373,8 @@ export default function AdminPage() { const [errors, setErrors] = useState>({}) const [saving, setSaving] = useState(false) const [saveMessage, setSaveMessage] = useState("") + const [justSaved, setJustSaved] = useState(false) + const [activeGroup, setActiveGroup] = useState(SETTING_GROUPS[0].id) const mainRef = useRef(null) const dirtyCount = Object.keys(pending).length @@ -429,6 +439,29 @@ export default function AdminPage() { return () => window.removeEventListener("beforeunload", handler) }, [dirtyCount]) + // Highlight the section currently in view in the sidebar + useEffect(() => { + if (!authedPassword) return + const observer = new IntersectionObserver( + (entries) => { + const visible = entries + .filter((e) => e.isIntersecting) + .sort( + (a, b) => + a.boundingClientRect.top - b.boundingClientRect.top, + ) + if (visible[0]) setActiveGroup(visible[0].target.id) + }, + // Track which section heading is in the top half of the viewport + { rootMargin: "-10% 0px -50% 0px" }, + ) + for (const group of SETTING_GROUPS) { + const el = document.getElementById(group.id) + if (el) observer.observe(el) + } + return () => observer.disconnect() + }, [authedPassword]) + const handleChange = useCallback( (key: string, value: string | null) => { setSaveMessage("") @@ -494,6 +527,11 @@ export default function AdminPage() { applyResponse(data) setPending({}) setSaveMessage("Settings saved. Changes apply immediately.") + setJustSaved(true) + setTimeout(() => { + setJustSaved(false) + setSaveMessage("") + }, 4000) } catch { setSaveMessage( "Save failed: network error. Check your connection and try again.", @@ -585,7 +623,7 @@ export default function AdminPage() { return (
-
+
-
+