fix: 9 web UI bugs - cost tracker, model badge, empty state, provider tabs, memory tools, session save, dark mode, view toggles
Some checks are pending
CI / build-check-test (push) Waiting to run

- Cost tracker: fix event type message -> message_end, handle usage.input fallback
- Model badge: update immediately on model select via onModelSelect hook
- Empty state: hide completely when hasMessages (not after LLM responds)
- Provider tabs: add renderProviderTabs() to ModelSelector content + filtering
- Memory tools: register memory_save/recall in tool index, export from web-ui, add to createTools
- Session save: save before newSession, relax shouldSaveSession to user-only, title fallback
- Dark mode: add text-foreground to memory-manager dialog + inputs
- View toggles: add tool-message and thinking-block element CSS selectors
- Empty state faded: return empty html instead of ghost mascot
This commit is contained in:
JAE 2026-03-26 23:02:54 +00:00
parent 63a773184c
commit 92a294a7a2
8 changed files with 55 additions and 24 deletions

View file

@ -4,6 +4,7 @@
/* ============================================================ /* ============================================================
Utility message visibility toggles Utility message visibility toggles
============================================================ */ ============================================================ */
#chat-wrapper.hide-tool-calls tool-message,
#chat-wrapper.hide-tool-calls [data-message-type="tool"], #chat-wrapper.hide-tool-calls [data-message-type="tool"],
#chat-wrapper.hide-tool-calls [data-tool-call], #chat-wrapper.hide-tool-calls [data-tool-call],
#chat-wrapper.hide-tool-calls .tool-call-renderer, #chat-wrapper.hide-tool-calls .tool-call-renderer,
@ -11,6 +12,7 @@
display: none !important; display: none !important;
} }
#chat-wrapper.hide-thinking thinking-block,
#chat-wrapper.hide-thinking [data-message-type="thinking"], #chat-wrapper.hide-thinking [data-message-type="thinking"],
#chat-wrapper.hide-thinking .thinking-block { #chat-wrapper.hide-thinking .thinking-block {
display: none !important; display: none !important;

View file

