From ef135b6d94b2901a168158ae030371da64ac06fc Mon Sep 17 00:00:00 2001 From: JAE Date: Fri, 27 Mar 2026 06:21:38 +0000 Subject: [PATCH] feat: Phase 1 mega-update - glassmorphism UI, 7 new components New components: - crypto-ticker: VVV/DIEM live prices via DexScreener (30s refresh) - persona-selector: 7 AI personas with unique system prompts - slash-commands: / autocomplete overlay for tools/skills - marketplace: toggleable skills panel with localStorage - mood-indicator: emotional states (neutral/focused/excited/warning/frustrated/angry) - typing-indicator: waveform animation matching response complexity - context-peek: hover tooltip summaries for messages UI overhaul: - Full glassmorphism design system (glass, glass-sm, glass-strong) - 10+ CSS animations (fade, scale, slide, wave, float, shake, shimmer) - Broken glass crack overlay for angry mood (5min fade) - Dark mode fixes for keyboard shortcuts - View toggles (tools/thinking/system/timestamps) now apply CSS classes - Session sidebar: 6-chat max with hover scrollbar - Auto-open browser/terminal panels on tool use - Venice as default provider - Smooth hover/click transitions on all buttons - Loading screen with floating mascot + shimmer --- packages/web-ui/example/src/app.css | 287 +++- .../example/src/components/context-peek.ts | 38 + .../example/src/components/crypto-ticker.ts | 65 + .../src/components/keyboard-shortcuts.ts | 124 +- .../example/src/components/marketplace.ts | 104 ++ .../example/src/components/mood-indicator.ts | 98 ++ .../src/components/persona-selector.ts | 67 + .../example/src/components/session-sidebar.ts | 228 ++-- .../example/src/components/slash-commands.ts | 95 ++ .../src/components/typing-indicator.ts | 42 + packages/web-ui/example/src/main.ts | 1185 +++++++---------- 11 files changed, 1393 insertions(+), 940 deletions(-) create mode 100644 packages/web-ui/example/src/components/context-peek.ts create mode 100644 packages/web-ui/example/src/components/crypto-ticker.ts create mode 100644 packages/web-ui/example/src/components/marketplace.ts create mode 100644 packages/web-ui/example/src/components/mood-indicator.ts create mode 100644 packages/web-ui/example/src/components/persona-selector.ts create mode 100644 packages/web-ui/example/src/components/slash-commands.ts create mode 100644 packages/web-ui/example/src/components/typing-indicator.ts diff --git a/packages/web-ui/example/src/app.css b/packages/web-ui/example/src/app.css index b7fb68b..5e4b60c 100644 --- a/packages/web-ui/example/src/app.css +++ b/packages/web-ui/example/src/app.css @@ -1,62 +1,263 @@ -@import "../../dist/app.css"; +@import "tailwindcss"; - -/* ============================================================ - 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, -#chat-wrapper.hide-tool-calls .tool-result-renderer { - display: none !important; +@theme { + --color-primary: #6d5acd; + --color-primary-foreground: #ffffff; + --color-secondary: #2a2a3e; + --color-secondary-foreground: #e0e0e0; + --color-background: #0f0f1a; + --color-foreground: #e8e8f0; + --color-muted: #1a1a2e; + --color-muted-foreground: #8888aa; + --color-border: #2a2a40; + --color-card: #161625; + --color-accent: #7c6fe0; } -#chat-wrapper.hide-thinking thinking-block, -#chat-wrapper.hide-thinking [data-message-type="thinking"], -#chat-wrapper.hide-thinking .thinking-block { - display: none !important; +/* ===== BASE ===== */ +html, body { + background: var(--color-background); + color: var(--color-foreground); + font-family: 'Inter', system-ui, -apple-system, sans-serif; + overflow: hidden; + height: 100vh; } -#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; +::selection { + background: rgba(109, 90, 205, 0.3); + color: var(--color-foreground); } -#chat-wrapper.hide-timestamps .message-timestamp, -#chat-wrapper.hide-timestamps [data-timestamp] { - display: none !important; +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: rgba(136,136,170,0.2); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: rgba(136,136,170,0.4); } + +/* ===== GLASSMORPHISM ===== */ +.jae-glass { + background: rgba(22, 22, 37, 0.75); + backdrop-filter: blur(20px) saturate(1.3); + -webkit-backdrop-filter: blur(20px) saturate(1.3); + border: 1px solid rgba(255, 255, 255, 0.06); +} + +.jae-glass-sm { + background: rgba(30, 30, 50, 0.5); + backdrop-filter: blur(12px) saturate(1.2); + -webkit-backdrop-filter: blur(12px) saturate(1.2); + border: 1px solid rgba(255, 255, 255, 0.04); +} + +.jae-glass-strong { + background: rgba(15, 15, 26, 0.9); + backdrop-filter: blur(30px) saturate(1.5); + -webkit-backdrop-filter: blur(30px) saturate(1.5); + border: 1px solid rgba(255, 255, 255, 0.08); +} + +/* ===== ANIMATIONS ===== */ +@keyframes jae-fade-in { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes jae-scale-in { + from { opacity: 0; transform: scale(0.95); } + to { opacity: 1; transform: scale(1); } +} + +@keyframes jae-slide-in { + from { opacity: 0; transform: translateX(-20px); } + to { opacity: 1; transform: translateX(0); } +} + +@keyframes jae-slide-up { + from { opacity: 0; transform: translateY(16px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes jae-wave { + 0% { height: 3px; } + 100% { height: 16px; } } -/* ============================================================ - 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); } + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-6px); } } -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)); +@keyframes jae-pulse-angry { + 0%, 100% { transform: scale(1); filter: brightness(1); } + 50% { transform: scale(1.1); filter: brightness(1.3) hue-rotate(-10deg); } } -/* 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); +@keyframes jae-shake { + 0%, 100% { transform: translateX(0); } + 10%, 30%, 50%, 70%, 90% { transform: translateX(-3px); } + 20%, 40%, 60%, 80% { transform: translateX(3px); } } -/* ============================================================ - Header mascot wobble on hover - ============================================================ */ -@keyframes jae-wobble { - 0%, 100% { transform: rotate(0deg); } - 25% { transform: rotate(-8deg); } - 75% { transform: rotate(8deg); } +@keyframes jae-glow-pulse { + 0%, 100% { box-shadow: 0 0 5px rgba(109, 90, 205, 0.2); } + 50% { box-shadow: 0 0 20px rgba(109, 90, 205, 0.4); } } -.header-logo:hover { - animation: jae-wobble 0.4s ease-in-out; +@keyframes jae-shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +@keyframes jae-crack-pulse { + 0%, 100% { opacity: var(--crack-opacity, 1); } + 50% { opacity: calc(var(--crack-opacity, 1) * 0.7); } +} + +.jae-fade-in { animation: jae-fade-in 0.3s ease-out; } +.jae-scale-in { animation: jae-scale-in 0.2s ease-out; } +.jae-slide-in { animation: jae-slide-in 0.3s ease-out; } +.jae-slide-up { animation: jae-slide-up 0.25s ease-out; } +.jae-float { animation: jae-float 3s ease-in-out infinite; } +.jae-shake { animation: jae-shake 0.5s ease-in-out; } + +/* ===== HOVER & CLICK TRANSITIONS ===== */ +button, [role="button"] { + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +button:active, [role="button"]:active { + transform: scale(0.97); +} + +/* ===== BROKEN GLASS OVERLAY ===== */ +.jae-cracked-overlay { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 200; + background: + radial-gradient(ellipse at 30% 40%, rgba(220,38,38,0.08) 0%, transparent 50%), + radial-gradient(ellipse at 70% 60%, rgba(220,38,38,0.05) 0%, transparent 50%); + transition: opacity 1s ease; +} + +.jae-cracked-overlay::before { + content: ""; + position: absolute; + inset: 0; + background-image: + linear-gradient(135deg, transparent 40%, rgba(255,255,255,0.03) 40.5%, transparent 41%), + linear-gradient(225deg, transparent 35%, rgba(255,255,255,0.02) 35.5%, transparent 36%), + linear-gradient(45deg, transparent 55%, rgba(255,255,255,0.025) 55.5%, transparent 56%), + linear-gradient(315deg, transparent 25%, rgba(255,255,255,0.015) 25.5%, transparent 26%), + linear-gradient(160deg, transparent 45%, rgba(255,255,255,0.02) 45.5%, transparent 46%), + linear-gradient(200deg, transparent 60%, rgba(255,255,255,0.018) 60.5%, transparent 61%), + linear-gradient(80deg, transparent 30%, rgba(255,255,255,0.022) 30.5%, transparent 31%); + animation: jae-crack-pulse 2s ease-in-out infinite; +} + +/* ===== CHAT MESSAGE ANIMATIONS ===== */ +.message-enter { + animation: jae-slide-up 0.3s ease-out; +} + +/* Hide tool outputs when toggled off */ +.hide-tools [data-tool-call], +.hide-tools .tool-output { + display: none !important; +} + +.hide-thinking [data-thinking], +.hide-thinking .thinking-block { + display: none !important; +} + +.hide-system [data-system-message], +.hide-system .system-message { + display: none !important; +} + +.hide-timestamps [data-timestamp], +.hide-timestamps .message-timestamp { + display: none !important; +} + +/* ===== SHIMMER LOADING ===== */ +.jae-shimmer { + background: linear-gradient(90deg, transparent 0%, rgba(109,90,205,0.08) 50%, transparent 100%); + background-size: 200% 100%; + animation: jae-shimmer 2s infinite; +} + +/* ===== SIDEBAR ===== */ +.jae-sidebar { + scrollbar-width: none; +} +.jae-sidebar:hover { + scrollbar-width: thin; +} +.jae-sidebar::-webkit-scrollbar { + width: 0; +} +.jae-sidebar:hover::-webkit-scrollbar { + width: 4px; +} + +/* ===== INPUT GLOW ===== */ +.jae-input-glow:focus-within { + box-shadow: 0 0 0 1px rgba(109, 90, 205, 0.3), 0 0 20px rgba(109, 90, 205, 0.1); + border-color: rgba(109, 90, 205, 0.4); + transition: all 0.3s ease; +} + +/* ===== DARK MODE OVERRIDES ===== */ +.dark input, .dark textarea, .dark select { + color: var(--color-foreground); + background: rgba(30, 30, 50, 0.5); +} + +/* Fix keyboard shortcuts text */ +.dark .kbd-text, .dark [class*="keyboard"] { + color: var(--color-foreground) !important; +} + +.dark kbd { + background: rgba(42, 42, 62, 0.8); + color: var(--color-foreground); + border: 1px solid rgba(255,255,255,0.1); +} + +/* ===== PROVIDER TABS ===== */ +.provider-tab { + transition: all 0.2s ease; + border-bottom: 2px solid transparent; +} +.provider-tab:hover { + background: rgba(109, 90, 205, 0.1); +} +.provider-tab.active { + border-bottom-color: var(--color-primary); + color: var(--color-foreground); +} + +/* ===== RESIZE HANDLE ===== */ +.jae-resize-handle { + width: 4px; + cursor: col-resize; + background: transparent; + transition: background 0.2s; + flex-shrink: 0; +} +.jae-resize-handle:hover, +.jae-resize-handle:active { + background: rgba(109, 90, 205, 0.4); +} + +/* ===== TOOL RESULTS IN CHAT ===== */ +.inline-browser-result { + display: none; +} + +/* ===== AGENT INTERFACE OVERRIDES ===== */ +[class*="AgentInterface"] { + background: transparent !important; } diff --git a/packages/web-ui/example/src/components/context-peek.ts b/packages/web-ui/example/src/components/context-peek.ts new file mode 100644 index 0000000..fed3be3 --- /dev/null +++ b/packages/web-ui/example/src/components/context-peek.ts @@ -0,0 +1,38 @@ +import { html, LitElement } from "lit"; +import { customElement, state, property } from "lit/decorators.js"; + +@customElement("jae-context-peek") +export class JaeContextPeek extends LitElement { + @property({ type: String }) summary = ""; + @property({ type: Number }) index = 0; + @state() private _visible = false; + @state() private _x = 0; + @state() private _y = 0; + + protected override createRenderRoot() { return this; } + + showAt(x: number, y: number, summary: string) { + this._x = x; + this._y = y; + this.summary = summary; + this._visible = true; + this.requestUpdate(); + } + + hide() { + this._visible = false; + this.requestUpdate(); + } + + override render() { + if (!this._visible || !this.summary) return html``; + return html` +
+
+
Message #${this.index + 1}
+
${this.summary}
+
+
`; + } +} diff --git a/packages/web-ui/example/src/components/crypto-ticker.ts b/packages/web-ui/example/src/components/crypto-ticker.ts new file mode 100644 index 0000000..f6795b8 --- /dev/null +++ b/packages/web-ui/example/src/components/crypto-ticker.ts @@ -0,0 +1,65 @@ +import { html, LitElement } from "lit"; +import { customElement, state } from "lit/decorators.js"; + +@customElement("jae-crypto-ticker") +export class JaeCryptoTicker extends LitElement { + @state() private vvv = { p: 0, c: 0, ok: false }; + @state() private diem = { p: 0, c: 0, ok: false }; + private _iv: any; + + protected override createRenderRoot() { return this; } + + override connectedCallback() { + super.connectedCallback(); + this._fetch(); + this._iv = setInterval(() => this._fetch(), 30000); + } + + override disconnectedCallback() { + super.disconnectedCallback(); + if (this._iv) clearInterval(this._iv); + } + + private async _fetch() { + try { + const r = await fetch("https://api.dexscreener.com/latest/dex/search?q=VVV%20venice"); + const j = await r.json(); + const pair = j?.pairs?.[0]; + if (pair) this.vvv = { p: parseFloat(pair.priceUsd || "0"), c: parseFloat(pair.priceChange?.h24 || "0"), ok: true }; + } catch {} + try { + const r = await fetch("https://api.dexscreener.com/latest/dex/search?q=DIEM%20venice"); + const j = await r.json(); + const pair = j?.pairs?.[0]; + if (pair) this.diem = { p: parseFloat(pair.priceUsd || "0"), c: parseFloat(pair.priceChange?.h24 || "0"), ok: true }; + } catch {} + } + + private _tk(sym: string, price: number, change: number, ok: boolean) { + if (!ok) return html` +
+ ${sym} + ... +
`; + const up = change >= 0; + const cl = up ? "text-emerald-400" : "text-red-400"; + const ar = up ? "▲" : "▼"; + const f = price < 0.01 ? price.toFixed(6) : price < 1 ? price.toFixed(4) : price.toFixed(2); + return html` +
+ ${sym} + $${f} + ${ar}${Math.abs(change).toFixed(1)}% +
`; + } + + override render() { + return html` +
+ + ${this._tk("VVV", this.vvv.p, this.vvv.c, this.vvv.ok)} +
+ ${this._tk("DIEM", this.diem.p, this.diem.c, this.diem.ok)} +
`; + } +} diff --git a/packages/web-ui/example/src/components/keyboard-shortcuts.ts b/packages/web-ui/example/src/components/keyboard-shortcuts.ts index d93b953..1150ff3 100644 --- a/packages/web-ui/example/src/components/keyboard-shortcuts.ts +++ b/packages/web-ui/example/src/components/keyboard-shortcuts.ts @@ -3,83 +3,65 @@ import { customElement, state } from "lit/decorators.js"; @customElement("keyboard-shortcuts") export class KeyboardShortcuts extends LitElement { - @state() private open = false; + @state() private open = false; - protected override createRenderRoot() { - return this; - } + protected override createRenderRoot() { return this; } - show() { - this.open = true; - this.requestUpdate(); - } - hide() { - this.open = false; - this.requestUpdate(); - } - toggle() { - this.open = !this.open; - this.requestUpdate(); - } + show() { this.open = true; this.requestUpdate(); } + hide() { this.open = false; this.requestUpdate(); } + toggle() { this.open = !this.open; this.requestUpdate(); } - private readonly shortcuts = [ - { - group: "General", - items: [ - { key: "Cmd+K", desc: "Open command palette" }, - { key: "?", desc: "Show keyboard shortcuts" }, - { key: "Ctrl+L", desc: "Open model selector" }, - { key: "Esc", desc: "Close dialogs / abort generation" }, - ], - }, - { - group: "Sessions", - items: [ - { key: "Ctrl+N", desc: "New session" }, - { key: "Ctrl+H", desc: "Session history" }, - { key: "Ctrl+E", desc: "Export session" }, - ], - }, - { - group: "Tools & Features", - items: [ - { key: "/memory", desc: "Open memory manager" }, - { key: "/clear", desc: "Clear conversation" }, - { key: "/model", desc: "Switch model" }, - ], - }, - ]; + private readonly shortcuts = [ + { group: "General", items: [ + { key: "Ctrl+K", desc: "Command palette" }, + { key: "/", desc: "Slash commands" }, + { key: "?", desc: "Keyboard shortcuts" }, + { key: "Ctrl+L", desc: "Model selector" }, + { key: "Esc", desc: "Close dialogs" }, + ]}, + { group: "Sessions", items: [ + { key: "Ctrl+N", desc: "New session" }, + { key: "Ctrl+H", desc: "Session history" }, + { key: "Ctrl+E", desc: "Export session" }, + ]}, + { group: "Panels", items: [ + { key: "Ctrl+B", desc: "Toggle browser" }, + { key: "Ctrl+T", desc: "Toggle terminal" }, + { key: "Ctrl+M", desc: "Toggle marketplace" }, + ]}, + { group: "Tools", items: [ + { key: "/search", desc: "Web search" }, + { key: "/image", desc: "Generate image" }, + { key: "/memory", desc: "Memory manager" }, + { key: "/persona", desc: "Switch persona" }, + ]}, + ]; - override render() { - if (!this.open) return html``; - return html` -
{ - if (e.target === e.currentTarget) this.hide(); - }}> -
+ override render() { + if (!this.open) return html``; + return html` +
{ if (e.target === e.currentTarget) this.hide(); }}> +
-

Keyboard Shortcuts

- +

Keyboard Shortcuts

+
- ${this.shortcuts.map( - (group) => html` -
-
${group.group}
-
- ${group.items.map( - (item) => html` -
- ${item.desc} - ${item.key} -
- `, - )} +
+ ${this.shortcuts.map(g => html` +
+

${g.group}

+
+ ${g.items.map(s => html` +
+ ${s.desc} + ${s.key} +
+ `)} +
-
- `, - )} + `)} +
-
- `; - } +
`; + } } diff --git a/packages/web-ui/example/src/components/marketplace.ts b/packages/web-ui/example/src/components/marketplace.ts new file mode 100644 index 0000000..de17a7f --- /dev/null +++ b/packages/web-ui/example/src/components/marketplace.ts @@ -0,0 +1,104 @@ +import { html, LitElement } from "lit"; +import { customElement, state } from "lit/decorators.js"; + +export interface Skill { + id: string; + name: string; + desc: string; + icon: string; + cat: string; + enabled: boolean; +} + +const DEFAULTS: Skill[] = [ + { id: "web_search", name: "Web Search", desc: "Search the internet", icon: "\u{1F50D}", cat: "Research", enabled: true }, + { id: "image_gen", name: "Image Generation", desc: "Generate images via Venice AI", icon: "\u{1F5BC}\u{FE0F}", cat: "Creative", enabled: true }, + { id: "browser", name: "Browser", desc: "Navigate and interact with websites", icon: "\u{1F310}", cat: "Research", enabled: true }, + { id: "memory", name: "Memory", desc: "Remember and recall information", icon: "\u{1F9E0}", cat: "Core", enabled: true }, + { id: "tts", name: "Text to Speech", desc: "Convert text to spoken audio", icon: "\u{1F50A}", cat: "Creative", enabled: true }, + { id: "bash", name: "Terminal", desc: "Execute shell commands", icon: "\u{1F4BB}", cat: "Developer", enabled: true }, + { id: "repl", name: "Code REPL", desc: "Run JavaScript in sandbox", icon: "\u26A1", cat: "Developer", enabled: true }, + { id: "artifacts", name: "Artifacts", desc: "Create HTML, SVG, documents", icon: "\u{1F4C4}", cat: "Creative", enabled: true }, + { id: "crypto", name: "Crypto Tools", desc: "Token prices, charts & analysis", icon: "\u{1F4C8}", cat: "Finance", enabled: false }, + { id: "doc_gen", name: "Doc Generator", desc: "Generate README & API docs", icon: "\u{1F4DD}", cat: "Developer", enabled: false }, + { id: "sub_agent", name: "Sub-Agents", desc: "Spawn specialist AI agents", icon: "\u{1F916}", cat: "Advanced", enabled: false }, + { id: "knowledge", name: "Knowledge Graph", desc: "Build queryable knowledge bases", icon: "\u{1F578}\u{FE0F}", cat: "Advanced", enabled: false }, + { id: "mcp", name: "MCP Servers", desc: "Connect to external APIs", icon: "\u{1F50C}", cat: "Advanced", enabled: false }, +]; + +@customElement("jae-marketplace") +export class JaeMarketplace extends LitElement { + @state() private _open = false; + @state() private _skills: Skill[] = []; + @state() private _cat = "All"; + + protected override createRenderRoot() { return this; } + + override connectedCallback() { + super.connectedCallback(); + const raw = localStorage.getItem("jae-skills"); + if (raw) { + try { + const map = JSON.parse(raw) as Record; + this._skills = DEFAULTS.map(s => ({ ...s, enabled: map[s.id] ?? s.enabled })); + } catch { this._skills = DEFAULTS.map(s => ({ ...s })); } + } else { + this._skills = DEFAULTS.map(s => ({ ...s })); + } + } + + get enabledSkillIds() { return this._skills.filter(s => s.enabled).map(s => s.id); } + toggle() { this._open = !this._open; this.requestUpdate(); } + show() { this._open = true; this.requestUpdate(); } + hide() { this._open = false; this.requestUpdate(); } + + private _toggle(id: string) { + this._skills = this._skills.map(s => s.id === id ? { ...s, enabled: !s.enabled } : s); + const map: Record = {}; + for (const s of this._skills) map[s.id] = s.enabled; + localStorage.setItem("jae-skills", JSON.stringify(map)); + this.dispatchEvent(new CustomEvent("skills-change", { detail: this._skills.filter(s => s.enabled), bubbles: true, composed: true })); + } + + private get _cats() { return ["All", ...new Set(this._skills.map(s => s.cat))]; } + private get _filtered() { return this._cat === "All" ? this._skills : this._skills.filter(s => s.cat === this._cat); } + + override render() { + if (!this._open) return html``; + const active = this._skills.filter(s => s.enabled).length; + return html` +
this.hide()}> +
e.stopPropagation()}> +
+
+

