feat(webui): polish chat layout and titles

Align the WebUI sidebar and chat chrome with the updated design, and generate WebUI session titles asynchronously without blocking turns.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Xubin Ren
2026-05-06 22:20:35 +08:00
committed by Xubin Ren
co-authored by Cursor
parent d8fd4c80bf
commit 790a03ec28
33 changed files with 1270 additions and 312 deletions
+29 -31
View File
@@ -25,7 +25,7 @@ type BootState =
};
const SIDEBAR_STORAGE_KEY = "nanobot-webui.sidebar";
const SIDEBAR_WIDTH = 279;
const SIDEBAR_WIDTH = 272;
type ShellView = "chat" | "settings";
function readSidebarOpen(): boolean {
@@ -99,13 +99,6 @@ export default function App() {
return (
<div className="flex h-full w-full items-center justify-center">
<div className="flex flex-col items-center gap-3 animate-in fade-in-0 duration-300">
<img
src="/brand/nanobot_icon.png"
alt=""
className="h-10 w-10 animate-pulse select-none"
aria-hidden
draggable={false}
/>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-foreground/40" />
@@ -121,13 +114,6 @@ export default function App() {
return (
<div className="flex h-full w-full items-center justify-center px-4 text-center">
<div className="flex max-w-md flex-col items-center gap-3">
<img
src="/brand/nanobot_icon.png"
alt=""
className="h-10 w-10 opacity-60 grayscale select-none"
aria-hidden
draggable={false}
/>
<p className="text-lg font-semibold">{t("app.error.title")}</p>
<p className="text-sm text-muted-foreground">{state.message}</p>
<p className="text-xs text-muted-foreground">
@@ -213,7 +199,7 @@ function Shell({ onModelNameChange }: { onModelNameChange: (modelName: string |
}
}, []);
const onNewChat = useCallback(async () => {
const onCreateChat = useCallback(async () => {
try {
const chatId = await createChat();
setActiveKey(`websocket:${chatId}`);
@@ -226,6 +212,12 @@ function Shell({ onModelNameChange }: { onModelNameChange: (modelName: string |
}
}, [createChat]);
const onNewChat = useCallback(() => {
setActiveKey(null);
setView("chat");
setMobileSidebarOpen(false);
}, []);
const onSelectChat = useCallback(
(key: string) => {
setActiveKey(key);
@@ -235,6 +227,15 @@ function Shell({ onModelNameChange }: { onModelNameChange: (modelName: string |
[],
);
const onOpenSettings = useCallback(() => {
setView("settings");
setMobileSidebarOpen(false);
}, []);
const onTurnEnd = useCallback(() => {
void refresh();
}, [refresh]);
const onConfirmDelete = useCallback(async () => {
if (!pendingDelete) return;
const key = pendingDelete.key;
@@ -254,7 +255,8 @@ function Shell({ onModelNameChange }: { onModelNameChange: (modelName: string |
}, [pendingDelete, deleteChat, activeKey, sessions]);
const headerTitle = activeSession
? activeSession.preview ||
? activeSession.title ||
activeSession.preview ||
t("chat.fallbackTitle", { id: activeSession.chatId.slice(0, 6) })
: t("app.brand");
@@ -268,20 +270,10 @@ function Shell({ onModelNameChange }: { onModelNameChange: (modelName: string |
sessions,
activeKey,
loading,
theme,
onToggleTheme: toggle,
onNewChat: () => {
void onNewChat();
},
onNewChat,
onSelect: onSelectChat,
onRefresh: () => void refresh(),
onRequestDelete: (key: string, label: string) =>
setPendingDelete({ key, label }),
activeView: view,
onOpenSettings: () => {
setView("settings" as const);
setMobileSidebarOpen(false);
},
};
return (
@@ -296,10 +288,11 @@ function Shell({ onModelNameChange }: { onModelNameChange: (modelName: string |
>
<div
className={cn(
"absolute inset-y-0 left-0 h-full w-[279px] overflow-hidden bg-sidebar shadow-inner-right",
"absolute inset-y-0 left-0 h-full overflow-hidden bg-sidebar shadow-inner-right",
"transition-transform duration-300 ease-out",
desktopSidebarOpen ? "translate-x-0" : "-translate-x-full",
)}
style={{ width: SIDEBAR_WIDTH }}
>
<Sidebar {...sidebarProps} onCollapse={closeDesktopSidebar} />
</div>
@@ -312,7 +305,8 @@ function Shell({ onModelNameChange }: { onModelNameChange: (modelName: string |
<SheetContent
side="left"
showCloseButton={false}
className="w-[279px] p-0 sm:max-w-[279px] lg:hidden"
className="p-0 lg:hidden"
style={{ width: SIDEBAR_WIDTH, maxWidth: SIDEBAR_WIDTH }}
>
<Sidebar {...sidebarProps} onCollapse={closeMobileSidebar} />
</SheetContent>
@@ -331,8 +325,12 @@ function Shell({ onModelNameChange }: { onModelNameChange: (modelName: string |
session={activeSession}
title={headerTitle}
onToggleSidebar={toggleSidebar}
onGoHome={() => setActiveKey(null)}
onNewChat={onNewChat}
onCreateChat={onCreateChat}
onTurnEnd={onTurnEnd}
theme={theme}
onToggleTheme={toggle}
onOpenSettings={onOpenSettings}
hideSidebarToggleOnDesktop={desktopSidebarOpen}
/>
)}