ui: add /public/chat route (public webchat shell)

This commit is contained in:
nzhu 2026-01-29 10:39:46 +08:00
parent 699784dbee
commit 7ad2ee3af5
4 changed files with 103 additions and 0 deletions

View File

@ -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;
}

View File

@ -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`
<div class="public-chat">
${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,
})}
</div>
`;
}
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;

View File

@ -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");
});
});

View File

@ -19,6 +19,7 @@ export type Tab =
| "skills"
| "nodes"
| "chat"
| "public-chat"
| "config"
| "debug"
| "logs";
@ -32,6 +33,7 @@ const TAB_PATHS: Record<Tab, string> = {
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":