feat: add mascot images, empty state, and utility message toggle
Some checks are pending
CI / build-check-test (push) Waiting to run
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:
parent
4cdf01ba9e
commit
21ff41fc77
9 changed files with 257 additions and 3 deletions
BIN
packages/web-ui/example/public/mascot/jae-camo.png
Normal file
BIN
packages/web-ui/example/public/mascot/jae-camo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 214 KiB |
BIN
packages/web-ui/example/public/mascot/jae-default.png
Normal file
BIN
packages/web-ui/example/public/mascot/jae-default.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 729 KiB |
BIN
packages/web-ui/example/public/mascot/jae-fire.png
Normal file
BIN
packages/web-ui/example/public/mascot/jae-fire.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 268 KiB |
BIN
packages/web-ui/example/public/mascot/jae-point-self.png
Normal file
BIN
packages/web-ui/example/public/mascot/jae-point-self.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 197 KiB |
BIN
packages/web-ui/example/public/mascot/jae-point-up.png
Normal file
BIN
packages/web-ui/example/public/mascot/jae-point-up.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 214 KiB |
|
|
@ -1 +1,60 @@
|
|||
@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;
|
||||
}
|
||||
|
|
|
|||
53
packages/web-ui/example/src/components/empty-state.ts
Normal file
53
packages/web-ui/example/src/components/empty-state.ts
Normal 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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
105
packages/web-ui/example/src/components/utility-toggle.ts
Normal file
105
packages/web-ui/example/src/components/utility-toggle.ts
Normal 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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +36,10 @@ import "./components/command-palette.js";
|
|||
import "./components/keyboard-shortcuts.js";
|
||||
import "./components/memory-manager.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
|
||||
registerCustomMessageRenderers();
|
||||
|
|
@ -80,6 +84,17 @@ const commandPalette = document.createElement("command-palette") as CommandPalet
|
|||
const keyboardShortcuts = document.createElement("keyboard-shortcuts") as KeyboardShortcuts;
|
||||
const memoryManager = document.createElement("memory-manager") as MemoryManager;
|
||||
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(keyboardShortcuts);
|
||||
document.body.appendChild(memoryManager);
|
||||
|
|
@ -275,6 +290,17 @@ const newSession = () => {
|
|||
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
|
||||
// ============================================================================
|
||||
|
|
@ -328,7 +354,7 @@ const renderApp = () => {
|
|||
});
|
||||
}}
|
||||
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>
|
||||
|
||||
|
|
@ -349,15 +375,26 @@ const renderApp = () => {
|
|||
${Button({ variant: "ghost", size: "sm",
|
||||
children: html`<span class="text-xs font-mono px-1">⌘K</span>`,
|
||||
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"),
|
||||
onClick: () => SettingsDialog.open([new ProvidersModelsTab(), new ProxyTab()]),
|
||||
title: "Settings" })}
|
||||
</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 -->
|
||||
${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>
|
||||
`;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue