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
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:
parent
63a773184c
commit
92a294a7a2
8 changed files with 55 additions and 24 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
|
|
||||||
|
|
@ -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">🧠 Memory Manager</h2>
|
<h2 class="font-semibold text-lg">🧠 Memory Manager</h2>
|
||||||
<button class="text-muted-foreground hover:text-foreground" @click=${() => this.hide()}>✕</button>
|
<button class="text-muted-foreground hover:text-foreground" @click=${() => this.hide()}>✕</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;
|
||||||
}} />
|
}} />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)}>
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue