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
============================================================ */
#chat-wrapper.hide-tool-calls tool-message,
#chat-wrapper.hide-tool-calls [data-message-type="tool"],
#chat-wrapper.hide-tool-calls [data-tool-call],
#chat-wrapper.hide-tool-calls .tool-call-renderer,
@ -11,6 +12,7 @@
display: none !important;
}
#chat-wrapper.hide-thinking thinking-block,
#chat-wrapper.hide-thinking [data-message-type="thinking"],
#chat-wrapper.hide-thinking .thinking-block {
display: none !important;

View file

@ -42,11 +42,11 @@ export class CostTracker extends LitElement {
this.requestCount = 0;
this.modelId = agent.state.model?.id || "";
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;
if (msg.usage) {
this.inputTokens += msg.usage.inputTokens || 0;
this.outputTokens += msg.usage.outputTokens || 0;
this.inputTokens += msg.usage.inputTokens || msg.usage.input || 0;
this.outputTokens += msg.usage.outputTokens || msg.usage.output || 0;
this.requestCount += 1;
}
}

View file

@ -23,13 +23,7 @@ export class JaeEmptyState extends LitElement {
if (!this.visible) return html``;
if (this.faded) {
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) => {
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">
<h2 class="font-semibold text-lg">&#x1F9E0; Memory Manager</h2>
<button class="text-muted-foreground hover:text-foreground" @click=${() => this.hide()}>&#x2715;</button>
</div>
<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) => {
this.filter = (e.target as HTMLInputElement).value;
}} />

View file

@ -15,13 +15,14 @@ import {
SessionListDialog,
SessionsStore,
SettingsDialog,
ModelSelector,
SettingsStore,
setAppStorage,
} from "@jaeswift/jae-web-ui";
import { html, render } from "lit";
import { Brain, Download, History, Keyboard, Plus, Settings } from "lucide";
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 { Button } from "@mariozechner/mini-lit/dist/Button.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 hasUserMsg = messages.some((m: any) => m.role === "user" || m.role === "user-with-attachments");
const hasAssistantMsg = messages.some((m: any) => m.role === "assistant");
return hasUserMsg && hasAssistantMsg;
return messages.some((m: any) => m.role === "user" || m.role === "user-with-attachments");
};
const saveSession = async () => {
if (!storage.sessions || !currentSessionId || !agent || !currentTitle) return;
if (!storage.sessions || !currentSessionId || !agent) return;
const state = agent.state;
if (!shouldSaveSession(state.messages)) return;
if (!currentTitle) {
currentTitle = generateTitle(state.messages) || "Untitled chat";
}
try {
const sessionData = {
id: currentSessionId,
@ -338,11 +340,27 @@ const createAgent = async (initialState?: Partial<AgentState>) => {
createTools: async (runtimeProvidersFactory: any) => {
const replTool = createJavaScriptReplTool();
replTool.runtimeProvidersFactory = runtimeProvidersFactory;
return [replTool, createWebSearchTool(), createImageGenTool(), createTTSTool()];
return [replTool, createWebSearchTool(), createImageGenTool(), createTTSTool(), ...createMemoryTools()];
},
});
costTracker.bindAgent(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();
};
@ -366,12 +384,19 @@ const loadSession = async (sessionId: string): Promise<boolean> => {
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;
currentTitle = "";
isEditingTitle = false;
hasStarted = false;
createAgent().then(() => renderApp());
await createAgent();
await refreshSidebar();
renderApp();
};
const handleSuggestion = (e: Event) => {
@ -548,9 +573,11 @@ ${sidebar}
}}
></div>
<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}>
<jae-empty-state .faded=${hasMessages} style="display:flex;flex-direction:column;flex:1;width:100%;min-height:0"></jae-empty-state>
${!hasMessages ? html`
<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>
` : html``}
<div id="chat-wrapper" class="flex flex-col flex-1 min-h-0" >
${chatPanel}
</div>

View file

@ -246,6 +246,9 @@ export class ModelSelector extends DialogBase {
if (this.filterVision) {
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.
// 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>`,
})}
</div>
</div>
${this.renderProviderTabs()}
</div>
<!-- Scrollable model list -->
<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";
// Venice / community tools
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";
// Utils
export { loadAttachment } from "./utils/attachment-utils.js";

View file

@ -1,6 +1,7 @@
import type { ToolResultMessage } from "@jaeswift/jae-ai";
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 { BashRenderer } from "./renderers/BashRenderer.js";
import { DefaultRenderer } from "./renderers/DefaultRenderer.js";
@ -48,3 +49,4 @@ export { getToolRenderer, registerToolRenderer };
export { createImageGenTool, type ImageGenDetails, imageGenTool } from "./image-gen.js";
export { createTTSTool, type TTSDetails, ttsTool } from "./voice-tts.js";
export { createWebSearchTool, type WebSearchDetails, type WebSearchResult, webSearchTool } from "./web-search.js";
export { createMemoryTools, saveMemoryTool, recallMemoryTool } from "./memory-tool.js";