feat(webui): wire version badge + system config dialog into header, admin gate

This commit is contained in:
m4
2026-08-11 15:07:53 +08:00
parent 598b2cfdc4
commit c6a2c347f8
+58 -25
View File
@@ -1,13 +1,13 @@
"use client";
import React, { useState, useEffect, useCallback, Suspense } from "react";
import Image from "next/image";
import { useQueryState } from "nuqs";
import { AccountDialog } from "@/app/components/AccountDialog";
import { ChangePasswordDialog } from "@/app/components/ChangePasswordDialog";
import { ConfigDialog } from "@/app/components/ConfigDialog";
import { ErrorLogBell } from "@/app/components/ErrorLogBell";
import { ImageModelsDialog } from "@/app/components/ImageModelsDialog";
import { SystemConfigDialog } from "@/app/components/system-config/SystemConfigDialog";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
@@ -29,6 +29,7 @@ import {
PanelRight,
PanelRightClose,
LogOut,
ServerCog,
SlidersHorizontal,
UserRound,
} from "lucide-react";
@@ -44,9 +45,15 @@ import { SkillsMarketplace } from "@/app/components/SkillsMarketplace";
import { MemoryPanel } from "@/app/components/MemoryPanel";
import { ScheduledTasksPanel } from "@/app/components/ScheduledTasksPanel";
import { ThemeToggle } from "@/app/components/ThemeToggle";
import { VersionBadge } from "@/app/components/VersionBadge";
import { HealthIndicator } from "@/app/components/HealthIndicator";
import { InspectorPanel } from "@/app/components/InspectorPanel";
import { setThreadAutoApprove } from "@/lib/autoApprove";
import {
DEFAULT_WORDMARK,
logoSrc,
usePublicSystemConfig,
} from "@/app/hooks/usePublicSystemConfig";
import type { MainChatReporter } from "@/lib/asyncAgents";
import { cn } from "@/lib/utils";
@@ -54,9 +61,11 @@ const FALLBACK_ASSISTANT_ID = "EvoScientist";
interface HomePageInnerProps {
authEnabled: boolean;
isAdmin: boolean;
}
function HomePageInner({ authEnabled }: HomePageInnerProps) {
function HomePageInner({ authEnabled, isAdmin }: HomePageInnerProps) {
const publicConfig = usePublicSystemConfig();
const [threadId, setThreadId] = useQueryState("threadId");
const [sidebar, setSidebar] = useQueryState("sidebar");
const [view, setView] = useQueryState("view");
@@ -75,6 +84,7 @@ function HomePageInner({ authEnabled }: HomePageInnerProps) {
const [changePasswordOpen, setChangePasswordOpen] = useState(false);
const [modelConfigOpen, setModelConfigOpen] = useState(false);
const [imageModelsOpen, setImageModelsOpen] = useState(false);
const [systemConfigOpen, setSystemConfigOpen] = useState(false);
// "Submit a message on the main thread" — registered by ChatInterface (only
// while it's mounted, i.e. on the chat view), used by the Agents board to loop
// an async result back to the main agent. Null when not on the chat view.
@@ -293,25 +303,31 @@ function HomePageInner({ authEnabled }: HomePageInnerProps) {
open={imageModelsOpen}
onOpenChange={setImageModelsOpen}
/>
<SystemConfigDialog
open={systemConfigOpen}
onOpenChange={setSystemConfigOpen}
/>
<div className="flex h-screen flex-col">
<header className="flex h-14 items-center justify-between gap-2 border-b border-border px-2.5 sm:px-4">
<div className="flex min-w-0 items-center gap-2 sm:gap-3">
<div className="flex min-w-0 items-center gap-2">
<Image
src="/evoscientist-logo.png"
alt="EvoScientist"
width={28}
height={28}
<img
src={logoSrc(publicConfig)}
alt={publicConfig?.wordmark ?? DEFAULT_WORDMARK}
className="size-6 shrink-0"
priority
/>
{/* Show the wordmark only when the thread sidebar is open (it
titles the panel). When collapsed, keep just the logo + the
toggle / new-chat icons for a compact header. */}
{sidebar && (
<h1 className="truncate text-base font-semibold sm:text-lg">
EvoScientist
</h1>
<div className="flex min-w-0 flex-col justify-center">
<h1 className="truncate text-sm font-semibold leading-tight sm:text-base">
{publicConfig?.wordmark ?? DEFAULT_WORDMARK}
</h1>
<div className="-ml-2 -my-1">
<VersionBadge />
</div>
</div>
)}
</div>
<div className="flex items-center gap-0.5">
@@ -394,19 +410,6 @@ function HomePageInner({ authEnabled }: HomePageInnerProps) {
/>
)}
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => setImageModelsOpen(true)}
aria-label="Image models"
title="Image models"
className="size-8"
>
<ImageIcon
className="size-5"
aria-hidden="true"
/>
</Button>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
@@ -449,6 +452,16 @@ function HomePageInner({ authEnabled }: HomePageInnerProps) {
<SlidersHorizontal aria-hidden="true" />
Model configuration
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setImageModelsOpen(true)}>
<ImageIcon aria-hidden="true" />
Image models
</DropdownMenuItem>
{isAdmin && (
<DropdownMenuItem onSelect={() => setSystemConfigOpen(true)}>
<ServerCog aria-hidden="true" />
System config
</DropdownMenuItem>
)}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
@@ -602,6 +615,7 @@ function HomePageInner({ authEnabled }: HomePageInnerProps) {
function HomePageContent() {
const [authEnabled, setAuthEnabled] = useState(false);
const [isAdmin, setIsAdmin] = useState(false);
useEffect(() => {
let cancelled = false;
@@ -618,7 +632,26 @@ function HomePageContent() {
};
}, []);
return <HomePageInner authEnabled={authEnabled} />;
useEffect(() => {
if (!authEnabled) {
setIsAdmin(true); // auth disabled → every request acts as local admin
return;
}
let cancelled = false;
fetch("/api/auth/me", { cache: "no-store" })
.then((response) => (response.ok ? response.json() : null))
.then((body: { role?: unknown } | null) => {
if (!cancelled) setIsAdmin(body?.role === "admin");
})
.catch(() => {
if (!cancelled) setIsAdmin(false);
});
return () => {
cancelled = true;
};
}, [authEnabled]);
return <HomePageInner authEnabled={authEnabled} isAdmin={isAdmin} />;
}
export default function HomePage() {