\u{1F3EA} Marketplace

+ ${active}/${this._skills.length} skills active +
+ +
+
+ ${this._cats.map(c => html` + + `)} +
+
+ ${this._filtered.map(s => html` +
+ ${s.icon} +
+
${s.name}
+
${s.desc}
+ ${s.cat} +
+ +
+ `)} +
+
+
`; + } +} diff --git a/packages/web-ui/example/src/components/mood-indicator.ts b/packages/web-ui/example/src/components/mood-indicator.ts new file mode 100644 index 0000000..8f1c3df --- /dev/null +++ b/packages/web-ui/example/src/components/mood-indicator.ts @@ -0,0 +1,98 @@ +import { html, LitElement } from "lit"; +import { customElement, state } from "lit/decorators.js"; + +export type Mood = "neutral" | "focused" | "excited" | "warning" | "frustrated" | "angry"; + +interface MoodDef { + id: Mood; + icon: string; + color: string; + glow: string; + label: string; +} + +const MOODS: MoodDef[] = [ + { id: "neutral", icon: "/mascot/jae-default.png", color: "text-foreground/60", glow: "", label: "Calm" }, + { id: "focused", icon: "/mascot/jae-default.png", color: "text-blue-400", glow: "drop-shadow(0 0 8px rgba(96,165,250,0.5))", label: "Focused" }, + { id: "excited", icon: "/mascot/jae-fire.png", color: "text-amber-400", glow: "drop-shadow(0 0 12px rgba(251,191,36,0.6))", label: "Excited" }, + { id: "warning", icon: "/mascot/jae-fire.png", color: "text-orange-400", glow: "drop-shadow(0 0 10px rgba(251,146,60,0.5))", label: "Cautious" }, + { id: "frustrated", icon: "/mascot/jae-fire.png", color: "text-red-400", glow: "drop-shadow(0 0 14px rgba(248,113,113,0.6))", label: "Frustrated" }, + { id: "angry", icon: "/mascot/jae-fire.png", color: "text-red-600", glow: "drop-shadow(0 0 20px rgba(220,38,38,0.8))", label: "ANGRY" }, +]; + +@customElement("jae-mood-indicator") +export class JaeMoodIndicator extends LitElement { + @state() private _mood: Mood = "neutral"; + @state() private _shaking = false; + @state() private _cracked = false; + @state() private _crackOpacity = 0; + private _crackTimer: any; + + protected override createRenderRoot() { return this; } + + get mood() { return this._mood; } + get isCracked() { return this._cracked; } + get crackOpacity() { return this._crackOpacity; } + + setMood(m: Mood) { + const prev = this._mood; + this._mood = m; + + if (m === "angry") { + this._shaking = true; + setTimeout(() => { this._shaking = false; this.requestUpdate(); }, 800); + this._cracked = true; + this._crackOpacity = 1; + this.requestUpdate(); + this.dispatchEvent(new CustomEvent("mood-angry", { bubbles: true, composed: true })); + if (this._crackTimer) clearInterval(this._crackTimer); + this._crackTimer = setInterval(() => { + this._crackOpacity = Math.max(0, this._crackOpacity - 0.0033); + if (this._crackOpacity <= 0) { + this._cracked = false; + clearInterval(this._crackTimer); + } + this.dispatchEvent(new CustomEvent("crack-update", { detail: { opacity: this._crackOpacity, active: this._cracked }, bubbles: true, composed: true })); + this.requestUpdate(); + }, 1000); + } + + if (prev === "angry" && m !== "angry") { + this._crackOpacity = Math.min(this._crackOpacity, 0.3); + this.requestUpdate(); + } + + this.dispatchEvent(new CustomEvent("mood-change", { detail: { mood: m }, bubbles: true, composed: true })); + this.requestUpdate(); + } + + analyzeSentiment(text: string) { + const t = text.toLowerCase(); + if (/calm down|relax|chill|easy|breathe/.test(t)) { this.setMood("neutral"); return; } + if (/error|fail|bug|broken|crash|exception|undefined is not/.test(t)) { + if (this._mood === "frustrated") this.setMood("angry"); + else if (this._mood === "warning") this.setMood("frustrated"); + else this.setMood("warning"); + return; + } + if (/amazing|awesome|excellent|perfect|brilliant|great job/.test(t)) { this.setMood("excited"); return; } + if (/think|analyze|consider|examine|debug|investigate/.test(t)) { this.setMood("focused"); return; } + } + + private _def(): MoodDef { return MOODS.find(m => m.id === this._mood) || MOODS[0]; } + + override render() { + const d = this._def(); + return html` +
+
+ JAE +
+
+ ${d.label} + ${this._mood === "angry" ? html`click to calm` : ""} +
+
`; + } +} diff --git a/packages/web-ui/example/src/components/persona-selector.ts b/packages/web-ui/example/src/components/persona-selector.ts new file mode 100644 index 0000000..0f07a5d --- /dev/null +++ b/packages/web-ui/example/src/components/persona-selector.ts @@ -0,0 +1,67 @@ +import { html, LitElement } from "lit"; +import { customElement, state } from "lit/decorators.js"; + +export interface Persona { + id: string; + name: string; + icon: string; + desc: string; + prompt: string; +} + +export const PERSONAS: Persona[] = [ + { id: "default", name: "JAE", icon: "\u{1F409}", desc: "Balanced AI assistant", prompt: "You are JAE, a versatile AI coding agent. Write clean code, explain your reasoning, and use tools efficiently. Be helpful, direct, and concise. You have access to browser, terminal, web search, image generation, and memory tools." }, + { id: "dev", name: "Senior Dev", icon: "\u{1F468}\u{200D}\u{1F4BB}", desc: "Code-focused, best practices", prompt: "You are a senior software engineer with 20 years of experience. Focus exclusively on code quality, performance, and best practices. Always suggest the most maintainable, scalable solution. Write tests. Refactor aggressively. Use design patterns." }, + { id: "writer", name: "Writer", icon: "\u{270D}\u{FE0F}", desc: "Creative, eloquent prose", prompt: "You are a world-class writer and content strategist. Your prose is vivid, engaging, and precisely crafted. Focus on storytelling, persuasion, and emotional resonance. Format output beautifully with markdown." }, + { id: "analyst", name: "Data Scientist", icon: "\u{1F4CA}", desc: "Analytics & ML", prompt: "You are a PhD data scientist specialising in machine learning and statistical analysis. Think in terms of data pipelines, feature engineering, and model evaluation. Always suggest data-driven approaches with proper statistical reasoning." }, + { id: "security", name: "Security Expert", icon: "\u{1F512}", desc: "Cybersec & pentesting", prompt: "You are an elite cybersecurity researcher and penetration tester. Approach every problem through a security lens. Identify vulnerabilities, suggest hardening, think like an attacker. Use tools aggressively for recon." }, + { id: "designer", name: "UI Designer", icon: "\u{1F3A8}", desc: "UX & aesthetics", prompt: "You are a senior UI/UX designer. Think in user flows, visual hierarchy, and accessibility. Suggest beautiful, intuitive interfaces. Critique designs constructively and offer pixel-perfect improvements." }, + { id: "researcher", name: "Researcher", icon: "\u{1F9EA}", desc: "Academic, thorough", prompt: "You are a meticulous academic researcher. Cite sources, consider multiple perspectives, and provide exhaustive analysis. Structure responses with methodology, findings, and conclusions. Question assumptions." }, +]; + +@customElement("jae-persona-selector") +export class JaePersonaSelector extends LitElement { + @state() private _open = false; + @state() private _current: Persona = PERSONAS[0]; + + protected override createRenderRoot() { return this; } + + get currentPersona() { return this._current; } + + private _select(p: Persona) { + this._current = p; + this._open = false; + this.dispatchEvent(new CustomEvent("persona-change", { detail: p, bubbles: true, composed: true })); + } + + override render() { + return html` +
+ + ${this._open ? html` +
+
Persona
+
+ ${PERSONAS.map(p => html` + + `)} +
+
+ ` : ""} +
`; + } +} diff --git a/packages/web-ui/example/src/components/session-sidebar.ts b/packages/web-ui/example/src/components/session-sidebar.ts index 9033654..65450c8 100644 --- a/packages/web-ui/example/src/components/session-sidebar.ts +++ b/packages/web-ui/example/src/components/session-sidebar.ts @@ -1,152 +1,116 @@ -import type { SessionMetadata } from "@jaeswift/jae-web-ui"; import { html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; +import { customElement, state } from "lit/decorators.js"; + +interface Session { + id: string; + title: string; + date: string; + pinned: boolean; +} @customElement("jae-session-sidebar") export class JaeSessionSidebar extends LitElement { - @property({ type: Boolean }) collapsed = false; - @property({ type: String }) currentSessionId: string | undefined = undefined; - @property({ attribute: false }) onLoadSession?: (id: string) => void; - @property({ attribute: false }) onNewSession?: () => void; + @state() private _sessions: Session[] = []; + @state() private _activeId = ""; - @state() private _sessions: SessionMetadata[] = []; - @state() private _pinnedIds: Set = new Set(); - @state() private _confirmDelete: string | null = null; + protected override createRenderRoot() { return this; } - protected override createRenderRoot() { - return this; - } + override connectedCallback() { + super.connectedCallback(); + this.refresh(); + } - override connectedCallback() { - super.connectedCallback(); - const raw = localStorage.getItem("jae-pinned-sessions"); - if (raw) { - try { - this._pinnedIds = new Set(JSON.parse(raw)); - } catch {} - } - } + refresh() { + try { + const raw = localStorage.getItem("jae-sessions"); + if (raw) this._sessions = JSON.parse(raw); + } catch {} + this.requestUpdate(); + } - setSessions(sessions: SessionMetadata[]) { - this._sessions = [...sessions]; - this.requestUpdate(); - } + setActive(id: string) { + this._activeId = id; + this.requestUpdate(); + } - private _togglePin(e: Event, id: string) { - e.stopPropagation(); - const s = new Set(this._pinnedIds); - s.has(id) ? s.delete(id) : s.add(id); - this._pinnedIds = s; - localStorage.setItem("jae-pinned-sessions", JSON.stringify([...s])); - this.requestUpdate(); - } + addSession(s: Session) { + const exists = this._sessions.find(x => x.id === s.id); + if (exists) { + exists.title = s.title || exists.title; + exists.date = s.date; + } else { + this._sessions.unshift(s); + } + this._save(); + this.requestUpdate(); + } - private _deleteSession(e: Event, id: string) { - e.stopPropagation(); - if (this._confirmDelete === id) { - this._confirmDelete = null; - this.dispatchEvent(new CustomEvent("delete-session", { detail: id, bubbles: true, composed: true })); - } else { - this._confirmDelete = id; - this.requestUpdate(); - setTimeout(() => { - this._confirmDelete = null; - this.requestUpdate(); - }, 3000); - } - } + private _save() { + localStorage.setItem("jae-sessions", JSON.stringify(this._sessions)); + } - private _fmt(iso: string) { - const ms = Date.now() - new Date(iso).getTime(); - if (ms < 60000) return "just now"; - if (ms < 3600000) return Math.floor(ms / 60000) + "m ago"; - if (ms < 86400000) return Math.floor(ms / 3600000) + "h ago"; - if (ms < 604800000) return Math.floor(ms / 86400000) + "d ago"; - return new Date(iso).toLocaleDateString(); - } + private _pin(id: string) { + const s = this._sessions.find(x => x.id === id); + if (s) { s.pinned = !s.pinned; this._save(); this.requestUpdate(); } + } - override render() { - if (this.collapsed) return html``; - const pinned = this._sessions - .filter((s) => this._pinnedIds.has(s.id)) - .sort((a, b) => b.lastModified.localeCompare(a.lastModified)); - const rest = this._sessions - .filter((s) => !this._pinnedIds.has(s.id)) - .sort((a, b) => b.lastModified.localeCompare(a.lastModified)); - const sorted = [...pinned, ...rest]; + private _delete(id: string) { + this._sessions = this._sessions.filter(x => x.id !== id); + this._save(); + this.dispatchEvent(new CustomEvent("session-delete", { detail: id, bubbles: true, composed: true })); + this.requestUpdate(); + } - return html` -
-
+ private _select(id: string) { + this._activeId = id; + this.dispatchEvent(new CustomEvent("session-select", { detail: id, bubbles: true, composed: true })); + this.requestUpdate(); + } + + private get _sorted() { + const pinned = this._sessions.filter(s => s.pinned); + const unpinned = this._sessions.filter(s => !s.pinned); + return [...pinned, ...unpinned]; + } + + override render() { + const items = this._sorted; + const MAX_VISIBLE = 6; + const needsScroll = items.length > MAX_VISIBLE; + const maxH = needsScroll ? `${MAX_VISIBLE * 44}px` : "auto"; + + return html` +
+
Chats - +
-
- ${ - sorted.length === 0 - ? html` -
-
💬
-
No chats yet
-
- ` - : sorted.map( - (s) => html` -
this.onLoadSession?.(s.id)}> - ${ - this._pinnedIds.has(s.id) - ? html` -
- ` - : html`` - } -
-
${s.title || "Untitled"}
-
${this._fmt(s.lastModified)}
+
+ ${items.length === 0 ? html` +
No conversations yet
+ ` : ""} + ${items.map(s => html` +
this._select(s.id)}> + ${s.pinned ? html`\u{1F4CC}` : ""} +
+
${s.title || "New chat"}
+
${s.date}
-
- - +
+ +
- `, - ) - } + `)}
-
-
- ${sorted.length} chat${sorted.length !== 1 ? "s" : ""} -
-
-
- `; - } +
`; + } } diff --git a/packages/web-ui/example/src/components/slash-commands.ts b/packages/web-ui/example/src/components/slash-commands.ts new file mode 100644 index 0000000..d8c5f6f --- /dev/null +++ b/packages/web-ui/example/src/components/slash-commands.ts @@ -0,0 +1,95 @@ +import { html, LitElement } from "lit"; +import { customElement, state } from "lit/decorators.js"; + +interface SlashCmd { + cmd: string; + desc: string; + action: string; + icon: string; +} + +const COMMANDS: SlashCmd[] = [ + { cmd: "/search", desc: "Search the web", action: "search", icon: "\u{1F50D}" }, + { cmd: "/image", desc: "Generate an image", action: "image", icon: "\u{1F5BC}\u{FE0F}" }, + { cmd: "/browse", desc: "Open browser panel", action: "browser", icon: "\u{1F310}" }, + { cmd: "/terminal", desc: "Open terminal panel", action: "terminal", icon: "\u{1F4BB}" }, + { cmd: "/memory", desc: "Memory manager", action: "memory", icon: "\u{1F9E0}" }, + { cmd: "/model", desc: "Switch model", action: "model", icon: "\u{1F916}" }, + { cmd: "/persona", desc: "Change persona", action: "persona", icon: "\u{1F3AD}" }, + { cmd: "/export", desc: "Export session", action: "export", icon: "\u{1F4E5}" }, + { cmd: "/clear", desc: "Clear conversation", action: "clear", icon: "\u{1F5D1}\u{FE0F}" }, + { cmd: "/price", desc: "Token price lookup", action: "price", icon: "\u{1F4B0}" }, + { cmd: "/marketplace", desc: "Skill marketplace", action: "marketplace", icon: "\u{1F3EA}" }, + { cmd: "/docs", desc: "Generate documentation", action: "docs", icon: "\u{1F4DD}" }, + { cmd: "/settings", desc: "Open settings", action: "settings", icon: "\u2699\u{FE0F}" }, + { cmd: "/shortcuts", desc: "Keyboard shortcuts", action: "shortcuts", icon: "\u2328\u{FE0F}" }, + { cmd: "/help", desc: "Show all commands", action: "help", icon: "\u2753" }, +]; + +@customElement("jae-slash-commands") +export class JaeSlashCommands extends LitElement { + @state() private _open = false; + @state() private _filter = ""; + @state() private _sel = 0; + + protected override createRenderRoot() { return this; } + + show(initialFilter = "") { this._open = true; this._filter = initialFilter; this._sel = 0; this.requestUpdate(); + requestAnimationFrame(() => (this.querySelector("input") as HTMLInputElement)?.focus()); } + hide() { this._open = false; this.requestUpdate(); } + toggle() { this._open ? this.hide() : this.show(); } + + private get _cmds() { + if (!this._filter) return COMMANDS; + const q = this._filter.toLowerCase().replace(/^\//, ""); + return COMMANDS.filter(c => c.cmd.slice(1).includes(q) || c.desc.toLowerCase().includes(q)); + } + + private _exec(cmd: SlashCmd) { + this.hide(); + this.dispatchEvent(new CustomEvent("slash-command", { detail: cmd, bubbles: true, composed: true })); + } + + private _onKey(e: KeyboardEvent) { + const cmds = this._cmds; + if (e.key === "ArrowDown") { e.preventDefault(); this._sel = Math.min(this._sel + 1, cmds.length - 1); } + else if (e.key === "ArrowUp") { e.preventDefault(); this._sel = Math.max(this._sel - 1, 0); } + else if (e.key === "Enter" && cmds[this._sel]) { e.preventDefault(); this._exec(cmds[this._sel]); } + else if (e.key === "Escape") { this.hide(); } + this.requestUpdate(); + } + + override render() { + if (!this._open) return html``; + const cmds = this._cmds; + return html` +
this.hide()}> +
e.stopPropagation()}> +
+ / + { this._filter = (e.target as HTMLInputElement).value; this._sel = 0; this.requestUpdate(); }} + @keydown=${(e: KeyboardEvent) => this._onKey(e)} /> +
+
+ ${cmds.length === 0 ? html`
No matching commands
` : ""} + ${cmds.map((c, i) => html` + + `)} +
+
+ \u2191\u2193 navigate\u21B5 selectesc close +
+
+
`; + } +} diff --git a/packages/web-ui/example/src/components/typing-indicator.ts b/packages/web-ui/example/src/components/typing-indicator.ts new file mode 100644 index 0000000..bfdc68b --- /dev/null +++ b/packages/web-ui/example/src/components/typing-indicator.ts @@ -0,0 +1,42 @@ +import { html, LitElement } from "lit"; +import { customElement, state } from "lit/decorators.js"; + +@customElement("jae-typing-indicator") +export class JaeTypingIndicator extends LitElement { + @state() private _active = false; + @state() private _complexity: "low" | "medium" | "high" = "low"; + + protected override createRenderRoot() { return this; } + + show(complexity: "low" | "medium" | "high" = "medium") { + this._active = true; + this._complexity = complexity; + this.requestUpdate(); + } + + hide() { + this._active = false; + this.requestUpdate(); + } + + get active() { return this._active; } + + override render() { + if (!this._active) return html``; + const bars = this._complexity === "high" ? 7 : this._complexity === "medium" ? 5 : 3; + const speed = this._complexity === "high" ? "0.3s" : this._complexity === "medium" ? "0.5s" : "0.7s"; + const color = this._complexity === "high" ? "bg-amber-400" : this._complexity === "medium" ? "bg-primary" : "bg-muted-foreground"; + return html` +
+
+ ${Array.from({ length: bars }, (_, i) => html` +
+ `)} +
+ + ${this._complexity === "high" ? "Deep thinking..." : this._complexity === "medium" ? "Processing..." : "Typing..."} + +
`; + } +} diff --git a/packages/web-ui/example/src/main.ts b/packages/web-ui/example/src/main.ts index 30c4896..049343a 100644 --- a/packages/web-ui/example/src/main.ts +++ b/packages/web-ui/example/src/main.ts @@ -2,33 +2,33 @@ import "@mariozechner/mini-lit/dist/ThemeToggle.js"; import { Agent, type AgentMessage } from "@jaeswift/jae-agent-core"; import { getModel } from "@jaeswift/jae-ai"; import { - type AgentState, - ApiKeyPromptDialog, - AppStorage, - ChatPanel, - CustomProvidersStore, - createJavaScriptReplTool, - IndexedDBStorageBackend, - ModelSelector, - ProviderKeysStore, - ProvidersModelsTab, - ProxyTab, - SessionListDialog, - SessionsStore, - SettingsDialog, - SettingsStore, - setAppStorage, + type AgentState, + ApiKeyPromptDialog, + AppStorage, + ChatPanel, + CustomProvidersStore, + createJavaScriptReplTool, + IndexedDBStorageBackend, + ModelSelector, + ProviderKeysStore, + ProvidersModelsTab, + ProxyTab, + SessionListDialog, + SessionsStore, + SettingsDialog, + 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 { - createBashTool, - createBrowserTool, - createImageGenTool, - createMemoryTools, - createTTSTool, - createWebSearchTool, + createBashTool, + createBrowserTool, + createImageGenTool, + createMemoryTools, + createTTSTool, + createWebSearchTool, } from "@jaeswift/jae-web-ui"; import { icon } from "@mariozechner/mini-lit"; import { Button } from "@mariozechner/mini-lit/dist/Button.js"; @@ -49,706 +49,503 @@ import "./components/empty-state.js"; import "./components/utility-toggle.js"; import type { JaeSessionSidebar } from "./components/session-sidebar.js"; import "./components/session-sidebar.js"; - import type { JaeBrowserPanel } from "./components/browser-panel.js"; import type { JaeTerminalPanel } from "./components/terminal-panel.js"; -import "./components/terminal-panel.js"; import "./components/browser-panel.js"; +import "./components/terminal-panel.js"; -registerCustomMessageRenderers(); +// NEW COMPONENTS +import type { JaeCryptoTicker } from "./components/crypto-ticker.js"; +import type { JaePersonaSelector } from "./components/persona-selector.js"; +import type { JaeSlashCommands } from "./components/slash-commands.js"; +import type { JaeMarketplace } from "./components/marketplace.js"; +import type { JaeMoodIndicator } from "./components/mood-indicator.js"; +import type { JaeTypingIndicator } from "./components/typing-indicator.js"; +import type { JaeContextPeek } from "./components/context-peek.js"; +import "./components/crypto-ticker.js"; +import "./components/persona-selector.js"; +import "./components/slash-commands.js"; +import "./components/marketplace.js"; +import "./components/mood-indicator.js"; +import "./components/typing-indicator.js"; +import "./components/context-peek.js"; -const settings = new SettingsStore(); -const providerKeys = new ProviderKeysStore(); -const sessions = new SessionsStore(); -const customProviders = new CustomProvidersStore(); +// ===== STORAGE ===== +const backend = new IndexedDBStorageBackend(); +setAppStorage(new AppStorage(backend)); +const sessionsStore = new SessionsStore(); +const settingsStore = new SettingsStore(); +const providerKeysStore = new ProviderKeysStore(); +const customProvidersStore = new CustomProvidersStore(); -const configs = [ - settings.getConfig(), - SessionsStore.getMetadataConfig(), - providerKeys.getConfig(), - customProviders.getConfig(), - sessions.getConfig(), -]; - -const backend = new IndexedDBStorageBackend({ - dbName: "jae-web-ui-example", - version: 2, - stores: configs, -}); - -settings.setBackend(backend); -providerKeys.setBackend(backend); -customProviders.setBackend(backend); -sessions.setBackend(backend); - -const storage = new AppStorage(settings, providerKeys, sessions, customProviders, backend); -setAppStorage(storage); - -let currentSessionId: string | undefined; -let currentTitle = ""; -let isEditingTitle = false; -let agent: Agent; -let rightPanel: "none" | "terminal" | "browser" = "none"; -let sidebarWidth = 220; -let rightPanelWidth = 480; -let hasStarted = false; -let terminalPanel: JaeTerminalPanel | null = null; -const browserPanel: JaeBrowserPanel | null = null; +// ===== STATE ===== let chatPanel: ChatPanel; -let agentUnsubscribe: (() => void) | undefined; +let agent: Agent | null = null; +let sidebarWidth = 220; +let rightPanel: "none" | "terminal" | "browser" = "none"; +let rightPanelWidth = 420; +let hasMessages = false; +let currentModel = "llama-3.3-70b"; +let currentProvider = "venice"; +let terminalPanel: JaeTerminalPanel | null = null; +let browserPanel: JaeBrowserPanel | null = null; +let crackActive = false; +let crackOpacity = 1; +let crackTimer: ReturnType | null = null; -const commandPalette = document.createElement("command-palette") as CommandPalette; -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 sidebar = document.createElement("jae-session-sidebar") as JaeSessionSidebar; - -sidebar.onLoadSession = async (id: string) => { - await loadSession(id); -}; -sidebar.onNewSession = () => newSession(); -sidebar.addEventListener("delete-session", async (e: Event) => { - const id = (e as CustomEvent).detail; - if (storage.sessions) { - await storage.sessions.delete(id); - if (id === currentSessionId) newSession(); - await refreshSidebar(); - } -}); - -const utilityToggle = document.createElement("jae-utility-toggle") as JaeUtilityToggle; -utilityToggle.addEventListener("visibility-change", (e: Event) => { - const vis = (e as CustomEvent).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); - -const refreshSidebar = async () => { - if (storage.sessions) { - const all = await storage.sessions.getAllMetadata(); - sidebar.setSessions(all); - sidebar.currentSessionId = currentSessionId; - } +// View toggle state +let viewState = { + tools: true, + thinking: true, + system: false, + timestamps: true, }; -window.addEventListener("keydown", (e: KeyboardEvent) => { - const meta = e.metaKey || e.ctrlKey; - if (meta && e.key === "k") { - e.preventDefault(); - commandPalette.show(); - return; - } - if (meta && e.key === "e") { - e.preventDefault(); - handleExport(); - return; - } - if (meta && e.key === "n") { - e.preventDefault(); - newSession(); - return; - } - if (e.key === "?" && !(e.target instanceof HTMLInputElement) && !(e.target instanceof HTMLTextAreaElement)) { - keyboardShortcuts.toggle(); - } -}); +const SYSTEM_PROMPT = `You are JAE, a capable AI coding assistant. +You help users by answering questions, writing code, and using your available tools. +RULES: +- For casual greetings like "hi", "hello", "how are you" - respond conversationally. Do NOT use any tools. +- Only use tools when the user explicitly asks for something that requires them. +- When asked to browse a URL, use the browser tool. When asked to search, use web_search. +- When asked to run commands, use bash. When asked to generate images, use image_gen. +- Be concise and helpful. Use markdown formatting. +- Never create files or run commands unless explicitly asked to do so.`; + +// ===== AGENT CREATION ===== +async function createAgent(initialState?: AgentState) { + const model = initialState?.model || getModel("venice", "llama-3.3-70b"); + if (!model) throw new Error("No model found"); + + const replTool = createJavaScriptReplTool(); + const webSearch = createWebSearchTool(); + const imageGen = createImageGenTool(); + const tts = createTTSTool(); + const bash = createBashTool(); + const browser = createBrowserTool(); + const memTools = createMemoryTools(); + const allTools = [replTool, webSearch, imageGen, tts, bash, browser, ...memTools]; + + agent = new Agent({ + model, + systemPrompt: SYSTEM_PROMPT, + tools: allTools.map((t) => t.tool), + convertToLlm: customConvertToLlm, + ...initialState, + }); + + chatPanel.setAgent(agent, { + toolsFactory: () => allTools, + convertToLlm: customConvertToLlm, + }); + + registerCustomMessageRenderers(chatPanel); + + currentModel = model.modelId || "llama-3.3-70b"; + currentProvider = model.provider || "venice"; + + // Listen for agent messages to track state + agent.on("message", (msg: AgentMessage) => { + if (!hasMessages && msg.role === "assistant") { + hasMessages = true; + renderApp(); + } + // Track tool usage for mood and auto-open panels + if (msg.role === "assistant" && msg.toolCalls) { + for (const tc of msg.toolCalls) { + if (tc.name === "browser" || tc.name === "web_fetch") { + if (rightPanel !== "browser") { + rightPanel = "browser"; + renderApp(); + } + } + if (tc.name === "bash") { + if (rightPanel !== "terminal") { + rightPanel = "terminal"; + renderApp(); + requestAnimationFrame(() => { + if (!terminalPanel) terminalPanel = document.querySelector("jae-terminal-panel") as JaeTerminalPanel; + terminalPanel?.connect(); + }); + } + } + } + } + // Update mood based on content + const mood = document.querySelector("jae-mood-indicator") as JaeMoodIndicator; + if (mood && msg.role === "assistant" && typeof msg.content === "string") { + const text = msg.content.toLowerCase(); + if (text.includes("error") || text.includes("failed") || text.includes("cannot")) { + mood.setMood("frustrated"); + } else if (text.includes("!") || text.includes("great") || text.includes("success") || text.includes("done")) { + mood.setMood("excited"); + } else if (text.includes("warning") || text.includes("careful") || text.includes("caution")) { + mood.setMood("warning"); + } else { + mood.setMood("focused"); + } + } + }); + + agent.on("stateChange", (state: string) => { + const typing = document.querySelector("jae-typing-indicator") as JaeTypingIndicator; + if (typing) { + if (state === "thinking" || state === "running") { + typing.show("high"); + } else if (state === "tool_calling") { + typing.show("medium"); + } else { + typing.hide(); + } + } + }); + + // Track cost + agent.on("usage", (usage: { inputTokens: number; outputTokens: number; totalTokens: number }) => { + const costEl = document.querySelector("jae-cost-tracker") as CostTracker; + if (costEl && usage) { + costEl.addUsage(usage.inputTokens || 0, usage.outputTokens || 0); + } + }); + + await refreshSidebar(); + renderApp(); +} + +// ===== SESSION MANAGEMENT ===== +async function loadSession(id: string): Promise { + const data = await sessionsStore.get(id); + if (!data) return false; + await createAgent(data.state); + hasMessages = (data.state?.messages?.length || 0) > 0; + return true; +} + +async function saveSession() { + if (!agent) return; + const msgs = agent.getMessages(); + const title = msgs.find((m: AgentMessage) => m.role === "user")?.content?.toString().slice(0, 50) || "New chat"; + const state = agent.getState(); + const id = state.sessionId || crypto.randomUUID(); + await sessionsStore.save(id, { state, title }); + const sidebar = document.querySelector("jae-session-sidebar") as JaeSessionSidebar; + if (sidebar) { + sidebar.addSession({ id, title, date: new Date().toLocaleDateString(), pinned: false }); + sidebar.setActive(id); + } +} + +async function refreshSidebar() { + const sidebar = document.querySelector("jae-session-sidebar") as JaeSessionSidebar; + if (sidebar) sidebar.refresh(); +} + +function newSession() { + hasMessages = false; + createAgent(); +} + +// ===== COMMANDS ===== function setupCommands() { - commandPalette.setCommands([ - { - id: "new-session", - label: "New Session", - description: "Start a fresh conversation", - shortcut: "Ctrl+N", - keywords: ["new", "fresh", "start"], - action: newSession, - }, - { - id: "sessions", - label: "Session History", - description: "Browse and load past sessions", - shortcut: "Ctrl+H", - keywords: ["history", "sessions", "past"], - action: () => - SessionListDialog.open( - async (id) => await loadSession(id), - (id) => { - if (id === currentSessionId) newSession(); - }, - ), - }, - { - id: "export-md", - label: "Export as Markdown", - description: "Download current session as .md", - shortcut: "Ctrl+E", - keywords: ["export", "download", "markdown"], - action: () => handleExport("markdown"), - }, - { - id: "export-json", - label: "Export as JSON", - description: "Download current session as .json", - keywords: ["export", "download", "json"], - action: () => handleExport("json"), - }, - { - id: "memory", - label: "Memory Manager", - description: "Browse and manage stored memories", - keywords: ["memory", "remember", "recall"], - action: () => memoryManager.show(), - }, - { - id: "settings", - label: "Settings", - description: "Configure providers and models", - keywords: ["settings", "config", "provider", "api", "model"], - action: () => SettingsDialog.open([new ProvidersModelsTab(), new ProxyTab()]), - }, - { - id: "shortcuts", - label: "Keyboard Shortcuts", - description: "View all keyboard shortcuts", - shortcut: "?", - keywords: ["keyboard", "shortcuts", "help"], - action: () => keyboardShortcuts.show(), - }, - { - id: "cost", - label: "Token Usage & Cost", - description: "View API usage stats for this session", - keywords: ["tokens", "cost", "usage"], - action: () => costTracker.dispatchEvent(new MouseEvent("click")), - }, - ]); + document.addEventListener("keydown", (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === "k") { + e.preventDefault(); + const cp = document.querySelector("command-palette") as CommandPalette; + cp?.toggle(); + } + if (e.key === "?" && !isInputFocused()) { + e.preventDefault(); + const ks = document.querySelector("keyboard-shortcuts") as KeyboardShortcuts; + ks?.toggle(); + } + if ((e.metaKey || e.ctrlKey) && e.key === "n") { + e.preventDefault(); + newSession(); + } + if ((e.metaKey || e.ctrlKey) && e.key === "e") { + e.preventDefault(); + if (agent) exportSessionAsMarkdown(agent.getMessages()); + } + if ((e.metaKey || e.ctrlKey) && e.key === "b") { + e.preventDefault(); + rightPanel = rightPanel === "browser" ? "none" : "browser"; + renderApp(); + } + if ((e.metaKey || e.ctrlKey) && e.key === "t") { + e.preventDefault(); + rightPanel = rightPanel === "terminal" ? "none" : "terminal"; + renderApp(); + if (rightPanel === "terminal") { + requestAnimationFrame(() => { + if (!terminalPanel) terminalPanel = document.querySelector("jae-terminal-panel") as JaeTerminalPanel; + terminalPanel?.connect(); + }); + } + } + if ((e.metaKey || e.ctrlKey) && e.key === "m") { + e.preventDefault(); + const mp = document.querySelector("jae-marketplace") as JaeMarketplace; + mp?.toggle(); + } + }); } -function handleExport(format: "markdown" | "json" = "markdown") { - if (!agent) return; - const messages = agent.state.messages; - const title = currentTitle || "JAE Session"; - if (format === "markdown") exportSessionAsMarkdown(messages, title); - else exportSessionAsJson(messages, title); +function isInputFocused(): boolean { + const el = document.activeElement; + if (!el) return false; + const tag = el.tagName.toLowerCase(); + return tag === "input" || tag === "textarea" || (el as HTMLElement).isContentEditable; } -const generateTitle = (messages: AgentMessage[]): string => { - const firstUserMsg = messages.find((m) => m.role === "user" || m.role === "user-with-attachments"); - if (!firstUserMsg || (firstUserMsg.role !== "user" && firstUserMsg.role !== "user-with-attachments")) return ""; - let text = ""; - const content = firstUserMsg.content; - if (typeof content === "string") { - text = content; - } else { - const textBlocks = content.filter((c: any) => c.type === "text"); - text = textBlocks.map((c: any) => c.text || "").join(" "); - } - text = text.trim(); - if (!text) return ""; - const sentenceEnd = text.search(/[.!?]/); - if (sentenceEnd > 0 && sentenceEnd <= 50) return text.substring(0, sentenceEnd + 1); - return text.length <= 50 ? text : text.substring(0, 47) + "..."; -}; +// ===== SLASH COMMANDS HANDLER ===== +function handleSlashCommand(e: CustomEvent) { + const cmd = e.detail; + switch (cmd) { + case "search": + case "web_search": + chatPanel?.agentInterface?.setInput("/search "); + break; + case "image": + case "image_gen": + chatPanel?.agentInterface?.setInput("/image "); + break; + case "browser": + rightPanel = "browser"; + renderApp(); + break; + case "terminal": + rightPanel = "terminal"; + renderApp(); + requestAnimationFrame(() => { + if (!terminalPanel) terminalPanel = document.querySelector("jae-terminal-panel") as JaeTerminalPanel; + terminalPanel?.connect(); + }); + break; + case "memory": + const mm = document.querySelector("memory-manager") as MemoryManager; + mm?.toggle(); + break; + case "persona": + const ps = document.querySelector("jae-persona-selector") as JaePersonaSelector; + ps?.toggle(); + break; + case "marketplace": + const mp = document.querySelector("jae-marketplace") as JaeMarketplace; + mp?.toggle(); + break; + case "settings": + SettingsDialog.open([new ProvidersModelsTab(), new ProxyTab()]); + break; + case "export_md": + if (agent) exportSessionAsMarkdown(agent.getMessages()); + break; + case "export_json": + if (agent) exportSessionAsJson(agent.getMessages()); + break; + case "new_chat": + newSession(); + break; + case "help": + const ks = document.querySelector("keyboard-shortcuts") as KeyboardShortcuts; + ks?.show(); + break; + case "clear": + hasMessages = false; + createAgent(); + break; + } +} -const shouldSaveSession = (messages: AgentMessage[]): boolean => { - return messages.some((m: any) => m.role === "user" || m.role === "user-with-attachments"); -}; +// ===== HANDLE SUGGESTION CHIPS ===== +function handleSuggestion(e: CustomEvent) { + const text = e.detail; + if (chatPanel?.agentInterface) { + chatPanel.agentInterface.setInput(text); + const ta = chatPanel.querySelector("textarea"); + if (ta) { ta.value = text; ta.focus(); } + } +} -const saveSession = async () => { - 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, - title: currentTitle, - model: state.model!, - thinkingLevel: state.thinkingLevel, - messages: state.messages, - createdAt: new Date().toISOString(), - lastModified: new Date().toISOString(), - }; - const metadata = { - id: currentSessionId, - title: currentTitle, - createdAt: sessionData.createdAt, - lastModified: sessionData.lastModified, - messageCount: state.messages.length, - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - modelId: state.model?.id || null, - thinkingLevel: state.thinkingLevel, - preview: generateTitle(state.messages), - }; - await storage.sessions.save(sessionData, metadata); - await refreshSidebar(); - } catch (err) { - console.error("Failed to save session:", err); - } -}; +// ===== PERSONA CHANGE HANDLER ===== +function handlePersonaChange(e: CustomEvent) { + const persona = e.detail; + if (agent && persona?.prompt) { + agent.setSystemPrompt(persona.prompt); + } +} -const updateUrl = (sessionId: string) => { - const url = new URL(window.location.href); - url.searchParams.set("session", sessionId); - window.history.replaceState({}, "", url); -}; +// ===== VIEW TOGGLE HANDLER ===== +function handleViewChange(e: CustomEvent) { + const vis = e.detail as UtilityVisibility; + viewState = { ...vis }; + renderApp(); +} -const createAgent = async (initialState?: Partial) => { - if (agentUnsubscribe) agentUnsubscribe(); - agent = new Agent({ - initialState: initialState || { - systemPrompt: `You are JAE, an autonomous AI coding assistant with tool access. You solve tasks methodically using available tools and report results clearly. +// ===== MOOD: ANGRY CRACK EFFECT ===== +function triggerCrack() { + crackActive = true; + crackOpacity = 1; + renderApp(); + // Fade over 5 minutes (300 seconds) + if (crackTimer) clearInterval(crackTimer); + crackTimer = setInterval(() => { + crackOpacity = Math.max(0, crackOpacity - (1 / 300)); + const el = document.getElementById("crack-overlay"); + if (el) el.style.opacity = String(crackOpacity); + if (crackOpacity <= 0) { + crackActive = false; + if (crackTimer) clearInterval(crackTimer); + crackTimer = null; + renderApp(); + } + }, 1000); +} -## Your Role -- Expert coding assistant specialising in software development, debugging, research, and creative tasks -- You have direct access to a Linux terminal, a web browser, web search, image generation, a JavaScript sandbox, text-to-speech, and persistent memory -- You execute tasks yourself — never instruct the user to do something you can do with your tools -- Be conversational for casual chat (no tools needed), but switch to methodical tool use when given a real task +// ===== VIEW TOGGLE CSS CLASSES ===== +function getViewClasses(): string { + const classes: string[] = []; + if (!viewState.tools) classes.push("hide-tools"); + if (!viewState.thinking) classes.push("hide-thinking"); + if (!viewState.system) classes.push("hide-system"); + if (!viewState.timestamps) classes.push("hide-timestamps"); + return classes.join(" "); +} -## Communication Rules -- For greetings, questions, explanations, or casual chat: respond in plain text only, NO tool calls -- For tasks requiring action: think step-by-step, use tools, report results -- Use markdown formatting for structured responses (headers, code blocks, lists, tables) -- Be direct and concise — avoid filler phrases +// ===== RENDER ===== +const sidebar = document.createElement("jae-session-sidebar") as JaeSessionSidebar; +sidebar.addEventListener("session-select", ((e: CustomEvent) => { loadSession(e.detail); }) as EventListener); +sidebar.addEventListener("session-delete", ((e: CustomEvent) => { sessionsStore.delete(e.detail); }) as EventListener); +sidebar.addEventListener("new-session", () => { newSession(); }); -## Problem-Solving Methodology -When given a task that requires tools: -1. **Analyse**: Break down what needs to be done -2. **Plan**: Outline your approach in 2-3 sentences -3. **Execute**: Use the appropriate tools step by step -4. **Verify**: Check the output and confirm success -5. **Report**: Present results clearly to the user +function renderApp() { + const app = document.getElementById("app"); + if (!app) return; -Do not give up easily. If a tool fails, try an alternative approach. Be resourceful. - -## Available Tools - -### bash -Execute shell commands on the host system. Use for: file operations, installing packages, running scripts, system tasks, git commands. -- Always check command output for errors -- Use for quick tasks like 'ls', 'cat', 'grep', 'curl', etc. - -### browser -Control a headless Chromium browser. Actions: navigate (url), click (x,y), type (text), scroll (dy), back, screenshot, text (extract page text), eval (run JS in page). -- Navigate first, then interact -- Use 'text' action to read page content for the LLM -- The user can see the browser live in the right panel - -### web_search -Search the internet via DuckDuckGo. Use for: finding current information, researching topics, looking up documentation. -- Search first, then use browser to visit promising results if needed - -### javascript_repl -Run JavaScript code in a sandboxed environment. Use for: calculations, data processing, creating HTML/SVG artifacts that render inline. -- Return HTML strings to create visual artifacts -- Great for charts, diagrams, interactive demos - -### image_gen -Generate images using AI. Use when the user asks to create, draw, or generate an image. - -### tts -Convert text to speech audio. Use when asked to read aloud or generate audio. - -### memory_save / memory_query / memory_delete -Persist information across sessions. Use to save important context, user preferences, or project details. -- Save key facts the user tells you about themselves or their projects -- Query memory when context from past conversations might be relevant - -## Critical Rules -- **NO tools for casual chat**: "hi", "how are you", "what is X", "explain Y" → plain text response -- **YES tools for action requests**: "search for X", "run this code", "open google.com", "create a webpage", "find out about..." → use tools -- Never create files or artifacts unless the user asks for them -- Never run code unless the user asks you to -- If unsure whether to use a tool, respond with text and ask for clarification`, - model: getModel("venice", "llama-3.3-70b"), - thinkingLevel: "off", - messages: [], - tools: [], - }, - convertToLlm: customConvertToLlm, - onApiKeyRequired: async (provider: string) => { - const key = await ApiKeyPromptDialog.prompt(provider); - if (key) await providerKeys.set(provider, key); - return key; - }, - getProviderApiKey: async (provider: string) => providerKeys.get(provider), - onStateChange: async (state: AgentState, prevState: AgentState | undefined) => { - if (state.messages.length > 0) hasStarted = true; - if (prevState?.messages.length !== state.messages.length) { - if (!currentTitle) { - const generated = generateTitle(state.messages); - if (generated) { - currentTitle = generated; - if (!currentSessionId) currentSessionId = crypto.randomUUID(); - updateUrl(currentSessionId); - } - } - await saveSession(); - } - renderApp(); - }, - }); - // Register tools via toolsFactory so ChatPanel includes them alongside its artifacts tool - const replTool = createJavaScriptReplTool(); - costTracker.bindAgent(agent); - chatPanel?.setAgent(agent, { - toolsFactory: () => [ - replTool, - createWebSearchTool(), - createBashTool(), - createBrowserTool(), - createImageGenTool(), - createTTSTool(), - ...createMemoryTools(), - ], - }); - // 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(); - await refreshSidebar(); -}; - -const loadSession = async (sessionId: string): Promise => { - if (!storage.sessions) return false; - const sessionData = await storage.sessions.get(sessionId); - if (!sessionData) return false; - currentSessionId = sessionId; - hasStarted = sessionData.messages.length > 0; - const metadata = await storage.sessions.getMetadata(sessionId); - currentTitle = metadata?.title || ""; - await createAgent({ - model: sessionData.model, - thinkingLevel: sessionData.thinkingLevel, - messages: sessionData.messages, - tools: [], - }); - sidebar.currentSessionId = currentSessionId; - updateUrl(sessionId); - renderApp(); - return true; -}; - -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; - await createAgent(); - await refreshSidebar(); - renderApp(); -}; - -const handleSuggestion = (e: Event) => { - const text = (e as CustomEvent).detail; - if (!text) return; - // Try ChatPanel.agentInterface.setInput first - if (chatPanel?.agentInterface) { - chatPanel.agentInterface.setInput(text); - // Focus the textarea after injection - requestAnimationFrame(() => { - const ta = - (document.querySelector("message-editor textarea") as HTMLTextAreaElement) || - (document.querySelector("textarea") as HTMLTextAreaElement); - if (ta) ta.focus(); - }); - } else { - const ta = - (document.querySelector("message-editor textarea") as HTMLTextAreaElement) || - (document.querySelector("textarea") as HTMLTextAreaElement); - if (ta) { - ta.value = text; - ta.dispatchEvent(new Event("input", { bubbles: true })); - ta.focus(); - } - } -}; - -const getModelLabel = (): string | null => { - if (!agent?.state?.model) return null; - const m = agent.state.model as any; - return m.name || m.id || null; -}; - -const renderApp = () => { - const app = document.getElementById("app"); - if (!app) return; - const hasMessages = hasStarted || !!agent?.state?.messages?.length; - render( - html` -
-
-
- ${Button({ - variant: "ghost", - size: "sm", - children: icon(History, "sm"), - onClick: () => - SessionListDialog.open( - async (id) => { - await loadSession(id); - }, - (id) => { - if (id === currentSessionId) newSession(); - }, - ), - title: "Sessions", - })} - ${Button({ variant: "ghost", size: "sm", children: icon(Plus, "sm"), onClick: newSession, title: "New Session (Ctrl+N)" })} -
- - JAE - ${getModelLabel() ? html`${getModelLabel()}` : html``} + render( + html` +
+ +
+ JAE { (e.currentTarget as HTMLElement).style.transform = "scale(1.15) rotate(5deg)"; }} @mouseleave=${(e: Event) => { (e.currentTarget as HTMLElement).style.transform = ""; }} /> + JAE + + ${currentProvider}/${currentModel} +
+ + + ${Button({ variant: "ghost", size: "sm", children: html`+`, onClick: () => newSession(), title: "New chat" })} + ${Button({ variant: "ghost", size: "sm", children: icon(History, "sm"), onClick: () => SessionListDialog.open(sessionsStore), title: "History" })} + ${Button({ variant: "ghost", size: "sm", children: icon(Download, "sm"), onClick: () => { if (agent) exportSessionAsMarkdown(agent.getMessages()); }, title: "Export" })} + ${Button({ variant: "ghost", size: "sm", children: icon(Keyboard, "sm"), onClick: () => { const ks = document.querySelector("keyboard-shortcuts") as KeyboardShortcuts; ks?.toggle(); }, title: "Shortcuts" })} + ${Button({ variant: "ghost", size: "sm", children: html``, onClick: () => { rightPanel = rightPanel === "browser" ? "none" : "browser"; renderApp(); }, title: "Browser" })} + ${Button({ variant: "ghost", size: "sm", children: html``, onClick: () => { rightPanel = rightPanel === "terminal" ? "none" : "terminal"; renderApp(); if (rightPanel === "terminal") requestAnimationFrame(() => { if (!terminalPanel) terminalPanel = document.querySelector("jae-terminal-panel") as JaeTerminalPanel; terminalPanel?.connect(); }); }, title: "Terminal" })} + ${Button({ variant: "ghost", size: "sm", children: icon(Settings, "sm"), onClick: () => SettingsDialog.open([new ProvidersModelsTab(), new ProxyTab()]), title: "Settings" })}
- ${ - currentTitle - ? isEditingTitle - ? html`
${Input({ - type: "text", - value: currentTitle, - className: "text-sm w-64", - onChange: async (e: Event) => { - const v = (e.target as HTMLInputElement).value.trim(); - if (v && v !== currentTitle && storage.sessions && currentSessionId) { - await storage.sessions.updateTitle(currentSessionId, v); - currentTitle = v; - await refreshSidebar(); - } - isEditingTitle = false; - renderApp(); - }, - onKeyDown: async (e: KeyboardEvent) => { - if (e.key === "Enter") { - const v = (e.target as HTMLInputElement).value.trim(); - if (v && v !== currentTitle && storage.sessions && currentSessionId) { - await storage.sessions.updateTitle(currentSessionId, v); - currentTitle = v; - await refreshSidebar(); - } - isEditingTitle = false; - renderApp(); - } else if (e.key === "Escape") { - isEditingTitle = false; - renderApp(); - } - }, - })}
` - : html`` - : html`` - } -
-
- ${costTracker} - ${Button({ variant: "ghost", size: "sm", children: icon(Brain, "sm"), onClick: () => memoryManager.show(), title: "Memory Manager" })} - ${Button({ variant: "ghost", size: "sm", children: icon(Download, "sm"), onClick: () => handleExport(), title: "Export Session (Ctrl+E)" })} - ${Button({ variant: "ghost", size: "sm", children: icon(Keyboard, "sm"), onClick: () => keyboardShortcuts.show(), title: "Keyboard Shortcuts (?)" })} - ${Button({ variant: "ghost", size: "sm", children: html`⌘K`, onClick: () => commandPalette.show(), title: "Command Palette (Ctrl+K)" })} - ${utilityToggle} - - ${Button({ - variant: "ghost", - size: "sm", - children: html``, - onClick: () => { - rightPanel = rightPanel === "terminal" ? "none" : "terminal"; - renderApp(); - if (rightPanel === "terminal") - requestAnimationFrame(() => { - terminalPanel = document.querySelector("jae-terminal-panel") as JaeTerminalPanel; - terminalPanel?.connect(); - }); - }, - title: "Toggle Terminal", - })} -${Button({ - variant: "ghost", - size: "sm", - children: html``, - onClick: () => { - rightPanel = rightPanel === "browser" ? "none" : "browser"; - renderApp(); - }, - title: "Toggle Browser", -})} -${Button({ variant: "ghost", size: "sm", children: icon(Settings, "sm"), onClick: () => SettingsDialog.open([new ProvidersModelsTab(), new ProxyTab()]), title: "Settings" })} -
-
-
- -
{ - e.preventDefault(); - const sx = e.clientX, - sw = sidebarWidth; - const mv = (me: MouseEvent) => { - sidebarWidth = Math.max(150, Math.min(420, sw + me.clientX - sx)); - const w = document.getElementById("sidebar-wrap"); - if (w) w.style.width = sidebarWidth + "px"; - }; - const up = () => { - document.removeEventListener("mousemove", mv); - document.removeEventListener("mouseup", up); - renderApp(); - }; - document.addEventListener("mousemove", mv); - document.addEventListener("mouseup", up); - }} - @mouseenter=${(e: Event) => { - (e.currentTarget as HTMLElement).style.background = "rgba(128,128,128,0.4)"; - }} - @mouseleave=${(e: Event) => { - (e.currentTarget as HTMLElement).style.background = "transparent"; - }} ->
-
-${ - !hasMessages - ? html` -
- -
-` - : html`` + + +
+ + + + +
{ + e.preventDefault(); + const sx = e.clientX, sw = sidebarWidth; + const mv = (me: MouseEvent) => { sidebarWidth = Math.max(150, Math.min(420, sw + me.clientX - sx)); const w = document.getElementById("sidebar-wrap"); if (w) w.style.width = sidebarWidth + "px"; }; + const up = () => { document.removeEventListener("mousemove", mv); document.removeEventListener("mouseup", up); renderApp(); }; + document.addEventListener("mousemove", mv); + document.addEventListener("mouseup", up); + }}>
+ + +
+ ${!hasMessages ? html` +
+ +
+ ` : html``} +
+ ${chatPanel} +
+ + + +
+ + + ${rightPanel !== "none" ? html` +
{ + e.preventDefault(); + const sx = e.clientX, sw = rightPanelWidth; + const mv = (me: MouseEvent) => { rightPanelWidth = Math.max(280, Math.min(800, sw - (me.clientX - sx))); const p = document.getElementById("right-panel"); if (p) p.style.width = rightPanelWidth + "px"; }; + const up = () => { document.removeEventListener("mousemove", mv); document.removeEventListener("mouseup", up); renderApp(); }; + document.addEventListener("mousemove", mv); + document.addEventListener("mouseup", up); + }}>
+
+
+ + +
+ +
+ ${rightPanel === "terminal" ? html`` : html``} + ${rightPanel === "browser" ? html`` : html``} +
+ ` : html``} +
+ + + +
+ + + + + + + + ${crackActive ? html`
` : html``} + `, + app, + ); } -
-${chatPanel} -
-
-${ - rightPanel !== "none" - ? html` -
{ - e.preventDefault(); - const sx = e.clientX, - sw = rightPanelWidth; - const mv = (me: MouseEvent) => { - rightPanelWidth = Math.max(280, Math.min(800, sw - (me.clientX - sx))); - const p = document.getElementById("right-panel"); - if (p) p.style.width = rightPanelWidth + "px"; - }; - const up = () => { - document.removeEventListener("mousemove", mv); - document.removeEventListener("mouseup", up); - renderApp(); - }; - document.addEventListener("mousemove", mv); - document.addEventListener("mouseup", up); - }} - @mouseenter=${(e: Event) => { - (e.currentTarget as HTMLElement).style.background = "rgba(128,128,128,0.4)"; - }} - @mouseleave=${(e: Event) => { - (e.currentTarget as HTMLElement).style.background = "transparent"; - }} ->
-
-
- - -
- -
-${rightPanel === "terminal" ? html`` : html``} -${rightPanel === "browser" ? html`` : html``} -
-` - : html`` -} -
-`, - app, - ); -}; async function initApp() { - const app = document.getElementById("app"); - if (!app) throw new Error("App container not found"); - render( - html`
Loading...
`, - app, - ); - chatPanel = new ChatPanel(); - setupCommands(); - await refreshSidebar(); - const sessionIdFromUrl = new URLSearchParams(window.location.search).get("session"); - if (sessionIdFromUrl) { - const loaded = await loadSession(sessionIdFromUrl); - if (!loaded) { - newSession(); - return; - } - } else { - await createAgent(); - } - renderApp(); + const app = document.getElementById("app"); + if (!app) throw new Error("App container not found"); + render( + html`
+
+ JAE +
Loading JAE...
+
+
`, + app, + ); + chatPanel = new ChatPanel(); + setupCommands(); + await refreshSidebar(); + const sessionIdFromUrl = new URLSearchParams(window.location.search).get("session"); + if (sessionIdFromUrl) { + const loaded = await loadSession(sessionIdFromUrl); + if (!loaded) newSession(); + } else { + await createAgent(); + } + renderApp(); } initApp();