ui: add /public/chat route (public webchat shell)
This commit is contained in:
parent
699784dbee
commit
7ad2ee3af5
@ -607,3 +607,16 @@
|
|||||||
grid-template-columns: 1fr;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -86,6 +86,80 @@ import { loadLogs } from "./controllers/logs";
|
|||||||
const AVATAR_DATA_RE = /^data:/i;
|
const AVATAR_DATA_RE = /^data:/i;
|
||||||
const AVATAR_HTTP_RE = /^https?:\/\//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 {
|
function resolveAssistantAvatarUrl(state: AppViewState): string | undefined {
|
||||||
const list = state.agentsList?.agents ?? [];
|
const list = state.agentsList?.agents ?? [];
|
||||||
const parsed = parseAgentSessionKey(state.sessionKey);
|
const parsed = parseAgentSessionKey(state.sessionKey);
|
||||||
@ -102,6 +176,10 @@ function resolveAssistantAvatarUrl(state: AppViewState): string | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function renderApp(state: AppViewState) {
|
export function renderApp(state: AppViewState) {
|
||||||
|
if (state.tab === "public-chat") {
|
||||||
|
return renderPublicChat(state);
|
||||||
|
}
|
||||||
|
|
||||||
const presenceCount = state.presenceEntries.length;
|
const presenceCount = state.presenceEntries.length;
|
||||||
const sessionsCount = state.sessionsResult?.count ?? null;
|
const sessionsCount = state.sessionsResult?.count ?? null;
|
||||||
const cronNext = state.cronStatus?.nextWakeAtMs ?? null;
|
const cronNext = state.cronStatus?.nextWakeAtMs ?? null;
|
||||||
|
|||||||
@ -129,6 +129,7 @@ describe("pathForTab", () => {
|
|||||||
describe("tabFromPath", () => {
|
describe("tabFromPath", () => {
|
||||||
it("returns tab for valid path", () => {
|
it("returns tab for valid path", () => {
|
||||||
expect(tabFromPath("/chat")).toBe("chat");
|
expect(tabFromPath("/chat")).toBe("chat");
|
||||||
|
expect(tabFromPath("/public/chat")).toBe("public-chat");
|
||||||
expect(tabFromPath("/overview")).toBe("overview");
|
expect(tabFromPath("/overview")).toBe("overview");
|
||||||
expect(tabFromPath("/sessions")).toBe("sessions");
|
expect(tabFromPath("/sessions")).toBe("sessions");
|
||||||
});
|
});
|
||||||
@ -148,6 +149,7 @@ describe("tabFromPath", () => {
|
|||||||
|
|
||||||
it("is case-insensitive", () => {
|
it("is case-insensitive", () => {
|
||||||
expect(tabFromPath("/CHAT")).toBe("chat");
|
expect(tabFromPath("/CHAT")).toBe("chat");
|
||||||
|
expect(tabFromPath("/Public/Chat")).toBe("public-chat");
|
||||||
expect(tabFromPath("/Overview")).toBe("overview");
|
expect(tabFromPath("/Overview")).toBe("overview");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export type Tab =
|
|||||||
| "skills"
|
| "skills"
|
||||||
| "nodes"
|
| "nodes"
|
||||||
| "chat"
|
| "chat"
|
||||||
|
| "public-chat"
|
||||||
| "config"
|
| "config"
|
||||||
| "debug"
|
| "debug"
|
||||||
| "logs";
|
| "logs";
|
||||||
@ -32,6 +33,7 @@ const TAB_PATHS: Record<Tab, string> = {
|
|||||||
skills: "/skills",
|
skills: "/skills",
|
||||||
nodes: "/nodes",
|
nodes: "/nodes",
|
||||||
chat: "/chat",
|
chat: "/chat",
|
||||||
|
"public-chat": "/public/chat",
|
||||||
config: "/config",
|
config: "/config",
|
||||||
debug: "/debug",
|
debug: "/debug",
|
||||||
logs: "/logs",
|
logs: "/logs",
|
||||||
@ -103,6 +105,7 @@ export function inferBasePathFromPathname(pathname: string): string {
|
|||||||
export function iconForTab(tab: Tab): IconName {
|
export function iconForTab(tab: Tab): IconName {
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case "chat":
|
case "chat":
|
||||||
|
case "public-chat":
|
||||||
return "messageSquare";
|
return "messageSquare";
|
||||||
case "overview":
|
case "overview":
|
||||||
return "barChart";
|
return "barChart";
|
||||||
@ -133,6 +136,8 @@ export function titleForTab(tab: Tab) {
|
|||||||
switch (tab) {
|
switch (tab) {
|
||||||
case "overview":
|
case "overview":
|
||||||
return "Overview";
|
return "Overview";
|
||||||
|
case "public-chat":
|
||||||
|
return "Chat";
|
||||||
case "channels":
|
case "channels":
|
||||||
return "Channels";
|
return "Channels";
|
||||||
case "instances":
|
case "instances":
|
||||||
@ -146,6 +151,7 @@ export function titleForTab(tab: Tab) {
|
|||||||
case "nodes":
|
case "nodes":
|
||||||
return "Nodes";
|
return "Nodes";
|
||||||
case "chat":
|
case "chat":
|
||||||
|
case "public-chat":
|
||||||
return "Chat";
|
return "Chat";
|
||||||
case "config":
|
case "config":
|
||||||
return "Config";
|
return "Config";
|
||||||
@ -162,6 +168,8 @@ export function subtitleForTab(tab: Tab) {
|
|||||||
switch (tab) {
|
switch (tab) {
|
||||||
case "overview":
|
case "overview":
|
||||||
return "Gateway status, entry points, and a fast health read.";
|
return "Gateway status, entry points, and a fast health read.";
|
||||||
|
case "public-chat":
|
||||||
|
return "Public web chat (mobile-friendly).";
|
||||||
case "channels":
|
case "channels":
|
||||||
return "Manage channels and settings.";
|
return "Manage channels and settings.";
|
||||||
case "instances":
|
case "instances":
|
||||||
@ -176,6 +184,8 @@ export function subtitleForTab(tab: Tab) {
|
|||||||
return "Paired devices, capabilities, and command exposure.";
|
return "Paired devices, capabilities, and command exposure.";
|
||||||
case "chat":
|
case "chat":
|
||||||
return "Direct gateway chat session for quick interventions.";
|
return "Direct gateway chat session for quick interventions.";
|
||||||
|
case "public-chat":
|
||||||
|
return "A minimal chat UI intended for sharing.";
|
||||||
case "config":
|
case "config":
|
||||||
return "Edit ~/.clawdbot/moltbot.json safely.";
|
return "Edit ~/.clawdbot/moltbot.json safely.";
|
||||||
case "debug":
|
case "debug":
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user