feat: add mascot images, empty state, and utility message toggle
Some checks are pending
CI / build-check-test (push) Waiting to run

- Add 5 mascot images (default, fire, point-up, point-self, camo) to public/mascot/
- New empty state component with floating mascot, tagline, and suggestion chips
- New utility toggle component (show/hide tool calls, thinking, system msgs, timestamps)
- Mascot logo in header with wobble animation on hover
- Floating animation and orange glow for mascot in empty state
- Suggestion chips dispatch events to fill chat textarea
- CSS visibility classes for all toggle states
This commit is contained in:
JAE 2026-03-26 01:55:59 +00:00
parent 4cdf01ba9e
commit 21ff41fc77
9 changed files with 257 additions and 3 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

View file

@ -1 +1,60 @@
@import "../../dist/app.css"; @import "../../dist/app.css";
/* ============================================================
Utility message visibility toggles
============================================================ */
#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,
#chat-wrapper.hide-tool-calls .tool-result-renderer {
display: none !important;
}
#chat-wrapper.hide-thinking [data-message-type="thinking"],
#chat-wrapper.hide-thinking .thinking-block {
display: none !important;
}
#chat-wrapper.hide-system-msgs [data-message-type="system"],
#chat-wrapper.hide-system-msgs [data-message-type="system-notification"],
#chat-wrapper.hide-system-msgs .system-notification {
display: none !important;
}
#chat-wrapper.hide-timestamps .message-timestamp,
#chat-wrapper.hide-timestamps [data-timestamp] {
display: none !important;
}
/* ============================================================
Empty state mascot floating animation
============================================================ */
@keyframes jae-float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
33% { transform: translateY(-10px) rotate(-1deg); }
66% { transform: translateY(-5px) rotate(1deg); }
}
jae-empty-state img {
animation: jae-float 3.5s ease-in-out infinite;
filter: drop-shadow(0 12px 32px rgba(255, 100, 0, 0.25));
}
/* Suggestion chips hover glow */
jae-empty-state button:hover {
box-shadow: 0 0 0 1px rgba(255, 100, 0, 0.3), 0 4px 16px rgba(255, 100, 0, 0.1);
}
/* ============================================================
Header mascot wobble on hover
============================================================ */
@keyframes jae-wobble {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(-8deg); }
75% { transform: rotate(8deg); }
}
.header-logo:hover {
animation: jae-wobble 0.4s ease-in-out;
}

View file

@ -0,0 +1,53 @@
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("jae-empty-state")
export class JaeEmptyState extends LitElement {
@property({ type: Boolean }) visible = true;
protected override createRenderRoot() { return this; }
private _suggestions = [
{ icon: "💻", text: "Write me a TypeScript function that debounces API calls" },
{ icon: "🔍", text: "Search the web for the latest news on AI coding agents" },
{ icon: "🖼️", text: "Generate an image of a black dragon breathing fire" },
{ icon: "📝", text: "Explain how async/await works in JavaScript" },
{ icon: "🔧", text: "Help me debug this code and explain the issue" },
{ icon: "📊", text: "Create a Mermaid diagram of a REST API flow" },
];
override render() {
if (!this.visible) return html``;
return html`
<div class="flex flex-col items-center justify-center h-full min-h-[60vh] px-4 pb-8 select-none">
<!-- Mascot -->
<div class="relative mb-2 group">
<img
src="/mascot/jae-default.png"
alt="JAE Mascot"
class="w-40 h-auto drop-shadow-2xl transition-transform duration-300 group-hover:scale-105"
/>
</div>
<!-- Title -->
<h1 class="text-2xl font-bold text-foreground mb-1">Hey, I'm JAE</h1>
<p class="text-muted-foreground text-sm mb-8 text-center max-w-sm">
Your AI coding agent. I can write code, search the web, generate images, and a whole lot more.
</p>
<!-- Suggestion chips -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 w-full max-w-xl">
${this._suggestions.map(s => html`
<button
class="flex items-center gap-3 px-4 py-3 rounded-xl border border-border bg-secondary/50 hover:bg-secondary hover:border-primary/40 transition-all text-left text-sm group"
@click=${() => this.dispatchEvent(new CustomEvent("suggestion", { detail: s.text, bubbles: true, composed: true }))}
>
<span class="text-lg shrink-0">${s.icon}</span>
<span class="text-muted-foreground group-hover:text-foreground transition-colors leading-tight">${s.text}</span>
</button>
`)}
</div>
</div>
`;
}
}

View file

@ -0,0 +1,105 @@
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators.js";
export interface UtilityVisibility {
showToolCalls: boolean;
showThinking: boolean;
showSystemMessages: boolean;
showTimestamps: boolean;
}
@customElement("jae-utility-toggle")
export class JaeUtilityToggle extends LitElement {
@property({ type: Object }) visibility: UtilityVisibility = {
showToolCalls: true,
showThinking: false,
showSystemMessages: false,
showTimestamps: true,
};
@property({ type: Boolean }) open = false;
protected override createRenderRoot() { return this; }
private _toggle(key: keyof UtilityVisibility) {
this.visibility = { ...this.visibility, [key]: !this.visibility[key] };
this.dispatchEvent(new CustomEvent("visibility-change", {
detail: this.visibility,
bubbles: true,
composed: true,
}));
}
private _items: { key: keyof UtilityVisibility; label: string; icon: string; desc: string }[] = [
{ key: "showToolCalls", label: "Tool Calls", icon: "🔧", desc: "Show web search, image gen & other tool results" },
{ key: "showThinking", label: "Thinking", icon: "🧠", desc: "Show model reasoning / thinking blocks" },
{ key: "showSystemMessages", label: "System Messages", icon: "⚙️", desc: "Show system notifications and prompts" },
{ key: "showTimestamps", label: "Timestamps", icon: "🕐", desc: "Show message timestamps" },
];
override render() {
const activeCount = Object.values(this.visibility).filter(Boolean).length;
return html`
<div class="relative">
<!-- Toggle button -->
<button
title="Message filters"
@click=${() => { this.open = !this.open; this.requestUpdate(); }}
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs border border-border bg-secondary/60 hover:bg-secondary hover:border-primary/40 transition-all text-muted-foreground hover:text-foreground"
>
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="4" y1="6" x2="20" y2="6"/>
<line x1="8" y1="12" x2="20" y2="12"/>
<line x1="12" y1="18" x2="20" y2="18"/>
</svg>
<span>View</span>
${activeCount < 4 ? html`<span class="bg-primary text-primary-foreground rounded-full w-4 h-4 flex items-center justify-center text-[10px] font-bold">${activeCount}</span>` : html``}
</button>
<!-- Dropdown panel -->
${this.open ? html`
<div class="absolute bottom-full right-0 mb-2 w-72 rounded-xl border border-border bg-background shadow-2xl z-50 overflow-hidden">
<!-- Header -->
<div class="flex items-center gap-2 px-4 py-3 border-b border-border bg-secondary/30">
<img src="/mascot/jae-default.png" alt="JAE" class="w-7 h-auto" />
<div>
<div class="text-sm font-semibold text-foreground">Message Filters</div>
<div class="text-xs text-muted-foreground">Control what JAE shows you</div>
</div>
<button @click=${() => { this.open = false; this.requestUpdate(); }} class="ml-auto text-muted-foreground hover:text-foreground transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<!-- Items -->
<div class="p-2">
${this._items.map(item => html`
<button
class="w-full flex items-start gap-3 px-3 py-2.5 rounded-lg hover:bg-secondary/60 transition-colors text-left"
@click=${() => this._toggle(item.key)}
>
<span class="text-base mt-0.5 shrink-0">${item.icon}</span>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-foreground">${item.label}</div>
<div class="text-xs text-muted-foreground leading-tight">${item.desc}</div>
</div>
<!-- Toggle switch -->
<div class="shrink-0 mt-0.5 w-9 h-5 rounded-full transition-colors duration-200 flex items-center px-0.5 ${this.visibility[item.key] ? 'bg-primary' : 'bg-muted'}">
<div class="w-4 h-4 rounded-full bg-white shadow transition-transform duration-200 ${this.visibility[item.key] ? 'translate-x-4' : 'translate-x-0'}"></div>
</div>
</button>
`)}
</div>
<!-- Footer -->
<div class="px-4 py-2 border-t border-border bg-secondary/20 text-xs text-muted-foreground text-center">
Settings auto-save per session
</div>
</div>
<!-- Click-outside overlay -->
<div class="fixed inset-0 z-40" @click=${() => { this.open = false; this.requestUpdate(); }}></div>
` : html``}
</div>
`;
}
}

View file

@ -36,6 +36,10 @@ import "./components/command-palette.js";
import "./components/keyboard-shortcuts.js"; import "./components/keyboard-shortcuts.js";
import "./components/memory-manager.js"; import "./components/memory-manager.js";
import "./components/cost-tracker.js"; import "./components/cost-tracker.js";
import { JaeEmptyState } from "./components/empty-state.js";
import { JaeUtilityToggle, type UtilityVisibility } from "./components/utility-toggle.js";
import "./components/empty-state.js";
import "./components/utility-toggle.js";
// Register custom message renderers // Register custom message renderers
registerCustomMessageRenderers(); registerCustomMessageRenderers();
@ -80,6 +84,17 @@ const commandPalette = document.createElement("command-palette") as CommandPalet
const keyboardShortcuts = document.createElement("keyboard-shortcuts") as KeyboardShortcuts; const keyboardShortcuts = document.createElement("keyboard-shortcuts") as KeyboardShortcuts;
const memoryManager = document.createElement("memory-manager") as MemoryManager; const memoryManager = document.createElement("memory-manager") as MemoryManager;
const costTracker = document.createElement("cost-tracker") as CostTracker; const costTracker = document.createElement("cost-tracker") as CostTracker;
const utilityToggle = document.createElement("jae-utility-toggle") as JaeUtilityToggle;
utilityToggle.addEventListener("visibility-change", (e: Event) => {
const vis = (e as CustomEvent<UtilityVisibility>).detail;
const chatEl = document.getElementById("chat-wrapper");
if (chatEl) {
chatEl.classList.toggle("hide-tool-calls", !vis.showToolCalls);
chatEl.classList.toggle("hide-thinking", !vis.showThinking);
chatEl.classList.toggle("hide-system-msgs", !vis.showSystemMessages);
chatEl.classList.toggle("hide-timestamps", !vis.showTimestamps);
}
});
document.body.appendChild(commandPalette); document.body.appendChild(commandPalette);
document.body.appendChild(keyboardShortcuts); document.body.appendChild(keyboardShortcuts);
document.body.appendChild(memoryManager); document.body.appendChild(memoryManager);
@ -275,6 +290,17 @@ const newSession = () => {
window.location.href = url.toString(); window.location.href = url.toString();
}; };
// Handle suggestion chip clicks from empty state
const handleSuggestion = (e: Event) => {
const ce = e as CustomEvent<string>;
const textarea = document.querySelector('textarea') as HTMLTextAreaElement;
if (textarea) {
textarea.value = ce.detail;
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.focus();
}
};
// ============================================================================ // ============================================================================
// RENDER // RENDER
// ============================================================================ // ============================================================================
@ -328,7 +354,7 @@ const renderApp = () => {
}); });
}} }}
title="Click to edit title">${currentTitle}</button>` title="Click to edit title">${currentTitle}</button>`
: html`<span class="text-base font-semibold text-foreground">JAE Web UI</span>` : html`<div class="flex items-center gap-2"><img src="/mascot/jae-default.png" alt="JAE" class="w-7 h-auto header-logo cursor-pointer" title="JAE - Your AI Coding Agent" /><span class="text-base font-semibold text-foreground">JAE</span></div>`
} }
</div> </div>
@ -349,15 +375,26 @@ const renderApp = () => {
${Button({ variant: "ghost", size: "sm", ${Button({ variant: "ghost", size: "sm",
children: html`<span class="text-xs font-mono px-1">&#x2318;K</span>`, children: html`<span class="text-xs font-mono px-1">&#x2318;K</span>`,
onClick: () => commandPalette.show(), title: "Command Palette (Ctrl+K)" })} onClick: () => commandPalette.show(), title: "Command Palette (Ctrl+K)" })}
<theme-toggle></theme-toggle> ${utilityToggle}
<theme-toggle></theme-toggle>
${Button({ variant: "ghost", size: "sm", children: icon(Settings, "sm"), ${Button({ variant: "ghost", size: "sm", children: icon(Settings, "sm"),
onClick: () => SettingsDialog.open([new ProvidersModelsTab(), new ProxyTab()]), onClick: () => SettingsDialog.open([new ProvidersModelsTab(), new ProxyTab()]),
title: "Settings" })} title: "Settings" })}
</div> </div>
</div> </div>
<!-- Empty State -->
${agent && agent.state.messages.length === 0 ? html`
<div class="flex-1 overflow-auto"
@suggestion=${handleSuggestion}>
<jae-empty-state></jae-empty-state>
</div>
` : html``}
<!-- Chat Panel --> <!-- Chat Panel -->
${chatPanel} <div id="chat-wrapper" style="${agent && agent.state.messages.length === 0 ? 'display:none' : 'flex:1;min-height:0;display:flex;flex-direction:column'}">
${chatPanel}
</div>
</div> </div>
`; `;