@ -42,11 +42,11 @@ export class CostTracker extends LitElement {
this.requestCount = 0; this.requestCount = 0;
this.modelId = agent.state.model?.id || ""; this.modelId = agent.state.model?.id || "";
this.unsubscribe = agent.subscribe((event) => { this.unsubscribe = agent.subscribe((event) => {
if (event.type === "message" && event.message.role === "assistant") { if (event.type === "message_end" && event.message.role === "assistant") {
const msg = event.message as any; const msg = event.message as any;
if (msg.usage) { if (msg.usage) {
this.inputTokens += msg.usage.inputTokens || 0; this.inputTokens += msg.usage.inputTokens || msg.usage.input || 0;
this.outputTokens += msg.usage.outputTokens || 0; this.outputTokens += msg.usage.outputTokens || msg.usage.output || 0;
this.requestCount += 1; this.requestCount += 1;
} }
} }

View file

@ -23,13 +23,7 @@ export class JaeEmptyState extends LitElement {
if (!this.visible) return html``; if (!this.visible) return html``;
if (this.faded) { if (this.faded) {
return html` return html``;
<div class="flex flex-col items-center justify-center h-full select-none"
style="pointer-events:none">
<img src="/mascot/jae-default.png" alt="JAE"
style="width:10rem;height:auto;opacity:0.04;filter:grayscale(40%)" />
</div>
`;
} }
return html` return html`

View file

@ -114,13 +114,13 @@ export class MemoryManager extends LitElement {
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click=${(e: Event) => { <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click=${(e: Event) => {
if (e.target === e.currentTarget) this.hide(); if (e.target === e.currentTarget) this.hide();
}}> }}>
<div class="bg-popover border border-border rounded-xl shadow-2xl w-full max-w-2xl mx-4 flex flex-col max-h-[80vh]"> <div class="bg-popover text-foreground border border-border rounded-xl shadow-2xl w-full max-w-2xl mx-4 flex flex-col max-h-[80vh]">
<div class="flex items-center justify-between px-6 py-4 border-b border-border shrink-0"> <div class="flex items-center justify-between px-6 py-4 border-b border-border shrink-0">
<h2 class="font-semibold text-lg">&#x1F9E0; Memory Manager</h2> <h2 class="font-semibold text-lg">&#x1F9E0; Memory Manager</h2>
<button class="text-muted-foreground hover:text-foreground" @click=${() => this.hide()}>&#x2715;</button> <button class="text-muted-foreground hover:text-foreground" @click=${() => this.hide()}>&#x2715;</button>
</div> </div>
<div class="px-6 py-3 border-b border-border shrink-0"> <div class="px-6 py-3 border-b border-border shrink-0">
<input type="text" placeholder="Filter memories..." class="w-full bg-secondary rounded-lg px-3 py-2 text-sm outline-none" <input type="text" placeholder="Filter memories..." class="w-full bg-secondary text-foreground rounded-lg px-3 py-2 text-sm outline-none"
.value=${this.filter} @input=${(e: Event) => { .value=${this.filter} @input=${(e: Event) => {
this.filter = (e.target as HTMLInputElement).value; this.filter = (e.target as HTMLInputElement).value;
}} /> }} />

View file

@ -15,13 +15,14 @@ import {
SessionListDialog, SessionListDialog,
SessionsStore, SessionsStore,
SettingsDialog, SettingsDialog,
ModelSelector,
SettingsStore, SettingsStore,
setAppStorage, setAppStorage,
} from "@jaeswift/jae-web-ui"; } from "@jaeswift/jae-web-ui";
import { html, render } from "lit"; import { html, render } from "lit";
import { Brain, Download, History, Keyboard, Plus, Settings } from "lucide"; import { Brain, Download, History, Keyboard, Plus, Settings } from "lucide";
import "./app.css"; import "./app.css";
import { createImageGenTool, createTTSTool, createWebSearchTool } from "@jaeswift/jae-web-ui"; import { createImageGenTool, createMemoryTools, createTTSTool, createWebSearchTool } from "@jaeswift/jae-web-ui";
import { icon } from "@mariozechner/mini-lit"; import { icon } from "@mariozechner/mini-lit";
import { Button } from "@mariozechner/mini-lit/dist/Button.js"; import { Button } from "@mariozechner/mini-lit/dist/Button.js";
import { Input } from "@mariozechner/mini-lit/dist/Input.js"; import { Input } from "@mariozechner/mini-lit/dist/Input.js";
@ -252,15 +253,16 @@ const generateTitle = (messages: AgentMessage[]): string => {
}; };
const shouldSaveSession = (messages: AgentMessage[]): boolean => { const shouldSaveSession = (messages: AgentMessage[]): boolean => {
const hasUserMsg = messages.some((m: any) => m.role === "user" || m.role === "user-with-attachments"); return messages.some((m: any) => m.role === "user" || m.role === "user-with-attachments");
const hasAssistantMsg = messages.some((m: any) => m.role === "assistant");
return hasUserMsg && hasAssistantMsg;
}; };
const saveSession = async () => { const saveSession = async () => {
if (!storage.sessions || !currentSessionId || !agent || !currentTitle) return; if (!storage.sessions || !currentSessionId || !agent) return;
const state = agent.state; const state = agent.state;
if (!shouldSaveSession(state.messages)) return; if (!shouldSaveSession(state.messages)) return;
if (!currentTitle) {
currentTitle = generateTitle(state.messages) || "Untitled chat";
}
try { try {
const sessionData = { const sessionData = {
id: currentSessionId, id: currentSessionId,
@ -338,11 +340,27 @@ const createAgent = async (initialState?: Partial<AgentState>) => {
createTools: async (runtimeProvidersFactory: any) => { createTools: async (runtimeProvidersFactory: any) => {
const replTool = createJavaScriptReplTool(); const replTool = createJavaScriptReplTool();
replTool.runtimeProvidersFactory = runtimeProvidersFactory; replTool.runtimeProvidersFactory = runtimeProvidersFactory;
return [replTool, createWebSearchTool(), createImageGenTool(), createTTSTool()]; return [replTool, createWebSearchTool(), createImageGenTool(), createTTSTool(), ...createMemoryTools()];
}, },
}); });
costTracker.bindAgent(agent); costTracker.bindAgent(agent);
chatPanel?.setAgent(agent); chatPanel?.setAgent(agent);
// Hook: live model badge + immediate empty state hide
requestAnimationFrame(() => {
if (chatPanel?.agentInterface) {
(chatPanel.agentInterface as any).onModelSelect = () => {
ModelSelector.open(agent.state.model, (model: any) => {
agent.setModel(model);
(chatPanel.agentInterface as any).requestUpdate();
renderApp();
});
};
(chatPanel.agentInterface as any).onBeforeSend = async () => {
hasStarted = true;
renderApp();
};
}
});
if (!currentSessionId) currentSessionId = crypto.randomUUID(); if (!currentSessionId) currentSessionId = crypto.randomUUID();
}; };
@ -366,12 +384,19 @@ const loadSession = async (sessionId: string): Promise<boolean> => {
return true; return true;
}; };
const newSession = () => { const newSession = async () => {
// Save current session before resetting
if (agent && agent.state.messages.length > 0 && currentSessionId) {
if (!currentTitle) currentTitle = generateTitle(agent.state.messages) || "Untitled chat";
await saveSession();
}
currentSessionId = undefined; currentSessionId = undefined;
currentTitle = ""; currentTitle = "";
isEditingTitle = false; isEditingTitle = false;
hasStarted = false; hasStarted = false;
createAgent().then(() => renderApp()); await createAgent();
await refreshSidebar();
renderApp();
}; };
const handleSuggestion = (e: Event) => { const handleSuggestion = (e: Event) => {
@ -548,9 +573,11 @@ ${sidebar}
}} }}
></div> ></div>
<div class="flex flex-col flex-1 min-w-0 min-h-0 relative"> <div class="flex flex-col flex-1 min-w-0 min-h-0 relative">
<div class="absolute inset-x-0 top-0 z-10 flex flex-col" style="bottom:130px;pointer-events:${hasMessages ? "none" : "auto"}" @suggestion=${handleSuggestion}> ${!hasMessages ? html`
<jae-empty-state .faded=${hasMessages} style="display:flex;flex-direction:column;flex:1;width:100%;min-height:0"></jae-empty-state> <div class="absolute inset-x-0 top-0 z-10 flex flex-col" style="bottom:130px" @suggestion=${handleSuggestion}>
<jae-empty-state style="display:flex;flex-direction:column;flex:1;width:100%;min-height:0"></jae-empty-state>
</div> </div>
` : html``}
<div id="chat-wrapper" class="flex flex-col flex-1 min-h-0" > <div id="chat-wrapper" class="flex flex-col flex-1 min-h-0" >
${chatPanel} ${chatPanel}
</div> </div>

View file

@ -246,6 +246,9 @@ export class ModelSelector extends DialogBase {
if (this.filterVision) { if (this.filterVision) {
filteredModels = filteredModels.filter(({ model }) => model.input.includes("image")); filteredModels = filteredModels.filter(({ model }) => model.input.includes("image"));
} }
if (this.filterProvider) {
filteredModels = filteredModels.filter(({ provider }) => provider === this.filterProvider);
}
// Sort: when not searching, current model first then by provider. // Sort: when not searching, current model first then by provider.
// When searching, preserve the score-based order from above, // When searching, preserve the score-based order from above,
@ -376,7 +379,9 @@ export class ModelSelector extends DialogBase {
children: html`<span class="inline-flex items-center gap-1">${icon(ImageIcon, "sm")} ${i18n("Vision")}</span>`, children: html`<span class="inline-flex items-center gap-1">${icon(ImageIcon, "sm")} ${i18n("Vision")}</span>`,
})} })}
</div> </div>
</div>
${this.renderProviderTabs()}
</div>
<!-- Scrollable model list --> <!-- Scrollable model list -->
<div class="flex-1 overflow-y-auto" ${ref(this.scrollContainerRef)}> <div class="flex-1 overflow-y-auto" ${ref(this.scrollContainerRef)}>

View file

@ -118,6 +118,7 @@ export type { ToolRenderer, ToolRenderResult } from "./tools/types.js";
export { createTTSTool, ttsTool } from "./tools/voice-tts.js"; export { createTTSTool, ttsTool } from "./tools/voice-tts.js";
// Venice / community tools // Venice / community tools
export { createWebSearchTool, webSearchTool } from "./tools/web-search.js"; export { createWebSearchTool, webSearchTool } from "./tools/web-search.js";
export { createMemoryTools, saveMemoryTool, recallMemoryTool } from "./tools/memory-tool.js";
export type { Attachment } from "./utils/attachment-utils.js"; export type { Attachment } from "./utils/attachment-utils.js";
// Utils // Utils
export { loadAttachment } from "./utils/attachment-utils.js"; export { loadAttachment } from "./utils/attachment-utils.js";

View file

@ -1,6 +1,7 @@
import type { ToolResultMessage } from "@jaeswift/jae-ai"; import type { ToolResultMessage } from "@jaeswift/jae-ai";
import "./javascript-repl.js"; // Auto-registers the renderer import "./javascript-repl.js"; // Auto-registers the renderer
import "./extract-document.js"; // Auto-registers the renderer import "./extract-document.js";
import "./memory-tool.js"; // Auto-registers the renderer
import { getToolRenderer, registerToolRenderer } from "./renderer-registry.js"; import { getToolRenderer, registerToolRenderer } from "./renderer-registry.js";
import { BashRenderer } from "./renderers/BashRenderer.js"; import { BashRenderer } from "./renderers/BashRenderer.js";
import { DefaultRenderer } from "./renderers/DefaultRenderer.js"; import { DefaultRenderer } from "./renderers/DefaultRenderer.js";
@ -48,3 +49,4 @@ export { getToolRenderer, registerToolRenderer };
export { createImageGenTool, type ImageGenDetails, imageGenTool } from "./image-gen.js"; export { createImageGenTool, type ImageGenDetails, imageGenTool } from "./image-gen.js";
export { createTTSTool, type TTSDetails, ttsTool } from "./voice-tts.js"; export { createTTSTool, type TTSDetails, ttsTool } from "./voice-tts.js";
export { createWebSearchTool, type WebSearchDetails, type WebSearchResult, webSearchTool } from "./web-search.js"; export { createWebSearchTool, type WebSearchDetails, type WebSearchResult, webSearchTool } from "./web-search.js";
export { createMemoryTools, saveMemoryTool, recallMemoryTool } from "./memory-tool.js";