feat: glassmorphism UI, persona fix, enhanced animations, waveform typing, context tooltips, skill cards, smooth transitions
Some checks are pending
CI / build-check-test (push) Waiting to run

This commit is contained in:
JAE 2026-03-27 06:58:31 +00:00
parent 84671d57bf
commit 33f439296f
9 changed files with 1244 additions and 495 deletions

View file

@ -161,25 +161,6 @@ button:active, [role="button"]:active {
} }
/* Hide tool outputs when toggled off */ /* 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 ===== */ /* ===== SHIMMER LOADING ===== */
.jae-shimmer { .jae-shimmer {
@ -262,38 +243,16 @@ button:active, [role="button"]:active {
background: transparent !important; background: transparent !important;
} }
/* ===== VIEW TOGGLE CONTROLS ===== */ /* ===== VIEW TOGGLE CONTROLS ===== */
/* Hide tool call blocks when tools toggled off */ /* Hide tool call blocks when tools toggled off */
.hide-tools tool-message {
display: none !important;
}
/* Hide thinking/reasoning blocks */ /* Hide thinking/reasoning blocks */
.hide-thinking [data-thinking],
.hide-thinking .thinking-block,
.hide-thinking [class*="thinking"] {
display: none !important;
}
/* Hide system messages */ /* Hide system messages */
.hide-system [data-system-message],
.hide-system .system-message {
display: none !important;
}
/* Hide timestamps on messages */ /* 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 */ /* 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 */ /* Smooth transition for tool-message visibility */
tool-message { tool-message {
@ -324,3 +283,480 @@ tool-message {
.jae-sidebar:hover::-webkit-scrollbar-thumb:hover { .jae-sidebar:hover::-webkit-scrollbar-thumb:hover {
background: rgba(255,255,255,0.25); 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;
}

View file

@ -1,65 +1,99 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
interface TokenPrice {
symbol: string;
price: number;
change24h: number;
loading: boolean;
}
@customElement("jae-crypto-ticker") @customElement("jae-crypto-ticker")
export class JaeCryptoTicker extends LitElement { export class JaeCryptoTicker extends LitElement {
@state() private vvv = { p: 0, c: 0, ok: false }; @state() private tokens: TokenPrice[] = [
@state() private diem = { p: 0, c: 0, ok: false }; { symbol: "VVV", price: 0, change24h: 0, loading: true },
private _iv: any; { symbol: "DIEM", price: 0, change24h: 0, loading: true },
];
private interval: ReturnType<typeof setInterval> | null = null;
protected override createRenderRoot() { return this; } protected override createRenderRoot() { return this; }
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this._fetch(); this.fetchPrices();
this._iv = setInterval(() => this._fetch(), 30000); this.interval = setInterval(() => this.fetchPrices(), 30000);
} }
override disconnectedCallback() { override disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
if (this._iv) clearInterval(this._iv); if (this.interval) clearInterval(this.interval);
} }
private async _fetch() { private async fetchPrices() {
try { try {
const r = await fetch("https://api.dexscreener.com/latest/dex/search?q=VVV%20venice"); // Try CoinGecko for VVV (Venice Token)
const j = await r.json(); const vvvRes = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=venice-token&vs_currencies=usd&include_24hr_change=true");
const pair = j?.pairs?.[0]; if (vvvRes.ok) {
if (pair) this.vvv = { p: parseFloat(pair.priceUsd || "0"), c: parseFloat(pair.priceChange?.h24 || "0"), ok: true }; const data = await vvvRes.json();
} catch {} const vt = data["venice-token"];
try { if (vt) {
const r = await fetch("https://api.dexscreener.com/latest/dex/search?q=DIEM%20venice"); this.tokens = this.tokens.map(t =>
const j = await r.json(); t.symbol === "VVV" ? { ...t, price: vt.usd || 0, change24h: vt.usd_24h_change || 0, loading: false } : t
const pair = j?.pairs?.[0]; );
if (pair) this.diem = { p: parseFloat(pair.priceUsd || "0"), c: parseFloat(pair.priceChange?.h24 || "0"), ok: true }; }
} catch {} }
} catch {
this.tokens = this.tokens.map(t => t.symbol === "VVV" ? { ...t, loading: false } : t);
} }
private _tk(sym: string, price: number, change: number, ok: boolean) { try {
if (!ok) return html` // Try CoinGecko for DIEM
<div class="flex items-center gap-1"> const diemRes = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=venice-diem&vs_currencies=usd&include_24hr_change=true");
<span class="font-bold text-[11px] text-foreground/60">${sym}</span> if (diemRes.ok) {
<span class="text-[10px] text-muted-foreground animate-pulse">...</span> const data = await diemRes.json();
</div>`; const dt = data["venice-diem"];
const up = change >= 0; if (dt) {
const cl = up ? "text-emerald-400" : "text-red-400"; this.tokens = this.tokens.map(t =>
const ar = up ? "▲" : "▼"; t.symbol === "DIEM" ? { ...t, price: dt.usd || 0, change24h: dt.usd_24h_change || 0, loading: false } : t
const f = price < 0.01 ? price.toFixed(6) : price < 1 ? price.toFixed(4) : price.toFixed(2); );
return html` }
<div class="flex items-center gap-1.5 transition-all duration-500"> }
<span class="font-bold text-[11px] text-foreground/70">${sym}</span> } catch {
<span class="text-[11px] font-mono text-foreground/90">$${f}</span> this.tokens = this.tokens.map(t => t.symbol === "DIEM" ? { ...t, loading: false } : t);
<span class="${cl} text-[9px] font-medium">${ar}${Math.abs(change).toFixed(1)}%</span> }
</div>`;
this.requestUpdate();
}
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() { override render() {
return html` return html`
<div class="jae-glass-sm flex items-center gap-3 px-3 py-1.5 rounded-xl cursor-default select-none" title="Venice tokens - updates every 30s"> <div class="px-2 py-1.5 flex items-center gap-2 text-[10px]" style="font-variant-numeric:tabular-nums">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-amber-400 shrink-0"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg> ${this.tokens.map(t => html`
${this._tk("VVV", this.vvv.p, this.vvv.c, this.vvv.ok)} <div class="flex items-center gap-1.5 px-2 py-1 rounded-lg transition-all duration-300"
<div class="w-px h-3 bg-border/40"></div> style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06)">
${this._tk("DIEM", this.diem.p, this.diem.c, this.diem.ok)} <span class="font-bold" style="color:var(--color-card-foreground, hsl(0 0% 90%))">${t.symbol}</span>
</div>`; ${t.loading ? html`
<span class="jae-ticker-pulse" style="color:var(--color-muted-foreground, hsl(0 0% 55%))">...</span>
` : html`
<span style="color:var(--color-card-foreground, hsl(0 0% 90%))">${this.formatPrice(t.price)}</span>
<span class="font-medium" style="color:${t.change24h >= 0 ? "rgb(34,197,94)" : "rgb(239,68,68)"}">${this.formatChange(t.change24h)}</span>
`}
</div>
`)}
</div>
`;
} }
} }

View file

@ -1,104 +1,146 @@
import { html, LitElement } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
export interface Skill { export interface Skill {
id: string; id: string;
name: string; name: string;
desc: string; description: string;
icon: string; emoji: string;
cat: string; category: string;
enabled: boolean; enabled: boolean;
builtin: boolean;
} }
const DEFAULTS: Skill[] = [ const DEFAULT_SKILLS: Skill[] = [
{ id: "web_search", name: "Web Search", desc: "Search the internet", icon: "\u{1F50D}", cat: "Research", enabled: true }, { id: "bash", name: "Shell Commands", description: "Execute bash/terminal commands", emoji: "💻", category: "Core", enabled: true, builtin: 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: "Web Browser", description: "Browse and interact with websites", emoji: "🌐", category: "Core", enabled: true, builtin: true },
{ id: "browser", name: "Browser", desc: "Navigate and interact with websites", icon: "\u{1F310}", cat: "Research", enabled: true }, { id: "web_search", name: "Web Search", description: "Search the internet for information", emoji: "🔍", category: "Core", enabled: true, builtin: true },
{ id: "memory", name: "Memory", desc: "Remember and recall information", icon: "\u{1F9E0}", cat: "Core", enabled: true }, { id: "javascript_repl", name: "JavaScript REPL", description: "Run JS code and create artifacts", emoji: "⚡", category: "Core", enabled: true, builtin: true },
{ id: "tts", name: "Text to Speech", desc: "Convert text to spoken audio", icon: "\u{1F50A}", cat: "Creative", enabled: true }, { id: "image_gen", name: "Image Generation", description: "Generate images with Venice AI", emoji: "🎨", category: "Creative", enabled: true, builtin: false },
{ id: "bash", name: "Terminal", desc: "Execute shell commands", icon: "\u{1F4BB}", cat: "Developer", enabled: true }, { id: "tts", name: "Text to Speech", description: "Convert text to spoken audio", emoji: "🔊", category: "Creative", enabled: true, builtin: false },
{ id: "repl", name: "Code REPL", desc: "Run JavaScript in sandbox", icon: "\u26A1", cat: "Developer", enabled: true }, { id: "memory", name: "Memory", description: "Save and recall information", emoji: "🧠", category: "Intelligence", enabled: true, builtin: false },
{ id: "artifacts", name: "Artifacts", desc: "Create HTML, SVG, documents", icon: "\u{1F4C4}", cat: "Creative", enabled: true }, { id: "code_analysis", name: "Code Analysis", description: "Analyse and review code quality", emoji: "🔬", category: "Developer", enabled: true, builtin: false },
{ id: "crypto", name: "Crypto Tools", desc: "Token prices, charts & analysis", icon: "\u{1F4C8}", cat: "Finance", enabled: 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", desc: "Generate README & API docs", icon: "\u{1F4DD}", cat: "Developer", enabled: false }, { id: "doc_gen", name: "Doc Generator", description: "Generate README, API docs from code", emoji: "📝", category: "Developer", enabled: false, builtin: false },
{ id: "sub_agent", name: "Sub-Agents", desc: "Spawn specialist AI agents", icon: "\u{1F916}", cat: "Advanced", enabled: false }, { id: "crypto_tracker", name: "Crypto Tracker", description: "Real-time token prices and charts", emoji: "💰", category: "Finance", enabled: true, builtin: false },
{ id: "knowledge", name: "Knowledge Graph", desc: "Build queryable knowledge bases", icon: "\u{1F578}\u{FE0F}", cat: "Advanced", enabled: false }, { id: "chart_analysis", name: "Chart Analysis", description: "Technical analysis with indicators", emoji: "📈", category: "Finance", enabled: false, builtin: false },
{ id: "mcp", name: "MCP Servers", desc: "Connect to external APIs", icon: "\u{1F50C}", cat: "Advanced", enabled: 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") @customElement("jae-marketplace")
export class JaeMarketplace extends LitElement { export class JaeMarketplace extends LitElement {
@state() private _open = false; @state() private open = false;
@state() private _skills: Skill[] = []; @state() private skills: Skill[] = [];
@state() private _cat = "All"; @state() private filterCat = "All";
protected override createRenderRoot() { return this; } protected override createRenderRoot() { return this; }
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
const raw = localStorage.getItem("jae-skills"); const saved = localStorage.getItem("jae-marketplace-skills");
if (raw) { if (saved) {
try { try { this.skills = JSON.parse(saved); } catch { this.skills = [...DEFAULT_SKILLS]; }
const map = JSON.parse(raw) as Record<string, boolean>;
this._skills = DEFAULTS.map(s => ({ ...s, enabled: map[s.id] ?? s.enabled }));
} catch { this._skills = DEFAULTS.map(s => ({ ...s })); }
} else { } 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(); }
toggle() { this._open = !this._open; this.requestUpdate(); } show() { this.open = true; this.requestUpdate(); }
show() { this._open = true; this.requestUpdate(); } hide() { this.open = false; 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) { private _toggle(id: string) {
this._skills = this._skills.map(s => s.id === id ? { ...s, enabled: !s.enabled } : s); this.skills = this.skills.map(s =>
const map: Record<string, boolean> = {}; s.id === id && !s.builtin ? { ...s, enabled: !s.enabled } : s
for (const s of this._skills) map[s.id] = s.enabled; );
localStorage.setItem("jae-skills", JSON.stringify(map)); localStorage.setItem("jae-marketplace-skills", JSON.stringify(this.skills));
this.dispatchEvent(new CustomEvent("skills-change", { detail: this._skills.filter(s => s.enabled), bubbles: true, composed: true })); 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 categories(): string[] {
private get _filtered() { return this._cat === "All" ? this._skills : this._skills.filter(s => s.cat === this._cat); } 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() { override render() {
if (!this._open) return html``;
const active = this._skills.filter(s => s.enabled).length;
return html` return html`
<div class="fixed inset-0 z-50 flex" @click=${() => this.hide()}> <button @click=${() => this.toggle()}
<div class="w-80 h-full jae-glass border-r border-border/60 shadow-2xl flex flex-col jae-slide-in" @click=${(e: Event) => e.stopPropagation()}> class="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-[11px] font-medium transition-all duration-300"
<div class="flex items-center justify-between px-4 py-3 border-b border-border/40 shrink-0"> style="background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);color:var(--color-card-foreground, hsl(0 0% 90%))"
@mouseenter=${(e: Event) => (e.currentTarget as HTMLElement).style.background = "rgba(255,255,255,0.08)"}
@mouseleave=${(e: Event) => (e.currentTarget as HTMLElement).style.background = "rgba(255,255,255,0.04)"}>
<span>🛒</span> Marketplace
</button>
${this.open ? html`
<div class="fixed inset-0 z-50" @click=${(e: Event) => { if (e.target === e.currentTarget) this.hide(); }}
style="background:rgba(0,0,0,0.5);backdrop-filter:blur(4px)">
<div class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[520px] max-h-[70vh] rounded-2xl border shadow-2xl jae-scale-in overflow-hidden flex flex-col"
style="background:var(--color-card, hsl(240 6% 10%));border-color:rgba(255,255,255,0.1);color:var(--color-card-foreground, hsl(0 0% 95%))">
<div class="px-5 py-4 border-b flex items-center justify-between shrink-0" style="border-color:rgba(255,255,255,0.08)">
<div> <div>
<h2 class="font-bold text-foreground text-sm">\u{1F3EA} Marketplace</h2> <h2 class="text-base font-bold">🛒 Skills Marketplace</h2>
<span class="text-[11px] text-muted-foreground">${active}/${this._skills.length} skills active</span> <p class="text-[11px] mt-0.5" style="color:var(--color-muted-foreground, hsl(0 0% 60%))">Toggle capabilities on or off</p>
</div> </div>
<button @click=${() => this.hide()} class="text-muted-foreground hover:text-foreground transition-colors text-lg">\u2715</button> <button @click=${() => this.hide()} class="w-7 h-7 flex items-center justify-center rounded-lg hover:bg-secondary/60 transition-colors" style="color:var(--color-muted-foreground, hsl(0 0% 55%))"></button>
</div> </div>
<div class="flex gap-1.5 px-3 py-2 overflow-x-auto border-b border-border/30 shrink-0"> <div class="flex gap-1 px-4 py-2 overflow-x-auto shrink-0" style="border-bottom:1px solid rgba(255,255,255,0.05)">
${this._cats.map(c => html` ${this.categories.map(cat => html`
<button @click=${() => { this._cat = c; this.requestUpdate(); }} <button @click=${() => { this.filterCat = cat; this.requestUpdate(); }}
class="px-2.5 py-1 rounded-full text-[11px] whitespace-nowrap transition-all duration-200 ${this._cat === c ? "bg-primary text-primary-foreground" : "bg-secondary/40 text-muted-foreground hover:bg-secondary"}">${c}</button> class="px-2.5 py-1 rounded-full text-[10px] font-medium whitespace-nowrap transition-all duration-200"
style="background:${cat === this.filterCat ? "rgba(255,255,255,0.1)" : "transparent"};color:${cat === this.filterCat ? "var(--color-card-foreground, hsl(0 0% 95%))" : "var(--color-muted-foreground, hsl(0 0% 55%))"};border:1px solid ${cat === this.filterCat ? "rgba(255,255,255,0.15)" : "transparent"}">
${cat}
</button>
`)} `)}
</div> </div>
<div class="flex-1 overflow-y-auto p-3 space-y-2"> <div class="flex-1 overflow-y-auto p-4 grid grid-cols-2 gap-2">
${this._filtered.map(s => html` ${this.filtered.map(skill => html`
<div class="jae-glass-sm rounded-xl p-3 flex items-start gap-3 transition-all duration-200 hover:border-primary/30 ${s.enabled ? "border-l-2 border-primary/50" : "opacity-50"}"> <div class="flex items-start gap-3 p-3 rounded-xl transition-all duration-200"
<span class="text-2xl shrink-0 mt-0.5">${s.icon}</span> style="background:${skill.enabled ? "rgba(255,255,255,0.05)" : "transparent"};border:1px solid ${skill.enabled ? "rgba(255,255,255,0.1)" : "rgba(255,255,255,0.04)"}">
<span class="text-xl mt-0.5">${skill.emoji}</span>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="font-medium text-sm text-foreground">${s.name}</div> <div class="flex items-center gap-2">
<div class="text-[11px] text-muted-foreground mt-0.5">${s.desc}</div> <span class="text-xs font-semibold">${skill.name}</span>
<span class="inline-block mt-1.5 text-[9px] px-1.5 py-0.5 rounded-full bg-secondary/40 text-muted-foreground">${s.cat}</span> ${skill.builtin ? html`<span class="text-[8px] px-1 py-0.5 rounded" style="background:rgba(96,165,250,0.15);color:rgb(96,165,250)">CORE</span>` : nothing}
</div> </div>
<button @click=${() => this._toggle(s.id)} <p class="text-[10px] mt-0.5" style="color:var(--color-muted-foreground, hsl(0 0% 55%))">${skill.description}</p>
class="shrink-0 w-10 h-[22px] rounded-full transition-all duration-300 relative ${s.enabled ? "bg-primary" : "bg-muted"}"> </div>
<div class="absolute top-[3px] w-4 h-4 rounded-full bg-white shadow-sm transition-all duration-300 ${s.enabled ? "left-[21px]" : "left-[3px]"}"></div> <button @click=${() => this._toggle(skill.id)}
class="shrink-0 w-9 h-5 rounded-full transition-all duration-300 relative mt-1"
style="background:${skill.enabled ? "rgb(34,197,94)" : "rgba(255,255,255,0.1)"};cursor:${skill.builtin ? "not-allowed" : "pointer"};opacity:${skill.builtin ? "0.5" : "1"}">
<div class="absolute top-0.5 w-4 h-4 rounded-full transition-all duration-300"
style="background:white;left:${skill.enabled ? "18px" : "2px"}"></div>
</button> </button>
</div> </div>
`)} `)}
</div> </div>
<div class="px-4 py-3 border-t shrink-0 flex items-center justify-between" style="border-color:rgba(255,255,255,0.08)">
<span class="text-[10px]" style="color:var(--color-muted-foreground, hsl(0 0% 55%))">${this.skills.filter(s => s.enabled).length} of ${this.skills.length} skills active</span>
<span class="text-[10px]" style="color:var(--color-muted-foreground, hsl(0 0% 45%))">Core skills cannot be disabled</span>
</div> </div>
</div>`; </div>
</div>
` : nothing}
`;
} }
} }

View file

@ -1,170 +1,224 @@
import { html, LitElement } from "lit"; import { html, LitElement, css } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, state, property } from "lit/decorators.js";
export interface MemoryEntry { export interface Memory {
id: string; id: string;
content: string; text: string;
tags: string[]; category: string;
timestamp: string; timestamp: number;
source: "auto" | "manual";
importance: number; // 1-5
} }
const DB_NAME = "jae-memory"; const CATEGORIES = ["preference", "fact", "project", "person", "skill", "context", "custom"] as const;
const DB_VERSION = 1;
const STORE_NAME = "memories";
let _db: IDBDatabase | null = null;
async function openDB(): Promise<IDBDatabase> {
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<MemoryEntry[]> {
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<string> {
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<void> {
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);
});
}
@customElement("memory-manager") @customElement("memory-manager")
export class MemoryManager extends LitElement { export class MemoryManager extends LitElement {
@state() private open = false; @state() private _memories: Memory[] = [];
@state() private entries: MemoryEntry[] = []; @state() private _filter = "";
@state() private loading = false; @state() private _catFilter = "all";
@state() private newContent = ""; @state() private _editing: string | null = null;
@state() private newTags = ""; @state() private _visible = false;
@state() private filter = ""; @property({ type: Boolean }) open = false;
protected override createRenderRoot() { protected override createRenderRoot() { return this; }
return this;
override connectedCallback() {
super.connectedCallback();
this._load();
} }
async show() { private _load() {
this.open = true; try {
this.loading = true; const raw = localStorage.getItem("jae-memories");
this.requestUpdate(); if (raw) this._memories = JSON.parse(raw);
this.entries = await memoryLoad(); } catch {}
this.loading = false;
this.requestUpdate();
}
hide() {
this.open = false;
this.requestUpdate();
} }
get filtered() { private _save() {
if (!this.filter) return this.entries; localStorage.setItem("jae-memories", JSON.stringify(this._memories));
const q = this.filter.toLowerCase(); }
return this.entries.filter(
(e) => e.content.toLowerCase().includes(q) || e.tags.some((t) => t.toLowerCase().includes(q)), // === 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();
} }
async deleteEntry(id: string) { private _extractFacts(user: string, assistant: string): Memory[] {
await memoryDelete(id); const facts: Memory[] = [];
this.entries = this.entries.filter((e) => e.id !== id); 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(); this.requestUpdate();
} }
async addEntry() { private _delete(id: string) {
if (!this.newContent.trim()) return; this._memories = this._memories.filter(m => m.id !== id);
const tags = this.newTags this._save();
.split(",")
.map((t) => t.trim())
.filter(Boolean);
await memorySave(this.newContent.trim(), tags);
this.newContent = "";
this.newTags = "";
this.entries = await memoryLoad();
this.requestUpdate(); 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() { override render() {
if (!this.open) return html``; if (!this._visible) return html``;
const entries = this.filtered; 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` return html`
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click=${(e: Event) => { <div class="glass-panel fixed inset-0 z-[200] flex items-center justify-center bg-black/50 backdrop-blur-sm animate-fade-in" @click=${(e: Event) => { if (e.target === e.currentTarget) this.hide(); }}>
if (e.target === e.currentTarget) this.hide(); <div class="glass-card w-[600px] max-h-[70vh] flex flex-col rounded-2xl overflow-hidden shadow-2xl" @click=${(e: Event) => e.stopPropagation()}>
}}> <div class="flex items-center justify-between p-4 border-b border-white/10">
<div class="bg-popover text-foreground border border-border rounded-xl shadow-2xl w-full max-w-2xl mx-4 flex flex-col max-h-[80vh]"> <div class="flex items-center gap-2">
<div class="flex items-center justify-between px-6 py-4 border-b border-border shrink-0"> <span class="text-lg">\u{1F9E0}</span>
<h2 class="font-semibold text-lg">&#x1F9E0; Memory Manager</h2> <span class="text-sm font-semibold text-foreground">Memory Bank</span>
<button class="text-muted-foreground hover:text-foreground" @click=${() => this.hide()}>&#x2715;</button> <span class="text-[10px] px-1.5 py-0.5 rounded-full bg-primary/20 text-primary">${this._memories.length} memories</span>
</div> </div>
<div class="px-6 py-3 border-b border-border shrink-0"> <button @click=${() => this.hide()} class="w-7 h-7 flex items-center justify-center rounded-lg hover:bg-white/10 text-muted-foreground transition-all">\u2715</button>
<input type="text" placeholder="Filter memories..." class="w-full bg-secondary text-foreground rounded-lg px-3 py-2 text-sm outline-none" </div>
.value=${this.filter} @input=${(e: Event) => { <div class="flex items-center gap-2 p-3 border-b border-white/5">
this.filter = (e.target as HTMLInputElement).value; <input type="text" placeholder="Search memories..." .value=${this._filter}
}} /> @input=${(e: InputEvent) => { 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" />
<select @change=${(e: Event) => { this._catFilter = (e.target as HTMLSelectElement).value; this.requestUpdate(); }}
class="px-2 py-1.5 text-xs bg-white/5 border border-white/10 rounded-lg text-foreground">
<option value="all">All</option>
${CATEGORIES.map(c => html`<option value=${c}>${c}</option>`)}
</select>
</div>
<div class="flex-1 overflow-y-auto p-3 space-y-2">
${filtered.length === 0 ? html`<div class="text-center text-xs text-muted-foreground/50 py-8">No memories yet. Chat with JAE to build your memory bank!</div>` : ""}
${filtered.map(m => html`
<div class="group flex items-start gap-3 p-3 rounded-xl bg-white/[0.03] hover:bg-white/[0.06] border border-white/5 transition-all duration-200">
<div class="flex flex-col items-center gap-1 shrink-0">
<span class="text-[10px] px-1.5 py-0.5 rounded bg-primary/15 text-primary/80">${m.category}</span>
<span class="text-[9px] text-muted-foreground/40">${m.source === "auto" ? "\u{1F916}" : "\u{270D}\uFE0F"}</span>
</div> </div>
<div class="flex-1 overflow-y-auto px-6 py-4">
${this.loading ? html`<div class="text-center text-muted-foreground py-8">Loading...</div>` : ""}
${!this.loading && entries.length === 0 ? html`<div class="text-center text-muted-foreground py-8">No memories stored yet</div>` : ""}
<div class="flex flex-col gap-2">
${entries.map(
(entry) => html`
<div class="flex gap-3 p-3 rounded-lg border border-border bg-background group">
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="text-sm">${entry.content}</div> <div class="text-xs text-foreground/90">${m.text}</div>
<div class="flex items-center gap-2 mt-1"> <div class="text-[9px] text-muted-foreground/40 mt-1">${new Date(m.timestamp).toLocaleDateString()}</div>
<span class="text-xs text-muted-foreground">${entry.timestamp.slice(0, 10)}</span> </div>
${entry.tags.map((tag) => html`<span class="text-xs bg-secondary px-1.5 py-0.5 rounded">${tag}</span>`)} <div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
<select .value=${String(m.importance)} @change=${(e: Event) => this._setImportance(m.id, Number((e.target as HTMLSelectElement).value))}
class="text-[10px] bg-transparent border border-white/10 rounded px-1 py-0.5 text-foreground" title="Importance">
${[1,2,3,4,5].map(n => html`<option value=${n} ?selected=${m.importance === n}>${"\u2B50".repeat(n)}</option>`)}
</select>
<button @click=${() => this._delete(m.id)} class="w-5 h-5 flex items-center justify-center rounded hover:bg-red-500/20 text-red-400 text-[10px] transition-colors">\u2715</button>
</div> </div>
</div> </div>
<button class="shrink-0 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity" `)}
@click=${() => this.deleteEntry(entry.id)} title="Delete">&#x1F5D1;</button>
</div> </div>
`, <div class="p-3 border-t border-white/10">
)}
</div>
</div>
<div class="px-6 py-4 border-t border-border shrink-0">
<div class="flex flex-col gap-2">
<textarea placeholder="New memory content..." class="w-full bg-secondary rounded-lg px-3 py-2 text-sm outline-none resize-none"
rows="2" .value=${this.newContent}
@input=${(e: Event) => {
this.newContent = (e.target as HTMLTextAreaElement).value;
}}></textarea>
<div class="flex gap-2"> <div class="flex gap-2">
<input type="text" placeholder="Tags (comma separated)" class="flex-1 bg-secondary rounded-lg px-3 py-2 text-sm outline-none" <input id="jae-mem-input" type="text" placeholder="Add a memory manually..."
.value=${this.newTags} @input=${(e: Event) => { 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"
this.newTags = (e.target as HTMLInputElement).value; @keydown=${(e: KeyboardEvent) => { if (e.key === "Enter") { const inp = e.target as HTMLInputElement; if (inp.value.trim()) { this.addManual(inp.value.trim()); inp.value = ""; } } }} />
}} /> <button @click=${() => { const inp = this.querySelector("#jae-mem-input") as HTMLInputElement; if (inp?.value.trim()) { this.addManual(inp.value.trim()); inp.value = ""; } }}
<button class="bg-primary text-primary-foreground px-4 py-2 rounded-lg text-sm font-medium hover:opacity-90" class="px-3 py-1.5 text-xs bg-primary/20 text-primary rounded-lg hover:bg-primary/30 transition-colors">Add</button>
@click=${() => this.addEntry()}>Save</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>`;
</div>
`;
} }
} }

View file

@ -3,96 +3,120 @@ import { customElement, state } from "lit/decorators.js";
export type Mood = "neutral" | "focused" | "excited" | "warning" | "frustrated" | "angry"; export type Mood = "neutral" | "focused" | "excited" | "warning" | "frustrated" | "angry";
interface MoodDef { const MOOD_CONFIG: Record<Mood, { emoji: string; label: string; color: string; glow: string }> = {
id: Mood; neutral: { emoji: "\ud83d\udc09", label: "Calm", color: "rgba(148,163,184,0.8)", glow: "none" },
icon: string; focused: { emoji: "\ud83e\udde0", label: "Focused", color: "rgba(96,165,250,0.9)", glow: "0 0 12px rgba(96,165,250,0.3)" },
color: string; excited: { emoji: "\u2728", label: "Excited", color: "rgba(250,204,21,0.9)", glow: "0 0 12px rgba(250,204,21,0.3)" },
glow: string; warning: { emoji: "\u26a0\ufe0f", label: "Cautious", color: "rgba(251,146,60,0.9)", glow: "0 0 12px rgba(251,146,60,0.3)" },
label: string; 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)" },
};
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") @customElement("jae-mood-indicator")
export class JaeMoodIndicator extends LitElement { export class JaeMoodIndicator extends LitElement {
@state() private _mood: Mood = "neutral"; @state() private mood: Mood = "neutral";
@state() private _shaking = false; @state() private shaking = false;
@state() private _cracked = false; @state() private cracked = false;
@state() private _crackOpacity = 0; @state() private crackOpacity = 1.0;
private _crackTimer: any;
private fadeTimer: ReturnType<typeof setInterval> | null = null;
protected override createRenderRoot() { return this; } protected override createRenderRoot() { return this; }
get mood() { return this._mood; }
get isCracked() { return this._cracked; }
get crackOpacity() { return this._crackOpacity; }
setMood(m: Mood) { setMood(m: Mood) {
const prev = this._mood; this.mood = m;
this._mood = m;
if (m === "angry") { if (m === "angry") {
this._shaking = true; this.triggerAnger();
setTimeout(() => { this._shaking = false; this.requestUpdate(); }, 800); }
this._cracked = true; if (m === "neutral" || m === "focused") {
this._crackOpacity = 1; // User calmed JAE down
this.requestUpdate(); this.shaking = false;
this.dispatchEvent(new CustomEvent("mood-angry", { bubbles: true, composed: true })); }
if (this._crackTimer) clearInterval(this._crackTimer); this.requestUpdate();
this._crackTimer = setInterval(() => { }
this._crackOpacity = Math.max(0, this._crackOpacity - 0.0033);
if (this._crackOpacity <= 0) { getMood(): Mood { return this.mood; }
this._cracked = false;
clearInterval(this._crackTimer); /** 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;
}
}
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.dispatchEvent(new CustomEvent("crack-update", { detail: { opacity: this._crackOpacity, active: this._cracked }, bubbles: true, composed: true }));
this.requestUpdate(); this.requestUpdate();
}, 1000); }, 1000);
} }
if (prev === "angry" && m !== "angry") { override disconnectedCallback() {
this._crackOpacity = Math.min(this._crackOpacity, 0.3); super.disconnectedCallback();
this.requestUpdate(); if (this.fadeTimer) clearInterval(this.fadeTimer);
} }
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() { override render() {
const d = this._def(); const cfg = MOOD_CONFIG[this.mood];
return html` return html`
<div class="flex items-center gap-2 px-2 py-1 rounded-lg transition-all duration-500 ${this._shaking ? "jae-shake" : ""}"> <!-- Mood badge -->
<div class="relative w-7 h-7 transition-all duration-500" style="filter: ${d.glow}"> <div class="flex items-center gap-1.5 px-2 py-1 rounded-full text-[10px] transition-all duration-500"
<img src="${d.icon}" alt="JAE" class="w-full h-full object-contain rounded-full transition-all duration-500" style="background:rgba(255,255,255,0.04);border:1px solid ${cfg.color};box-shadow:${cfg.glow}">
style="${this._mood === "angry" ? "animation: jae-pulse-angry 0.3s infinite;" : ""}" /> <span class="text-sm jae-mood-emoji ${this.mood === "angry" ? "jae-angry-pulse" : ""}">${cfg.emoji}</span>
<span class="font-medium" style="color:${cfg.color}">${cfg.label}</span>
</div> </div>
<div class="flex flex-col">
<span class="text-[10px] font-medium ${d.color} transition-colors duration-500">${d.label}</span> <!-- Crack overlay on chat -->
${this._mood === "angry" ? html`<span class="text-[8px] text-red-500 animate-pulse">click to calm</span>` : ""} ${this.cracked ? html`
</div> <div class="jae-crack-overlay" style="opacity:${this.crackOpacity};pointer-events:none"></div>
</div>`; ` : html``}
<!-- Shake class injector -->
${this.shaking ? html`<style>.jae-chat-area { animation: jae-shake 0.1s infinite alternate !important; }</style>` : html``}
`;
} }
} }

View file

@ -4,64 +4,136 @@ import { customElement, state } from "lit/decorators.js";
export interface Persona { export interface Persona {
id: string; id: string;
name: string; name: string;
icon: string; emoji: string;
desc: string; label: string;
prompt: string; description: string;
systemPrompt: string;
style: { tone: string; verbosity: string };
} }
export const PERSONAS: Persona[] = [ 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: "default",
{ 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." }, name: "JAE",
{ 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." }, emoji: "\u{1F409}",
{ 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." }, label: "Default",
{ 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." }, description: "Balanced coding assistant",
{ 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." }, 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") @customElement("persona-selector")
export class JaePersonaSelector extends LitElement { export class PersonaSelector extends LitElement {
@state() private _open = false; @state() private _open = false;
@state() private _current: Persona = PERSONAS[0]; @state() private _activeId = "default";
protected override createRenderRoot() { return this; } 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) { setPersona(id: string) {
this._current = p; this._activeId = id;
this._open = false; 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() { override render() {
const current = this.active;
return html` return html`
<div class="relative"> <div class="relative">
<button @click=${() => { this._open = !this._open; this.requestUpdate(); }} <button @click=${() => { this._open = !this._open; this.requestUpdate(); }}
class="jae-glass-sm flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs hover:bg-secondary/80 transition-all duration-200" class="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs bg-white/5 border border-white/10 hover:bg-white/10 hover:border-white/20 text-foreground transition-all duration-200"
title="Switch persona"> title="Switch persona">
<span class="text-sm">${this._current.icon}</span> <span>${current.emoji}</span>
<span class="text-foreground/80 font-medium hidden sm:inline">${this._current.name}</span> <span class="text-muted-foreground">${current.label}</span>
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-muted-foreground"><path d="m6 9 6 6 6-6"/></svg> <span class="text-[10px] text-muted-foreground/50">${this._open ? "\u25B2" : "\u25BC"}</span>
</button> </button>
${this._open ? html` ${this._open ? html`
<div class="absolute bottom-full mb-2 left-0 w-72 rounded-2xl jae-glass border border-border/60 shadow-2xl z-50 overflow-hidden jae-scale-in"> <div class="absolute bottom-full left-0 mb-2 w-72 glass-card rounded-xl border border-white/10 shadow-2xl overflow-hidden z-[100] animate-scale-in">
<div class="px-3 py-2 border-b border-border/40 text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">Persona</div> <div class="p-2.5 border-b border-white/10">
<div class="max-h-80 overflow-y-auto py-1"> <div class="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">Personas</div>
${PERSONAS.map(p => html`
<button @click=${() => this._select(p)}
class="w-full flex items-center gap-3 px-3 py-2.5 text-left transition-all duration-200 hover:bg-secondary/60 ${p.id === this._current.id ? "bg-primary/10 border-l-2 border-primary" : ""}">
<span class="text-lg shrink-0">${p.icon}</span>
<div class="min-w-0 flex-1">
<div class="text-sm font-medium text-foreground">${p.name}</div>
<div class="text-[11px] text-muted-foreground truncate">${p.desc}</div>
</div> </div>
${p.id === this._current.id ? html`<span class="text-primary text-xs font-bold">\u2713</span>` : ""} <div class="max-h-[320px] overflow-y-auto p-1.5 space-y-0.5">
${PERSONAS.map(p => html`
<button @click=${() => this.setPersona(p.id)}
class="w-full flex items-start gap-2.5 p-2.5 rounded-lg text-left transition-all duration-200
${p.id === this._activeId ? "bg-primary/15 border border-primary/30" : "hover:bg-white/5 border border-transparent"}">
<span class="text-lg shrink-0 mt-0.5">${p.emoji}</span>
<div class="flex-1 min-w-0">
<div class="text-xs font-medium text-foreground">${p.label}</div>
<div class="text-[10px] text-muted-foreground/70 mt-0.5">${p.description}</div>
<div class="flex gap-1 mt-1">
<span class="text-[9px] px-1 py-0.5 rounded bg-white/5 text-muted-foreground/50">${p.style.tone}</span>
<span class="text-[9px] px-1 py-0.5 rounded bg-white/5 text-muted-foreground/50">${p.style.verbosity}</span>
</div>
</div>
${p.id === this._activeId ? html`<span class="text-primary text-xs mt-1">\u2713</span>` : ""}
</button> </button>
`)} `)}
</div> </div>
</div> </div>` : ""}
` : ""}
</div>`; </div>`;
} }
} }

View file

@ -1,95 +1,175 @@
import { html, LitElement } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
interface SlashCmd { export interface SlashCommand {
cmd: string; command: string;
desc: string; label: string;
action: string; description: string;
icon: string; category: string;
emoji: string;
} }
const COMMANDS: SlashCmd[] = [ const ALL_COMMANDS: SlashCommand[] = [
{ cmd: "/search", desc: "Search the web", action: "search", icon: "\u{1F50D}" }, // Tools
{ cmd: "/image", desc: "Generate an image", action: "image", icon: "\u{1F5BC}\u{FE0F}" }, { command: "/search", label: "Web Search", description: "Search the web for information", category: "Tools", emoji: "\ud83d\udd0d" },
{ cmd: "/browse", desc: "Open browser panel", action: "browser", icon: "\u{1F310}" }, { command: "/image", label: "Generate Image", description: "Create an image with AI", category: "Tools", emoji: "\ud83c\udfa8" },
{ cmd: "/terminal", desc: "Open terminal panel", action: "terminal", icon: "\u{1F4BB}" }, { command: "/browse", label: "Browse URL", description: "Open and read a webpage", category: "Tools", emoji: "\ud83c\udf10" },
{ cmd: "/memory", desc: "Memory manager", action: "memory", icon: "\u{1F9E0}" }, { command: "/bash", label: "Run Command", description: "Execute a shell command", category: "Tools", emoji: "\ud83d\udcbb" },
{ cmd: "/model", desc: "Switch model", action: "model", icon: "\u{1F916}" }, { command: "/code", label: "Write Code", description: "Generate or edit code", category: "Tools", emoji: "\ud83d\udcc4" },
{ cmd: "/persona", desc: "Change persona", action: "persona", icon: "\u{1F3AD}" }, { command: "/repl", label: "JavaScript REPL", description: "Run JavaScript in sandbox", category: "Tools", emoji: "\u26a1" },
{ cmd: "/export", desc: "Export session", action: "export", icon: "\u{1F4E5}" }, // Memory
{ cmd: "/clear", desc: "Clear conversation", action: "clear", icon: "\u{1F5D1}\u{FE0F}" }, { command: "/memory", label: "Memory Manager", description: "View and manage memories", category: "Memory", emoji: "\ud83e\udde0" },
{ cmd: "/price", desc: "Token price lookup", action: "price", icon: "\u{1F4B0}" }, { command: "/remember", label: "Save Memory", description: "Save information for later", category: "Memory", emoji: "\ud83d\udcbe" },
{ cmd: "/marketplace", desc: "Skill marketplace", action: "marketplace", icon: "\u{1F3EA}" }, { command: "/recall", label: "Recall Memory", description: "Search saved memories", category: "Memory", emoji: "\ud83d\udd0e" },
{ cmd: "/docs", desc: "Generate documentation", action: "docs", icon: "\u{1F4DD}" }, { command: "/forget", label: "Forget Memory", description: "Remove a saved memory", category: "Memory", emoji: "\ud83d\uddd1\ufe0f" },
{ cmd: "/settings", desc: "Open settings", action: "settings", icon: "\u2699\u{FE0F}" }, // Personas
{ cmd: "/shortcuts", desc: "Keyboard shortcuts", action: "shortcuts", icon: "\u2328\u{FE0F}" }, { command: "/persona", label: "Switch Persona", description: "Change JAE's personality", category: "Personas", emoji: "\ud83c\udfad" },
{ cmd: "/help", desc: "Show all commands", action: "help", icon: "\u2753" }, { 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") @customElement("jae-slash-commands")
export class JaeSlashCommands extends LitElement { export class JaeSlashCommands extends LitElement {
@state() private _open = false; @state() private open = false;
@state() private _filter = ""; @state() private filter = "";
@state() private _sel = 0; @state() private selectedIndex = 0;
protected override createRenderRoot() { return this; } protected override createRenderRoot() { return this; }
show(initialFilter = "") { this._open = true; this._filter = initialFilter; this._sel = 0; this.requestUpdate(); show(initialFilter = "") {
requestAnimationFrame(() => (this.querySelector("input") as HTMLInputElement)?.focus()); } this.filter = initialFilter;
hide() { this._open = false; this.requestUpdate(); } this.selectedIndex = 0;
toggle() { this._open ? this.hide() : this.show(); } this.open = true;
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(); 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() { override render() {
if (!this._open) return html``; if (!this.open) return nothing;
const cmds = this._cmds; const items = this.filtered;
if (items.length === 0) return nothing;
// Group by category
const groups = new Map<string, SlashCommand[]>();
for (const cmd of items) {
const list = groups.get(cmd.category) || [];
list.push(cmd);
groups.set(cmd.category, list);
}
let globalIdx = 0;
return html` return html`
<div class="fixed inset-0 z-50" @click=${() => this.hide()}> <div class="absolute bottom-full left-0 right-0 mb-1 mx-4 z-50">
<div class="absolute bottom-24 left-1/2 -translate-x-1/2 w-[420px] max-w-[90vw] jae-glass rounded-2xl border border-border/60 shadow-2xl overflow-hidden jae-scale-in" <div class="rounded-xl border shadow-2xl overflow-hidden jae-scale-in"
@click=${(e: Event) => e.stopPropagation()}> style="background:var(--color-card, hsl(240 6% 10%));border-color:rgba(255,255,255,0.1);color:var(--color-card-foreground, hsl(0 0% 95%));max-height:340px;overflow-y:auto">
<div class="flex items-center gap-2 px-4 py-3 border-b border-border/40"> ${Array.from(groups.entries()).map(([cat, cmds]) => html`
<span class="text-muted-foreground text-lg font-mono">/</span> <div class="px-3 pt-2 pb-1">
<input type="text" class="flex-1 bg-transparent outline-none text-sm text-foreground placeholder:text-muted-foreground" <span class="text-[10px] font-semibold uppercase tracking-widest" style="color:var(--color-muted-foreground, hsl(0 0% 55%))">${cat}</span>
placeholder="Type a command..." .value=${this._filter}
@input=${(e: Event) => { this._filter = (e.target as HTMLInputElement).value; this._sel = 0; this.requestUpdate(); }}
@keydown=${(e: KeyboardEvent) => this._onKey(e)} />
</div> </div>
<div class="max-h-64 overflow-y-auto py-1"> ${cmds.map(cmd => {
${cmds.length === 0 ? html`<div class="px-4 py-6 text-center text-sm text-muted-foreground">No matching commands</div>` : ""} const idx = globalIdx++;
${cmds.map((c, i) => html` const isSelected = idx === this.selectedIndex;
<button @click=${() => this._exec(c)} return html`
class="w-full flex items-center gap-3 px-4 py-2.5 text-left transition-all duration-150 ${i === this._sel ? "bg-primary/10 text-foreground" : "hover:bg-secondary/50 text-foreground/80"}"> <button @click=${() => this._select(cmd)}
<span class="text-base shrink-0">${c.icon}</span> class="flex items-center gap-3 w-full px-3 py-2 text-left transition-all duration-150"
style="background:${isSelected ? "rgba(255,255,255,0.08)" : "transparent"}"
@mouseenter=${() => { this.selectedIndex = idx; this.requestUpdate(); }}>
<span class="text-base w-6 text-center">${cmd.emoji}</span>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<span class="text-sm font-mono font-medium">${c.cmd}</span> <div class="flex items-center gap-2">
<span class="text-xs text-muted-foreground ml-2">${c.desc}</span> <span class="text-xs font-semibold" style="color:var(--color-card-foreground, hsl(0 0% 95%))">${cmd.label}</span>
<span class="text-[10px] font-mono" style="color:var(--color-muted-foreground, hsl(0 0% 50%))">${cmd.command}</span>
</div>
<p class="text-[10px] truncate" style="color:var(--color-muted-foreground, hsl(0 0% 55%))">${cmd.description}</p>
</div> </div>
</button> </button>
`;
})}
`)} `)}
</div> </div>
<div class="px-4 py-2 border-t border-border/40 flex gap-4 text-[10px] text-muted-foreground/60">
<span>\u2191\u2193 navigate</span><span>\u21B5 select</span><span>esc close</span>
</div> </div>
</div> `;
</div>`;
} }
} }

View file

@ -267,7 +267,7 @@ async function createAgent(initialState?: AgentState) {
// Track tool usage for mood and auto-open panels // Track tool usage for mood and auto-open panels
if (msg.role === "assistant" && msg.toolCalls) { if (msg.role === "assistant" && msg.toolCalls) {
for (const tc of 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") { if (rightPanel !== "browser") {
rightPanel = "browser"; rightPanel = "browser";
renderApp(); renderApp();
@ -487,15 +487,20 @@ function handleSuggestion(e: CustomEvent) {
// ===== PERSONA CHANGE HANDLER ===== // ===== PERSONA CHANGE HANDLER =====
function handlePersonaChange(e: CustomEvent) { function handlePersonaChange(e: CustomEvent) {
const persona = e.detail; const persona = e.detail;
if (agent && persona?.prompt) { if (agent && persona?.systemPrompt) {
agent.setSystemPrompt(persona.prompt); agent.setSystemPrompt(persona.systemPrompt);
} }
} }
// ===== VIEW TOGGLE HANDLER ===== // ===== VIEW TOGGLE HANDLER =====
function handleViewChange(e: CustomEvent) { function handleViewChange(e: CustomEvent) {
const vis = e.detail as UtilityVisibility; const vis = e.detail as UtilityVisibility;
viewState = { ...vis }; viewState = {
tools: vis.showToolCalls,
thinking: vis.showThinking,
system: vis.showSystemMessages,
timestamps: vis.showTimestamps,
};
renderApp(); renderApp();
} }

View file

@ -243,6 +243,8 @@ export class ToolMessage extends LitElement {
override render() { override render() {
const toolName = this.tool?.name || this.toolCall.name; 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) // Render tool content (renderer handles errors and styling)
const result: ToolResultMessageType<any> | undefined = this.aborted const result: ToolResultMessageType<any> | undefined = this.aborted