From 7ad2ee3af5b65c70f649391355b729b3bbc86386 Mon Sep 17 00:00:00 2001 From: nzhu Date: Thu, 29 Jan 2026 10:39:46 +0800 Subject: [PATCH] ui: add /public/chat route (public webchat shell) --- ui/src/styles/layout.css | 13 ++++++ ui/src/ui/app-render.ts | 78 ++++++++++++++++++++++++++++++++++++ ui/src/ui/navigation.test.ts | 2 + ui/src/ui/navigation.ts | 10 +++++ 4 files changed, 103 insertions(+) diff --git a/ui/src/styles/layout.css b/ui/src/styles/layout.css index c2a5c6fe3..878e89e30 100644 --- a/ui/src/styles/layout.css +++ b/ui/src/styles/layout.css @@ -607,3 +607,16 @@ grid-template-columns: 1fr; } } + +/* Public chat mode: minimal chrome for shareable/mobile use. */ +.public-chat { + min-height: 100vh; + display: flex; + flex-direction: column; + background: var(--bg); +} + +.public-chat .chat { + flex: 1; + min-height: 0; +} diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 422af6863..6cf9dcfd0 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -86,6 +86,80 @@ import { loadLogs } from "./controllers/logs"; const AVATAR_DATA_RE = /^data:/i; const AVATAR_HTTP_RE = /^https?:\/\//i; +function renderPublicChat(state: AppViewState) { + const chatDisabledReason = state.connected ? null : "Disconnected from gateway."; + const showThinking = state.onboarding ? false : state.settings.chatShowThinking; + const assistantAvatarUrl = resolveAssistantAvatarUrl(state); + const chatAvatarUrl = state.chatAvatarUrl ?? assistantAvatarUrl ?? null; + + return html` +
+ ${renderChat({ + sessionKey: state.sessionKey, + onSessionKeyChange: (next) => { + state.sessionKey = next; + state.chatMessage = ""; + state.chatAttachments = []; + state.chatStream = null; + state.chatStreamStartedAt = null; + state.chatRunId = null; + state.chatQueue = []; + state.resetToolStream(); + state.resetChatScroll(); + state.applySettings({ + ...state.settings, + sessionKey: next, + lastActiveSessionKey: next, + }); + void state.loadAssistantIdentity(); + void loadChatHistory(state); + void refreshChatAvatar(state); + }, + thinkingLevel: state.chatThinkingLevel, + showThinking, + loading: state.chatLoading, + sending: state.chatSending, + compactionStatus: state.compactionStatus, + assistantAvatarUrl: chatAvatarUrl, + messages: state.chatMessages, + toolMessages: state.chatToolMessages, + stream: state.chatStream, + streamStartedAt: state.chatStreamStartedAt, + draft: state.chatMessage, + queue: state.chatQueue, + connected: state.connected, + canSend: state.connected, + disabledReason: chatDisabledReason, + error: state.lastError, + // Public chat should not expose session lists. + sessions: null, + // Always "focus" (no sidebar/nav). + focusMode: true, + onRefresh: () => { + state.resetToolStream(); + return Promise.all([loadChatHistory(state), refreshChatAvatar(state)]); + }, + onToggleFocusMode: () => { + // no-op + }, + onChatScroll: (event) => state.handleChatScroll(event), + onDraftChange: (next) => (state.chatMessage = next), + attachments: state.chatAttachments, + onAttachmentsChange: (next) => (state.chatAttachments = next), + onSend: () => state.handleSendChat(), + canAbort: Boolean(state.chatRunId), + onAbort: () => void state.handleAbortChat(), + onQueueRemove: (id) => state.removeQueuedMessage(id), + onNewSession: () => state.handleSendChat("/new", { restoreDraft: true }), + // Hide tool sidebar in public chat. + sidebarOpen: false, + assistantName: state.assistantName, + assistantAvatar: state.assistantAvatar, + })} +
+ `; +} + function resolveAssistantAvatarUrl(state: AppViewState): string | undefined { const list = state.agentsList?.agents ?? []; const parsed = parseAgentSessionKey(state.sessionKey); @@ -102,6 +176,10 @@ function resolveAssistantAvatarUrl(state: AppViewState): string | undefined { } export function renderApp(state: AppViewState) { + if (state.tab === "public-chat") { + return renderPublicChat(state); + } + const presenceCount = state.presenceEntries.length; const sessionsCount = state.sessionsResult?.count ?? null; const cronNext = state.cronStatus?.nextWakeAtMs ?? null; diff --git a/ui/src/ui/navigation.test.ts b/ui/src/ui/navigation.test.ts index 7b15deb4a..eac66589d 100644 --- a/ui/src/ui/navigation.test.ts +++ b/ui/src/ui/navigation.test.ts @@ -129,6 +129,7 @@ describe("pathForTab", () => { describe("tabFromPath", () => { it("returns tab for valid path", () => { expect(tabFromPath("/chat")).toBe("chat"); + expect(tabFromPath("/public/chat")).toBe("public-chat"); expect(tabFromPath("/overview")).toBe("overview"); expect(tabFromPath("/sessions")).toBe("sessions"); }); @@ -148,6 +149,7 @@ describe("tabFromPath", () => { it("is case-insensitive", () => { expect(tabFromPath("/CHAT")).toBe("chat"); + expect(tabFromPath("/Public/Chat")).toBe("public-chat"); expect(tabFromPath("/Overview")).toBe("overview"); }); }); diff --git a/ui/src/ui/navigation.ts b/ui/src/ui/navigation.ts index 966abec96..1b48a9f43 100644 --- a/ui/src/ui/navigation.ts +++ b/ui/src/ui/navigation.ts @@ -19,6 +19,7 @@ export type Tab = | "skills" | "nodes" | "chat" + | "public-chat" | "config" | "debug" | "logs"; @@ -32,6 +33,7 @@ const TAB_PATHS: Record = { skills: "/skills", nodes: "/nodes", chat: "/chat", + "public-chat": "/public/chat", config: "/config", debug: "/debug", logs: "/logs", @@ -103,6 +105,7 @@ export function inferBasePathFromPathname(pathname: string): string { export function iconForTab(tab: Tab): IconName { switch (tab) { case "chat": + case "public-chat": return "messageSquare"; case "overview": return "barChart"; @@ -133,6 +136,8 @@ export function titleForTab(tab: Tab) { switch (tab) { case "overview": return "Overview"; + case "public-chat": + return "Chat"; case "channels": return "Channels"; case "instances": @@ -146,6 +151,7 @@ export function titleForTab(tab: Tab) { case "nodes": return "Nodes"; case "chat": + case "public-chat": return "Chat"; case "config": return "Config"; @@ -162,6 +168,8 @@ export function subtitleForTab(tab: Tab) { switch (tab) { case "overview": return "Gateway status, entry points, and a fast health read."; + case "public-chat": + return "Public web chat (mobile-friendly)."; case "channels": return "Manage channels and settings."; case "instances": @@ -176,6 +184,8 @@ export function subtitleForTab(tab: Tab) { return "Paired devices, capabilities, and command exposure."; case "chat": return "Direct gateway chat session for quick interventions."; + case "public-chat": + return "A minimal chat UI intended for sharing."; case "config": return "Edit ~/.clawdbot/moltbot.json safely."; case "debug":