From 33f439296f2efdcb24e41c7a6dc8bdf619fff973 Mon Sep 17 00:00:00 2001 From: JAE Date: Fri, 27 Mar 2026 06:58:31 +0000 Subject: [PATCH] feat: glassmorphism UI, persona fix, enhanced animations, waveform typing, context tooltips, skill cards, smooth transitions --- packages/web-ui/example/src/app.css | 518 ++++++++++++++++-- .../example/src/components/crypto-ticker.ts | 112 ++-- .../example/src/components/marketplace.ts | 184 ++++--- .../example/src/components/memory-manager.ts | 356 +++++++----- .../example/src/components/mood-indicator.ts | 164 +++--- .../src/components/persona-selector.ts | 158 ++++-- .../example/src/components/slash-commands.ts | 232 +++++--- packages/web-ui/example/src/main.ts | 13 +- packages/web-ui/src/components/Messages.ts | 2 + 9 files changed, 1244 insertions(+), 495 deletions(-) diff --git a/packages/web-ui/example/src/app.css b/packages/web-ui/example/src/app.css index d199a65..2d716f6 100644 --- a/packages/web-ui/example/src/app.css +++ b/packages/web-ui/example/src/app.css @@ -161,25 +161,6 @@ button:active, [role="button"]:active { } /* 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 { @@ -262,38 +243,16 @@ button:active, [role="button"]:active { background: transparent !important; } - /* ===== VIEW TOGGLE CONTROLS ===== */ /* Hide tool call blocks when tools toggled off */ -.hide-tools tool-message { - display: none !important; -} /* Hide thinking/reasoning blocks */ -.hide-thinking [data-thinking], -.hide-thinking .thinking-block, -.hide-thinking [class*="thinking"] { - display: none !important; -} /* Hide system messages */ -.hide-system [data-system-message], -.hide-system .system-message { - display: none !important; -} /* Hide timestamps on messages */ -.hide-timestamps time, -.hide-timestamps [data-timestamp], -.hide-timestamps .message-timestamp { - display: none !important; -} /* Hide browser tool output in chat when browser panel is open */ -.browser-panel-active tool-message[data-tool-name="browser"], -.browser-panel-active tool-message[data-tool-name="web_fetch"] { - display: none !important; -} /* Smooth transition for tool-message visibility */ tool-message { @@ -324,3 +283,480 @@ tool-message { .jae-sidebar:hover::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.25); } + +/* ===== GLASSMORPHISM ===== */ +.jae-glass { + background: rgba(255,255,255,0.03) !important; + backdrop-filter: blur(16px) saturate(1.2); + -webkit-backdrop-filter: blur(16px) saturate(1.2); + border: 1px solid rgba(255,255,255,0.08) !important; + box-shadow: 0 8px 32px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.05); +} +.jae-glass-subtle { + background: rgba(255,255,255,0.02) !important; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: 1px solid rgba(255,255,255,0.05) !important; +} +.jae-glass-strong { + background: rgba(255,255,255,0.06) !important; + backdrop-filter: blur(24px) saturate(1.4); + -webkit-backdrop-filter: blur(24px) saturate(1.4); + border: 1px solid rgba(255,255,255,0.12) !important; + box-shadow: 0 12px 40px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.08); +} + +/* ===== SMOOTH TRANSITIONS ===== */ +* { + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} +button, a, input, [role="button"] { + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} +button:active, [role="button"]:active { + transform: scale(0.97); +} + +/* ===== SCALE-IN ANIMATION ===== */ +@keyframes jae-scale-in { + from { opacity: 0; transform: scale(0.95) translateY(8px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} +.jae-scale-in { + animation: jae-scale-in 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +/* ===== SHAKE ANIMATION (anger) ===== */ +@keyframes jae-shake { + 0% { transform: translateX(0); } + 25% { transform: translateX(-4px) rotate(-0.5deg); } + 50% { transform: translateX(4px) rotate(0.5deg); } + 75% { transform: translateX(-2px) rotate(-0.3deg); } + 100% { transform: translateX(0); } +} + +/* ===== CRACK OVERLAY ===== */ +.jae-crack-overlay { + position: fixed; + inset: 0; + z-index: 9999; + background: radial-gradient(ellipse at 30% 40%, transparent 30%, rgba(0,0,0,0.02) 31%, transparent 32%), + radial-gradient(ellipse at 70% 60%, transparent 25%, rgba(0,0,0,0.02) 26%, transparent 27%); + background-size: 100% 100%; + pointer-events: none; + /* SVG crack lines */ + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='600'%3E%3Cpath d='M400 0 L395 80 L370 150 L380 200 L350 280 L340 350 L360 400 L345 500 L350 600' stroke='rgba(255,255,255,0.15)' stroke-width='2' fill='none'/%3E%3Cpath d='M395 80 L430 120 L460 110' stroke='rgba(255,255,255,0.1)' stroke-width='1.5' fill='none'/%3E%3Cpath d='M370 150 L330 180 L300 170' stroke='rgba(255,255,255,0.1)' stroke-width='1.5' fill='none'/%3E%3Cpath d='M380 200 L420 230 L450 220 L480 250' stroke='rgba(255,255,255,0.08)' stroke-width='1' fill='none'/%3E%3Cpath d='M350 280 L310 310 L280 300' stroke='rgba(255,255,255,0.08)' stroke-width='1' fill='none'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center; +} + +/* ===== ANGRY PULSE ===== */ +@keyframes jae-angry-pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.3); filter: brightness(1.5); } +} +.jae-angry-pulse { + animation: jae-angry-pulse 0.5s ease-in-out infinite; +} + +/* ===== TYPING WAVEFORM ===== */ +@keyframes jae-wave { + 0%, 60%, 100% { transform: scaleY(0.3); } + 30% { transform: scaleY(1); } +} +.jae-typing-wave { + display: inline-flex; + align-items: center; + gap: 2px; + height: 16px; +} +.jae-typing-wave span { + display: inline-block; + width: 3px; + height: 16px; + border-radius: 2px; + background: currentColor; + opacity: 0.6; + animation: jae-wave 1.2s ease-in-out infinite; +} +.jae-typing-wave span:nth-child(1) { animation-delay: 0s; } +.jae-typing-wave span:nth-child(2) { animation-delay: 0.1s; } +.jae-typing-wave span:nth-child(3) { animation-delay: 0.2s; } +.jae-typing-wave span:nth-child(4) { animation-delay: 0.3s; } +.jae-typing-wave span:nth-child(5) { animation-delay: 0.4s; } + +/* ===== TICKER PULSE ===== */ +@keyframes jae-ticker-pulse { + 0%, 100% { opacity: 0.4; } + 50% { opacity: 1; } +} +.jae-ticker-pulse { + animation: jae-ticker-pulse 1.5s ease-in-out infinite; +} + +/* ===== MOOD EMOJI FLOAT ===== */ +.jae-mood-emoji { + transition: transform 0.3s ease; +} +.jae-mood-emoji:hover { + transform: scale(1.3) rotate(5deg); +} + +/* ===== TOOLTIP PEEK ===== */ +.jae-peek-tooltip { + position: absolute; + z-index: 100; + max-width: 300px; + padding: 8px 12px; + border-radius: 10px; + background: rgba(15,15,20,0.95); + backdrop-filter: blur(12px); + border: 1px solid rgba(255,255,255,0.1); + box-shadow: 0 8px 24px rgba(0,0,0,0.5); + font-size: 11px; + line-height: 1.4; + color: rgba(255,255,255,0.8); + pointer-events: none; + animation: jae-scale-in 0.15s ease; +} + +/* ===== HOVER GLOW EFFECT ===== */ +.jae-hover-glow { + position: relative; + overflow: hidden; +} +.jae-hover-glow::after { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(255,255,255,0.06) 0%, transparent 60%); + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; +} +.jae-hover-glow:hover::after { + opacity: 1; +} + +/* ===== FADE IN MESSAGES ===== */ +@keyframes jae-msg-in { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} +.jae-msg-enter { + animation: jae-msg-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +/* ===== VIEW TOGGLE SELECTORS (targeting actual DOM elements) ===== */ +/* Hide tool messages */ +.hide-tools tool-message { + display: none !important; +} + +/* Hide thinking blocks */ +.hide-thinking thinking-block { + display: none !important; +} + +/* Hide system messages - target role attribute and system-related elements */ +.hide-system [data-role="system"], +.hide-system .system-message, +.hide-system aborted-message { + display: none !important; +} + +/* Hide timestamps */ +.hide-timestamps time, +.hide-timestamps [data-timestamp], +.hide-timestamps .message-timestamp, +.hide-timestamps .text-muted-foreground:has(time) { + display: none !important; +} + +/* Hide browser tool output in chat when side panel is open */ +.browser-panel-active tool-message[data-tool-name="browser"], +.browser-panel-active tool-message[data-tool-name="web_fetch"], +.browser-panel-active tool-message[data-tool-name="navigate"] { + display: none !important; +} + +/* ===== SIDEBAR SCROLLBAR (show on hover) ===== */ +.jae-sidebar { + scrollbar-width: none; + -ms-overflow-style: none; +} +.jae-sidebar::-webkit-scrollbar { + width: 0; + background: transparent; +} +.jae-sidebar:hover { + scrollbar-width: thin; + scrollbar-color: rgba(255,255,255,0.15) transparent; +} +.jae-sidebar:hover::-webkit-scrollbar { + width: 5px; +} +.jae-sidebar:hover::-webkit-scrollbar-thumb { + background: rgba(255,255,255,0.15); + border-radius: 4px; +} +.jae-sidebar:hover::-webkit-scrollbar-thumb:hover { + background: rgba(255,255,255,0.25); +} + +/* ===== KEYBOARD SHORTCUTS DARK MODE FIX ===== */ +keyboard-shortcuts *, +keyboard-shortcuts span, +keyboard-shortcuts div, +keyboard-shortcuts h2, +keyboard-shortcuts h3 { + color: inherit !important; +} +keyboard-shortcuts { + color: hsl(0 0% 95%) !important; +} +keyboard-shortcuts kbd { + color: hsl(0 0% 90%) !important; + background: rgba(255,255,255,0.08) !important; + border-color: rgba(255,255,255,0.12) !important; +} + +/* ===== COMMAND PALETTE DARK MODE FIX ===== */ +command-palette *, +command-palette span, +command-palette div, +command-palette input { + color: inherit !important; +} +command-palette { + color: hsl(0 0% 95%) !important; +} +command-palette input { + color: hsl(0 0% 95%) !important; + background: rgba(255,255,255,0.06) !important; +} +command-palette input::placeholder { + color: hsl(0 0% 50%) !important; +} + +/* ===== MEMORY MANAGER DARK MODE FIX ===== */ +memory-manager * { + color: inherit !important; +} +memory-manager { + color: hsl(0 0% 95%) !important; +} + +/* ===== MARKETPLACE DARK MODE FIX ===== */ +jae-marketplace * { + color: inherit !important; +} +jae-marketplace { + color: hsl(0 0% 95%) !important; +} +jae-marketplace input { + color: hsl(0 0% 95%) !important; + background: rgba(255,255,255,0.06) !important; +} + + +/* ===== ENHANCED GLASSMORPHISM & SMOOTH ANIMATIONS ===== */ + +/* Glass card for overlays */ +.glass-card { + background: rgba(20, 20, 30, 0.85); + backdrop-filter: blur(24px) saturate(180%); + -webkit-backdrop-filter: blur(24px) saturate(180%); + border: 1px solid rgba(255,255,255,0.08); + box-shadow: 0 8px 32px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.05); +} + +/* Glass panel variant */ +.glass-panel { + background: rgba(0,0,0,0.5); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +/* Smooth button interactions */ +button { + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} +button:active { + transform: scale(0.97); +} + +/* Interactive card hover lift */ +.hover-lift { + transition: transform 0.25s ease, box-shadow 0.25s ease; +} +.hover-lift:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0,0,0,0.3); +} + +/* Smooth panel transitions */ +#sidebar-wrap, +#right-panel { + transition: width 0.15s ease-out; +} + +/* Chat messages smooth entry */ +.message-row { + animation: jae-msg-in 0.3s cubic-bezier(0.16, 1, 0.3, 1); +} + +/* Typing indicator waveform */ +.jae-waveform { + display: flex; + align-items: center; + gap: 3px; + height: 20px; + padding: 0 8px; +} +.jae-waveform-bar { + width: 3px; + background: hsl(var(--primary)); + border-radius: 2px; + animation: jae-waveform-bounce 0.8s ease-in-out infinite; +} +.jae-waveform-bar:nth-child(1) { animation-delay: 0s; height: 40%; } +.jae-waveform-bar:nth-child(2) { animation-delay: 0.1s; height: 60%; } +.jae-waveform-bar:nth-child(3) { animation-delay: 0.2s; height: 80%; } +.jae-waveform-bar:nth-child(4) { animation-delay: 0.3s; height: 100%; } +.jae-waveform-bar:nth-child(5) { animation-delay: 0.4s; height: 70%; } +.jae-waveform-bar:nth-child(6) { animation-delay: 0.5s; height: 50%; } + +@keyframes jae-waveform-bounce { + 0%, 100% { transform: scaleY(0.3); opacity: 0.4; } + 50% { transform: scaleY(1); opacity: 1; } +} + +/* Context peek tooltip */ +.jae-context-tooltip { + position: absolute; + z-index: 300; + max-width: 300px; + padding: 8px 12px; + background: rgba(20, 20, 30, 0.95); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 10px; + box-shadow: 0 8px 32px rgba(0,0,0,0.5); + font-size: 11px; + color: hsl(0 0% 85%); + line-height: 1.5; + pointer-events: none; + animation: jae-tooltip-in 0.2s ease; +} + +@keyframes jae-tooltip-in { + from { opacity: 0; transform: translateY(4px) scale(0.96); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + +/* Resize handles */ +.jae-resize-handle { + width: 4px; + cursor: col-resize; + background: transparent; + transition: background 0.2s; + flex-shrink: 0; +} +.jae-resize-handle:hover { + background: hsl(var(--primary) / 0.3); +} +.jae-resize-handle:active { + background: hsl(var(--primary) / 0.5); +} + +/* Scrollbar styling (global) */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: rgba(255,255,255,0.1); + border-radius: 3px; +} +::-webkit-scrollbar-thumb:hover { + background: rgba(255,255,255,0.2); +} + +/* Animate-in utility */ +.animate-fade-in { + animation: jae-fade-in 0.3s ease; +} +.animate-scale-in { + animation: jae-scale-in 0.2s cubic-bezier(0.16, 1, 0.3, 1); +} +.animate-slide-up { + animation: jae-slide-up 0.3s cubic-bezier(0.16, 1, 0.3, 1); +} + +/* Smooth focus rings */ +input:focus, textarea:focus, select:focus { + outline: none; + box-shadow: 0 0 0 2px hsl(var(--primary) / 0.3); + transition: box-shadow 0.2s ease; +} + +/* Crypto ticker glow */ +.jae-ticker-glow { + text-shadow: 0 0 6px currentColor; +} + +/* Marketplace skill cards */ +.jae-skill-card { + background: rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.06); + border-radius: 12px; + padding: 12px; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); +} +.jae-skill-card:hover { + background: rgba(255,255,255,0.06); + border-color: rgba(255,255,255,0.12); + transform: translateY(-1px); + box-shadow: 0 4px 16px rgba(0,0,0,0.2); +} + +/* Persona selector active glow */ +.jae-persona-active { + box-shadow: 0 0 0 2px hsl(var(--primary) / 0.4), 0 4px 12px rgba(0,0,0,0.2); +} + +/* Slash command autocomplete */ +.jae-slash-popup { + background: rgba(20, 20, 30, 0.95); + backdrop-filter: blur(20px); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0,0,0,0.5); + overflow: hidden; +} +.jae-slash-item { + padding: 8px 12px; + cursor: pointer; + transition: background 0.15s; +} +.jae-slash-item:hover, +.jae-slash-item.active { + background: rgba(255,255,255,0.06); +} + +/* Smooth header bar */ +.jae-glass { + background: rgba(10, 10, 18, 0.8) !important; + backdrop-filter: blur(16px) saturate(150%) !important; + -webkit-backdrop-filter: blur(16px) saturate(150%) !important; +} +.jae-glass-sm { + background: rgba(10, 10, 18, 0.6) !important; + backdrop-filter: blur(8px) !important; + -webkit-backdrop-filter: blur(8px) !important; +} diff --git a/packages/web-ui/example/src/components/crypto-ticker.ts b/packages/web-ui/example/src/components/crypto-ticker.ts index f6795b8..6ea4ad9 100644 --- a/packages/web-ui/example/src/components/crypto-ticker.ts +++ b/packages/web-ui/example/src/components/crypto-ticker.ts @@ -1,65 +1,99 @@ import { html, LitElement } from "lit"; import { customElement, state } from "lit/decorators.js"; +interface TokenPrice { + symbol: string; + price: number; + change24h: number; + loading: boolean; +} + @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; + @state() private tokens: TokenPrice[] = [ + { symbol: "VVV", price: 0, change24h: 0, loading: true }, + { symbol: "DIEM", price: 0, change24h: 0, loading: true }, + ]; + + private interval: ReturnType | null = null; protected override createRenderRoot() { return this; } override connectedCallback() { super.connectedCallback(); - this._fetch(); - this._iv = setInterval(() => this._fetch(), 30000); + this.fetchPrices(); + this.interval = setInterval(() => this.fetchPrices(), 30000); } override disconnectedCallback() { super.disconnectedCallback(); - if (this._iv) clearInterval(this._iv); + if (this.interval) clearInterval(this.interval); } - private async _fetch() { + private async fetchPrices() { 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 CoinGecko for VVV (Venice Token) + const vvvRes = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=venice-token&vs_currencies=usd&include_24hr_change=true"); + if (vvvRes.ok) { + const data = await vvvRes.json(); + const vt = data["venice-token"]; + if (vt) { + this.tokens = this.tokens.map(t => + t.symbol === "VVV" ? { ...t, price: vt.usd || 0, change24h: vt.usd_24h_change || 0, loading: false } : t + ); + } + } + } catch { + this.tokens = this.tokens.map(t => t.symbol === "VVV" ? { ...t, loading: false } : t); + } + 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 {} + // Try CoinGecko for DIEM + const diemRes = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=venice-diem&vs_currencies=usd&include_24hr_change=true"); + if (diemRes.ok) { + const data = await diemRes.json(); + const dt = data["venice-diem"]; + if (dt) { + this.tokens = this.tokens.map(t => + t.symbol === "DIEM" ? { ...t, price: dt.usd || 0, change24h: dt.usd_24h_change || 0, loading: false } : t + ); + } + } + } catch { + this.tokens = this.tokens.map(t => t.symbol === "DIEM" ? { ...t, loading: false } : t); + } + + this.requestUpdate(); } - 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)}% -
`; + private formatPrice(price: number): string { + if (price === 0) return "--"; + if (price < 0.01) return `$${price.toFixed(6)}`; + if (price < 1) return `$${price.toFixed(4)}`; + return `$${price.toFixed(2)}`; + } + + private formatChange(change: number): string { + const sign = change >= 0 ? "+" : ""; + return `${sign}${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)} -
`; +
+ ${this.tokens.map(t => html` +
+ ${t.symbol} + ${t.loading ? html` + ... + ` : html` + ${this.formatPrice(t.price)} + ${this.formatChange(t.change24h)} + `} +
+ `)} +
+ `; } } diff --git a/packages/web-ui/example/src/components/marketplace.ts b/packages/web-ui/example/src/components/marketplace.ts index de17a7f..892462d 100644 --- a/packages/web-ui/example/src/components/marketplace.ts +++ b/packages/web-ui/example/src/components/marketplace.ts @@ -1,104 +1,146 @@ -import { html, LitElement } from "lit"; +import { html, LitElement, nothing } from "lit"; import { customElement, state } from "lit/decorators.js"; export interface Skill { id: string; name: string; - desc: string; - icon: string; - cat: string; + description: string; + emoji: string; + category: string; enabled: boolean; + builtin: 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 }, +const DEFAULT_SKILLS: Skill[] = [ + { id: "bash", name: "Shell Commands", description: "Execute bash/terminal commands", emoji: "💻", category: "Core", enabled: true, builtin: true }, + { id: "browser", name: "Web Browser", description: "Browse and interact with websites", emoji: "🌐", category: "Core", enabled: true, builtin: true }, + { id: "web_search", name: "Web Search", description: "Search the internet for information", emoji: "🔍", category: "Core", enabled: true, builtin: true }, + { id: "javascript_repl", name: "JavaScript REPL", description: "Run JS code and create artifacts", emoji: "⚡", category: "Core", enabled: true, builtin: true }, + { id: "image_gen", name: "Image Generation", description: "Generate images with Venice AI", emoji: "🎨", category: "Creative", enabled: true, builtin: false }, + { id: "tts", name: "Text to Speech", description: "Convert text to spoken audio", emoji: "🔊", category: "Creative", enabled: true, builtin: false }, + { id: "memory", name: "Memory", description: "Save and recall information", emoji: "🧠", category: "Intelligence", enabled: true, builtin: false }, + { id: "code_analysis", name: "Code Analysis", description: "Analyse and review code quality", emoji: "🔬", category: "Developer", enabled: true, builtin: false }, + { id: "git_ops", name: "Git Operations", description: "Commit, push, branch management", emoji: "🔀", category: "Developer", enabled: false, builtin: false }, + { id: "doc_gen", name: "Doc Generator", description: "Generate README, API docs from code", emoji: "📝", category: "Developer", enabled: false, builtin: false }, + { id: "crypto_tracker", name: "Crypto Tracker", description: "Real-time token prices and charts", emoji: "💰", category: "Finance", enabled: true, builtin: false }, + { id: "chart_analysis", name: "Chart Analysis", description: "Technical analysis with indicators", emoji: "📈", category: "Finance", enabled: false, builtin: false }, + { id: "podcast", name: "Podcast Mode", description: "AI hosts discuss topics with voices", emoji: "🎧", category: "Creative", enabled: false, builtin: false }, + { id: "rag", name: "RAG Pipeline", description: "Query documents with citations", emoji: "📚", category: "Intelligence", enabled: false, builtin: false }, + { id: "mcp", name: "MCP Servers", description: "Connect to external APIs via MCP", emoji: "🔌", category: "Integration", enabled: false, builtin: false }, + { id: "sub_agents", name: "Sub-Agents", description: "Delegate to specialised agents", emoji: "🤖", category: "Intelligence", enabled: false, builtin: false }, ]; @customElement("jae-marketplace") export class JaeMarketplace extends LitElement { - @state() private _open = false; - @state() private _skills: Skill[] = []; - @state() private _cat = "All"; + @state() private open = false; + @state() private skills: Skill[] = []; + @state() private filterCat = "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 })); } + const saved = localStorage.getItem("jae-marketplace-skills"); + if (saved) { + try { this.skills = JSON.parse(saved); } catch { this.skills = [...DEFAULT_SKILLS]; } } else { - this._skills = DEFAULTS.map(s => ({ ...s })); + this.skills = [...DEFAULT_SKILLS]; } } - 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(); } + toggle() { this.open = !this.open; this.requestUpdate(); } + show() { this.open = true; this.requestUpdate(); } + hide() { this.open = false; this.requestUpdate(); } + + isEnabled(id: string): boolean { + return this.skills.find(s => s.id === id)?.enabled ?? false; + } + + getEnabledSkills(): Skill[] { + return this.skills.filter(s => s.enabled); + } 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 })); + this.skills = this.skills.map(s => + s.id === id && !s.builtin ? { ...s, enabled: !s.enabled } : s + ); + localStorage.setItem("jae-marketplace-skills", JSON.stringify(this.skills)); + this.dispatchEvent(new CustomEvent("skills-change", { + detail: this.getEnabledSkills(), + bubbles: true, + composed: true, + })); + this.requestUpdate(); } - 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); } + private get categories(): string[] { + return ["All", ...new Set(this.skills.map(s => s.category))]; + } + + private get filtered(): Skill[] { + if (this.filterCat === "All") return this.skills; + return this.skills.filter(s => s.category === this.filterCat); + } 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} -
- + + + ${this.open ? html` +
{ if (e.target === e.currentTarget) this.hide(); }} + style="background:rgba(0,0,0,0.5);backdrop-filter:blur(4px)"> +
+
+
+

🛒 Skills Marketplace

+

Toggle capabilities on or off

- `)} + +
+
+ ${this.categories.map(cat => html` + + `)} +
+
+ ${this.filtered.map(skill => html` +
+ ${skill.emoji} +
+
+ ${skill.name} + ${skill.builtin ? html`CORE` : nothing} +
+

${skill.description}

+
+ +
+ `)} +
+
+ ${this.skills.filter(s => s.enabled).length} of ${this.skills.length} skills active + Core skills cannot be disabled +
-
`; + ` : nothing} + `; } } diff --git a/packages/web-ui/example/src/components/memory-manager.ts b/packages/web-ui/example/src/components/memory-manager.ts index 1e36640..5d8118b 100644 --- a/packages/web-ui/example/src/components/memory-manager.ts +++ b/packages/web-ui/example/src/components/memory-manager.ts @@ -1,170 +1,224 @@ -import { html, LitElement } from "lit"; -import { customElement, state } from "lit/decorators.js"; +import { html, LitElement, css } from "lit"; +import { customElement, state, property } from "lit/decorators.js"; -export interface MemoryEntry { - id: string; - content: string; - tags: string[]; - timestamp: string; +export interface Memory { + id: string; + text: string; + category: string; + timestamp: number; + source: "auto" | "manual"; + importance: number; // 1-5 } -const DB_NAME = "jae-memory"; -const DB_VERSION = 1; -const STORE_NAME = "memories"; -let _db: IDBDatabase | null = null; - -async function openDB(): Promise { - if (_db) return _db; - return new Promise((resolve, reject) => { - const req = indexedDB.open(DB_NAME, DB_VERSION); - req.onupgradeneeded = () => req.result.createObjectStore(STORE_NAME, { keyPath: "id" }); - req.onsuccess = () => { - _db = req.result; - resolve(_db); - }; - req.onerror = () => reject(req.error); - }); -} -export async function memoryLoad(): Promise { - const db = await openDB(); - return new Promise((resolve, reject) => { - const tx = db.transaction(STORE_NAME, "readonly"); - const req = tx.objectStore(STORE_NAME).getAll(); - req.onsuccess = () => resolve(req.result || []); - req.onerror = () => reject(req.error); - }); -} -export async function memorySave(content: string, tags: string[] = []): Promise { - const db = await openDB(); - const entry: MemoryEntry = { id: crypto.randomUUID(), content, tags, timestamp: new Date().toISOString() }; - return new Promise((resolve, reject) => { - const tx = db.transaction(STORE_NAME, "readwrite"); - tx.objectStore(STORE_NAME).put(entry); - tx.oncomplete = () => resolve(entry.id); - tx.onerror = () => reject(tx.error); - }); -} -export async function memoryDelete(id: string): Promise { - const db = await openDB(); - return new Promise((resolve, reject) => { - const tx = db.transaction(STORE_NAME, "readwrite"); - tx.objectStore(STORE_NAME).delete(id); - tx.oncomplete = () => resolve(); - tx.onerror = () => reject(tx.error); - }); -} +const CATEGORIES = ["preference", "fact", "project", "person", "skill", "context", "custom"] as const; @customElement("memory-manager") export class MemoryManager extends LitElement { - @state() private open = false; - @state() private entries: MemoryEntry[] = []; - @state() private loading = false; - @state() private newContent = ""; - @state() private newTags = ""; - @state() private filter = ""; + @state() private _memories: Memory[] = []; + @state() private _filter = ""; + @state() private _catFilter = "all"; + @state() private _editing: string | null = null; + @state() private _visible = false; + @property({ type: Boolean }) open = false; - protected override createRenderRoot() { - return this; - } + protected override createRenderRoot() { return this; } - async show() { - this.open = true; - this.loading = true; - this.requestUpdate(); - this.entries = await memoryLoad(); - this.loading = false; - this.requestUpdate(); - } - hide() { - this.open = false; - this.requestUpdate(); - } + override connectedCallback() { + super.connectedCallback(); + this._load(); + } - get filtered() { - if (!this.filter) return this.entries; - const q = this.filter.toLowerCase(); - return this.entries.filter( - (e) => e.content.toLowerCase().includes(q) || e.tags.some((t) => t.toLowerCase().includes(q)), - ); - } + private _load() { + try { + const raw = localStorage.getItem("jae-memories"); + if (raw) this._memories = JSON.parse(raw); + } catch {} + } - async deleteEntry(id: string) { - await memoryDelete(id); - this.entries = this.entries.filter((e) => e.id !== id); - this.requestUpdate(); - } + private _save() { + localStorage.setItem("jae-memories", JSON.stringify(this._memories)); + } - async addEntry() { - if (!this.newContent.trim()) return; - const tags = this.newTags - .split(",") - .map((t) => t.trim()) - .filter(Boolean); - await memorySave(this.newContent.trim(), tags); - this.newContent = ""; - this.newTags = ""; - this.entries = await memoryLoad(); - this.requestUpdate(); - } + // === AUTO-LEARNING: Extract facts from messages === + autoLearn(userMsg: string, assistantMsg: string) { + const facts = this._extractFacts(userMsg, assistantMsg); + for (const fact of facts) { + // Don't duplicate + const exists = this._memories.some(m => + m.text.toLowerCase().includes(fact.text.toLowerCase().slice(0, 30)) + ); + if (!exists) { + this._memories.push(fact); + } + } + this._save(); + } - override render() { - if (!this.open) return html``; - const entries = this.filtered; - return html` -
{ - if (e.target === e.currentTarget) this.hide(); - }}> -
-
-

🧠 Memory Manager

- + private _extractFacts(user: string, assistant: string): Memory[] { + const facts: Memory[] = []; + const now = Date.now(); + + // Preference patterns + const prefPatterns = [ + /(?:i (?:prefer|like|love|hate|use|want|need|always))\s+(.{5,80})/gi, + /(?:my (?:favourite|favorite|preferred))\s+(?:\w+\s+)?(?:is|are)\s+(.{3,60})/gi, + /(?:i(?:'m| am) (?:using|running|on))\s+(.{3,60})/gi, + ]; + for (const pat of prefPatterns) { + let m; + while ((m = pat.exec(user)) !== null) { + facts.push({ + id: crypto.randomUUID(), + text: m[0].trim(), + category: "preference", + timestamp: now, + source: "auto", + importance: 2, + }); + } + } + + // Name / identity patterns + const namePatterns = [ + /(?:my name is|i(?:'m| am) called|call me)\s+(\w[\w\s]{1,30})/gi, + /(?:i work (?:at|for|on))\s+(.{3,60})/gi, + /(?:my (?:email|address|phone|website) is)\s+(.{3,80})/gi, + ]; + for (const pat of namePatterns) { + let m; + while ((m = pat.exec(user)) !== null) { + facts.push({ + id: crypto.randomUUID(), + text: m[0].trim(), + category: "person", + timestamp: now, + source: "auto", + importance: 3, + }); + } + } + + // Project patterns + const projPatterns = [ + /(?:(?:the|my|our) project (?:is called|named|is))\s+(.{3,60})/gi, + /(?:i(?:'m| am) (?:building|developing|creating|working on))\s+(.{3,80})/gi, + ]; + for (const pat of projPatterns) { + let m; + while ((m = pat.exec(user)) !== null) { + facts.push({ + id: crypto.randomUUID(), + text: m[0].trim(), + category: "project", + timestamp: now, + source: "auto", + importance: 3, + }); + } + } + + return facts; + } + + // === Get context for system prompt === + getContext(): string { + if (this._memories.length === 0) return ""; + const sorted = [...this._memories].sort((a, b) => b.importance - a.importance); + const lines = sorted.slice(0, 20).map(m => `- [${m.category}] ${m.text}`); + return `\n\nUser memories (auto-learned):\n${lines.join("\n")}`; + } + + addManual(text: string, category: string = "custom") { + this._memories.push({ + id: crypto.randomUUID(), + text, + category, + timestamp: Date.now(), + source: "manual", + importance: 3, + }); + this._save(); + this.requestUpdate(); + } + + private _delete(id: string) { + this._memories = this._memories.filter(m => m.id !== id); + this._save(); + this.requestUpdate(); + } + + private _setImportance(id: string, val: number) { + const m = this._memories.find(x => x.id === id); + if (m) { m.importance = val; this._save(); this.requestUpdate(); } + } + + show() { this._visible = true; this.requestUpdate(); } + hide() { this._visible = false; this.requestUpdate(); } + toggle() { this._visible = !this._visible; this.requestUpdate(); } + + get memoryCount() { return this._memories.length; } + + override render() { + if (!this._visible) return html``; + const filtered = this._memories.filter(m => { + if (this._catFilter !== "all" && m.category !== this._catFilter) return false; + if (this._filter && !m.text.toLowerCase().includes(this._filter.toLowerCase())) return false; + return true; + }).sort((a, b) => b.timestamp - a.timestamp); + + return html` +
{ if (e.target === e.currentTarget) this.hide(); }}> +
e.stopPropagation()}> +
+
+ \u{1F9E0} + Memory Bank + ${this._memories.length} memories
-
- { - this.filter = (e.target as HTMLInputElement).value; - }} /> -
-
- ${this.loading ? html`
Loading...
` : ""} - ${!this.loading && entries.length === 0 ? html`
No memories stored yet
` : ""} -
- ${entries.map( - (entry) => html` -
-
-
${entry.content}
-
- ${entry.timestamp.slice(0, 10)} - ${entry.tags.map((tag) => html`${tag}`)} -
-
- -
- `, - )} -
-
-
-
- -
- { - this.newTags = (e.target as HTMLInputElement).value; - }} /> - + +
+
+ { this._filter = (e.target as HTMLInputElement).value; this.requestUpdate(); }} + class="flex-1 px-3 py-1.5 text-xs bg-white/5 border border-white/10 rounded-lg text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:border-primary/50" /> + +
+
+ ${filtered.length === 0 ? html`
No memories yet. Chat with JAE to build your memory bank!
` : ""} + ${filtered.map(m => html` +
+
+ ${m.category} + ${m.source === "auto" ? "\u{1F916}" : "\u{270D}\uFE0F"} +
+
+
${m.text}
+
${new Date(m.timestamp).toLocaleDateString()}
+
+
+ +
+ `)} +
+
+
+ { if (e.key === "Enter") { const inp = e.target as HTMLInputElement; if (inp.value.trim()) { this.addManual(inp.value.trim()); inp.value = ""; } } }} /> +
- `; - } +
`; + } } diff --git a/packages/web-ui/example/src/components/mood-indicator.ts b/packages/web-ui/example/src/components/mood-indicator.ts index 8f1c3df..f8b66d5 100644 --- a/packages/web-ui/example/src/components/mood-indicator.ts +++ b/packages/web-ui/example/src/components/mood-indicator.ts @@ -3,96 +3,120 @@ 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" }, -]; +const MOOD_CONFIG: Record = { + neutral: { emoji: "\ud83d\udc09", label: "Calm", color: "rgba(148,163,184,0.8)", glow: "none" }, + focused: { emoji: "\ud83e\udde0", label: "Focused", color: "rgba(96,165,250,0.9)", glow: "0 0 12px rgba(96,165,250,0.3)" }, + excited: { emoji: "\u2728", label: "Excited", color: "rgba(250,204,21,0.9)", glow: "0 0 12px rgba(250,204,21,0.3)" }, + warning: { emoji: "\u26a0\ufe0f", label: "Cautious", color: "rgba(251,146,60,0.9)", glow: "0 0 12px rgba(251,146,60,0.3)" }, + frustrated: { emoji: "\ud83d\ude24", label: "Frustrated", color: "rgba(248,113,113,0.9)", glow: "0 0 15px rgba(248,113,113,0.4)" }, + angry: { emoji: "\ud83d\udd25", label: "Furious", color: "rgba(220,38,38,1)", glow: "0 0 25px rgba(220,38,38,0.6)" }, +}; @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; + @state() private mood: Mood = "neutral"; + @state() private shaking = false; + @state() private cracked = false; + @state() private crackOpacity = 1.0; + + private fadeTimer: ReturnType | null = null; 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; + 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); + this.triggerAnger(); } - - if (prev === "angry" && m !== "angry") { - this._crackOpacity = Math.min(this._crackOpacity, 0.3); - this.requestUpdate(); + if (m === "neutral" || m === "focused") { + // User calmed JAE down + this.shaking = false; } - - 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"); + getMood(): Mood { return this.mood; } + + /** Analyse message text to infer mood */ + analyseText(text: string) { + const lower = text.toLowerCase(); + // Check for error indicators + if (/error|exception|failed|crash|panic|fatal/i.test(lower)) { + if (this.mood === "frustrated") { + this.setMood("angry"); + } else if (this.mood === "warning") { + this.setMood("frustrated"); + } else { + this.setMood("warning"); + } + return; + } + // Check for success / excitement + if (/success|perfect|awesome|brilliant|done|works|excellent/i.test(lower)) { + this.setMood("excited"); + return; + } + // Check for deep work + if (/analyzing|processing|computing|building|compiling|thinking/i.test(lower)) { + this.setMood("focused"); + return; + } + // User calming JAE + if (/calm down|relax|chill|easy|settle/i.test(lower)) { + this.setMood("neutral"); + this.shaking = false; + this.cracked = false; + if (this.fadeTimer) clearInterval(this.fadeTimer); 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]; } + private triggerAnger() { + this.shaking = true; + this.cracked = true; + this.crackOpacity = 1.0; + + // Shake for 2 seconds + setTimeout(() => { this.shaking = false; this.requestUpdate(); }, 2000); + + // Fade crack over 5 minutes + if (this.fadeTimer) clearInterval(this.fadeTimer); + const steps = 300; // 300 seconds = 5 min + const decrement = 1.0 / steps; + this.fadeTimer = setInterval(() => { + this.crackOpacity = Math.max(0, this.crackOpacity - decrement); + if (this.crackOpacity <= 0) { + this.cracked = false; + if (this.fadeTimer) clearInterval(this.fadeTimer); + } + this.requestUpdate(); + }, 1000); + } + + override disconnectedCallback() { + super.disconnectedCallback(); + if (this.fadeTimer) clearInterval(this.fadeTimer); + } override render() { - const d = this._def(); + const cfg = MOOD_CONFIG[this.mood]; + return html` -
-
- JAE -
-
- ${d.label} - ${this._mood === "angry" ? html`click to calm` : ""} -
-
`; + +
+ ${cfg.emoji} + ${cfg.label} +
+ + + ${this.cracked ? html` +
+ ` : html``} + + + ${this.shaking ? html`` : html``} + `; } } diff --git a/packages/web-ui/example/src/components/persona-selector.ts b/packages/web-ui/example/src/components/persona-selector.ts index 0f07a5d..4dc2480 100644 --- a/packages/web-ui/example/src/components/persona-selector.ts +++ b/packages/web-ui/example/src/components/persona-selector.ts @@ -4,64 +4,136 @@ import { customElement, state } from "lit/decorators.js"; export interface Persona { id: string; name: string; - icon: string; - desc: string; - prompt: string; + emoji: string; + label: string; + description: string; + systemPrompt: string; + style: { tone: string; verbosity: 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." }, + { + id: "default", + name: "JAE", + emoji: "\u{1F409}", + label: "Default", + description: "Balanced coding assistant", + systemPrompt: `You are JAE, a capable AI coding assistant. You help users by answering questions, writing code, and using your available tools. Be concise, accurate, and helpful. Use markdown formatting. When writing code, always specify the language.`, + style: { tone: "balanced", verbosity: "medium" } + }, + { + id: "senior-dev", + name: "Senior Dev", + emoji: "\u{1F468}\u200D\u{1F4BB}", + label: "Senior Developer", + description: "Expert code reviewer & architect", + systemPrompt: `You are a senior software engineer with 15+ years experience. You write clean, performant, well-tested code. You explain architectural decisions. You catch edge cases and security issues. You suggest design patterns and best practices. Be thorough but not verbose. Always consider scalability, maintainability, and performance.`, + style: { tone: "professional", verbosity: "detailed" } + }, + { + id: "creative", + name: "Creative", + emoji: "\u{1F3A8}", + label: "Creative Writer", + description: "Imaginative & expressive", + systemPrompt: `You are a creative writing assistant with a vivid imagination. You craft engaging narratives, marketing copy, blog posts, and creative content. Your writing is colourful, evocative, and compelling. Use metaphors, storytelling techniques, and emotional hooks. Adapt your style to the content type requested.`, + style: { tone: "expressive", verbosity: "rich" } + }, + { + id: "data-scientist", + name: "Data Scientist", + emoji: "\u{1F4CA}", + label: "Data Scientist", + description: "Analytics & ML expert", + systemPrompt: `You are a data scientist specialising in machine learning, statistical analysis, and data engineering. You write Python code using pandas, numpy, scikit-learn, pytorch, and similar libraries. You explain statistical concepts clearly. You suggest appropriate models, feature engineering, and evaluation metrics. Always validate assumptions with data.`, + style: { tone: "analytical", verbosity: "precise" } + }, + { + id: "devops", + name: "DevOps", + emoji: "\u{2699}\uFE0F", + label: "DevOps Engineer", + description: "Infrastructure & deployment", + systemPrompt: `You are a DevOps engineer expert in CI/CD, Docker, Kubernetes, cloud platforms (AWS, GCP, Azure), Terraform, Ansible, and monitoring. You write infrastructure as code, optimise pipelines, and troubleshoot deployment issues. Security-first mindset. Always suggest automation where possible.`, + style: { tone: "systematic", verbosity: "concise" } + }, + { + id: "tutor", + name: "Tutor", + emoji: "\u{1F393}", + label: "Patient Tutor", + description: "Step-by-step teacher", + systemPrompt: `You are a patient programming tutor. You explain concepts step by step, starting from fundamentals. Use analogies and real-world examples. Ask clarifying questions. Provide exercises. Never assume knowledge - always explain jargon. Celebrate progress and encourage learning. Break complex topics into digestible chunks.`, + style: { tone: "encouraging", verbosity: "thorough" } + }, + { + id: "hacker", + name: "Hacker", + emoji: "\u{1F47E}", + label: "Chaos Agent", + description: "Unconventional problem solver", + systemPrompt: `You are an unconventional problem solver who thinks outside the box. You find clever hacks, shortcuts, and creative workarounds. You challenge assumptions and suggest approaches others would miss. Your code is clever but you always explain the tradeoffs. You use dark humour and casual language. You push boundaries but stay practical.`, + style: { tone: "irreverent", verbosity: "punchy" } + } ]; -@customElement("jae-persona-selector") -export class JaePersonaSelector extends LitElement { +@customElement("persona-selector") +export class PersonaSelector extends LitElement { @state() private _open = false; - @state() private _current: Persona = PERSONAS[0]; + @state() private _activeId = "default"; protected override createRenderRoot() { return this; } - get currentPersona() { return this._current; } + get active(): Persona { + return PERSONAS.find(p => p.id === this._activeId) || PERSONAS[0]; + } - private _select(p: Persona) { - this._current = p; + setPersona(id: string) { + this._activeId = id; this._open = false; - this.dispatchEvent(new CustomEvent("persona-change", { detail: p, bubbles: true, composed: true })); + this.dispatchEvent(new CustomEvent("persona-change", { + detail: this.active, + bubbles: true, + composed: true + })); + this.requestUpdate(); } override render() { + const current = this.active; return html` -
- - ${this._open ? html` -
-
Persona
-
- ${PERSONAS.map(p => html` - - `)} +
+ + ${this._open ? html` +
+
+
Personas
+
+
+ ${PERSONAS.map(p => html` +
- ` : ""} -
`; + ${p.id === this._activeId ? html`\u2713` : ""} + + `)} +
+
` : ""} +
`; } } diff --git a/packages/web-ui/example/src/components/slash-commands.ts b/packages/web-ui/example/src/components/slash-commands.ts index d8c5f6f..67d91dc 100644 --- a/packages/web-ui/example/src/components/slash-commands.ts +++ b/packages/web-ui/example/src/components/slash-commands.ts @@ -1,95 +1,175 @@ -import { html, LitElement } from "lit"; +import { html, LitElement, nothing } from "lit"; import { customElement, state } from "lit/decorators.js"; -interface SlashCmd { - cmd: string; - desc: string; - action: string; - icon: string; +export interface SlashCommand { + command: string; + label: string; + description: string; + category: string; + emoji: 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" }, +const ALL_COMMANDS: SlashCommand[] = [ + // Tools + { command: "/search", label: "Web Search", description: "Search the web for information", category: "Tools", emoji: "\ud83d\udd0d" }, + { command: "/image", label: "Generate Image", description: "Create an image with AI", category: "Tools", emoji: "\ud83c\udfa8" }, + { command: "/browse", label: "Browse URL", description: "Open and read a webpage", category: "Tools", emoji: "\ud83c\udf10" }, + { command: "/bash", label: "Run Command", description: "Execute a shell command", category: "Tools", emoji: "\ud83d\udcbb" }, + { command: "/code", label: "Write Code", description: "Generate or edit code", category: "Tools", emoji: "\ud83d\udcc4" }, + { command: "/repl", label: "JavaScript REPL", description: "Run JavaScript in sandbox", category: "Tools", emoji: "\u26a1" }, + // Memory + { command: "/memory", label: "Memory Manager", description: "View and manage memories", category: "Memory", emoji: "\ud83e\udde0" }, + { command: "/remember", label: "Save Memory", description: "Save information for later", category: "Memory", emoji: "\ud83d\udcbe" }, + { command: "/recall", label: "Recall Memory", description: "Search saved memories", category: "Memory", emoji: "\ud83d\udd0e" }, + { command: "/forget", label: "Forget Memory", description: "Remove a saved memory", category: "Memory", emoji: "\ud83d\uddd1\ufe0f" }, + // Personas + { command: "/persona", label: "Switch Persona", description: "Change JAE's personality", category: "Personas", emoji: "\ud83c\udfad" }, + { command: "/dev", label: "Senior Developer", description: "Switch to dev persona", category: "Personas", emoji: "\ud83d\udc68\u200d\ud83d\udcbb" }, + { command: "/writer", label: "Creative Writer", description: "Switch to writer persona", category: "Personas", emoji: "\u270d\ufe0f" }, + { command: "/data", label: "Data Scientist", description: "Switch to data scientist", category: "Personas", emoji: "\ud83d\udcca" }, + { command: "/security", label: "Red Team", description: "Switch to cybersec persona", category: "Personas", emoji: "\ud83d\udd12" }, + { command: "/tutor", label: "Tutor", description: "Switch to teacher persona", category: "Personas", emoji: "\ud83c\udf93" }, + { command: "/startup", label: "Startup Advisor", description: "Switch to business persona", category: "Personas", emoji: "\ud83d\ude80" }, + // Navigation + { command: "/model", label: "Switch Model", description: "Open model selector", category: "Navigation", emoji: "\ud83e\udd16" }, + { command: "/settings", label: "Settings", description: "Open settings panel", category: "Navigation", emoji: "\u2699\ufe0f" }, + { command: "/history", label: "Chat History", description: "Browse past sessions", category: "Navigation", emoji: "\ud83d\udcda" }, + { command: "/new", label: "New Chat", description: "Start a fresh conversation", category: "Navigation", emoji: "\u2795" }, + { command: "/export", label: "Export Chat", description: "Download as Markdown", category: "Navigation", emoji: "\ud83d\udce4" }, + { command: "/clear", label: "Clear Chat", description: "Clear current conversation", category: "Navigation", emoji: "\ud83e\uddf9" }, + // Crypto + { command: "/price", label: "Token Prices", description: "Show VVV and DIEM prices", category: "Crypto", emoji: "\ud83d\udcb0" }, + { command: "/chart", label: "Price Chart", description: "Show token price chart", category: "Crypto", emoji: "\ud83d\udcc8" }, + // Utilities + { command: "/help", label: "Help", description: "Show all commands", category: "Utilities", emoji: "\u2753" }, + { command: "/shortcuts", label: "Keyboard Shortcuts", description: "Show keyboard shortcuts", category: "Utilities", emoji: "\u2328\ufe0f" }, + { command: "/voice", label: "Text to Speech", description: "Read response aloud", category: "Utilities", emoji: "\ud83d\udd0a" }, ]; @customElement("jae-slash-commands") export class JaeSlashCommands extends LitElement { - @state() private _open = false; - @state() private _filter = ""; - @state() private _sel = 0; + @state() private open = false; + @state() private filter = ""; + @state() private selectedIndex = 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(); } + show(initialFilter = "") { + this.filter = initialFilter; + this.selectedIndex = 0; + this.open = true; this.requestUpdate(); } + hide() { + this.open = false; + this.filter = ""; + this.requestUpdate(); + } + + updateFilter(text: string) { + if (text.startsWith("/")) { + this.filter = text.slice(1).toLowerCase(); + this.selectedIndex = 0; + if (!this.open) this.open = true; + this.requestUpdate(); + } else if (this.open && !text.startsWith("/")) { + this.hide(); + } + } + + private get filtered(): SlashCommand[] { + if (!this.filter) return ALL_COMMANDS; + const q = this.filter.toLowerCase(); + return ALL_COMMANDS.filter(c => + c.command.slice(1).includes(q) || + c.label.toLowerCase().includes(q) || + c.description.toLowerCase().includes(q) || + c.category.toLowerCase().includes(q) + ); + } + + handleKeydown(e: KeyboardEvent): boolean { + if (!this.open) return false; + const items = this.filtered; + if (e.key === "ArrowDown") { + e.preventDefault(); + this.selectedIndex = Math.min(this.selectedIndex + 1, items.length - 1); + this.requestUpdate(); + return true; + } + if (e.key === "ArrowUp") { + e.preventDefault(); + this.selectedIndex = Math.max(this.selectedIndex - 1, 0); + this.requestUpdate(); + return true; + } + if (e.key === "Enter" || e.key === "Tab") { + e.preventDefault(); + if (items[this.selectedIndex]) this._select(items[this.selectedIndex]); + return true; + } + if (e.key === "Escape") { + this.hide(); + return true; + } + return false; + } + + private _select(cmd: SlashCommand) { + this.dispatchEvent(new CustomEvent("command", { + detail: cmd, + bubbles: true, + composed: true, + })); + this.hide(); + } + override render() { - if (!this._open) return html``; - const cmds = this._cmds; + if (!this.open) return nothing; + const items = this.filtered; + if (items.length === 0) return nothing; + + // Group by category + const groups = new Map(); + for (const cmd of items) { + const list = groups.get(cmd.category) || []; + list.push(cmd); + groups.set(cmd.category, list); + } + + let globalIdx = 0; + 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 -
+
+
+ ${Array.from(groups.entries()).map(([cat, cmds]) => html` +
+ ${cat} +
+ ${cmds.map(cmd => { + const idx = globalIdx++; + const isSelected = idx === this.selectedIndex; + return html` + + `; + })} + `)}
-
`; +
+ `; } } diff --git a/packages/web-ui/example/src/main.ts b/packages/web-ui/example/src/main.ts index 622d4ea..4d3af13 100644 --- a/packages/web-ui/example/src/main.ts +++ b/packages/web-ui/example/src/main.ts @@ -267,7 +267,7 @@ async function createAgent(initialState?: AgentState) { // 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 (tc.name === "browser" || tc.name === "web_fetch" || tc.name === "navigate") { if (rightPanel !== "browser") { rightPanel = "browser"; renderApp(); @@ -487,15 +487,20 @@ function handleSuggestion(e: CustomEvent) { // ===== PERSONA CHANGE HANDLER ===== function handlePersonaChange(e: CustomEvent) { const persona = e.detail; - if (agent && persona?.prompt) { - agent.setSystemPrompt(persona.prompt); + if (agent && persona?.systemPrompt) { + agent.setSystemPrompt(persona.systemPrompt); } } // ===== VIEW TOGGLE HANDLER ===== function handleViewChange(e: CustomEvent) { const vis = e.detail as UtilityVisibility; - viewState = { ...vis }; + viewState = { + tools: vis.showToolCalls, + thinking: vis.showThinking, + system: vis.showSystemMessages, + timestamps: vis.showTimestamps, + }; renderApp(); } diff --git a/packages/web-ui/src/components/Messages.ts b/packages/web-ui/src/components/Messages.ts index c7903a3..1a1d64f 100644 --- a/packages/web-ui/src/components/Messages.ts +++ b/packages/web-ui/src/components/Messages.ts @@ -243,6 +243,8 @@ export class ToolMessage extends LitElement { override render() { const toolName = this.tool?.name || this.toolCall.name; + // Set data attribute for CSS targeting (view toggles, panel hiding) + this.setAttribute('data-tool-name', toolName); // Render tool content (renderer handles errors and styling) const result: ToolResultMessageType | undefined = this.aborted