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
Some checks are pending
CI / build-check-test (push) Waiting to run
This commit is contained in:
parent
84671d57bf
commit
33f439296f
9 changed files with 1244 additions and 495 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<typeof setInterval> | 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 {
|
||||
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 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);
|
||||
}
|
||||
|
||||
private _tk(sym: string, price: number, change: number, ok: boolean) {
|
||||
if (!ok) return html`
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="font-bold text-[11px] text-foreground/60">${sym}</span>
|
||||
<span class="text-[10px] text-muted-foreground animate-pulse">...</span>
|
||||
</div>`;
|
||||
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`
|
||||
<div class="flex items-center gap-1.5 transition-all duration-500">
|
||||
<span class="font-bold text-[11px] text-foreground/70">${sym}</span>
|
||||
<span class="text-[11px] font-mono text-foreground/90">$${f}</span>
|
||||
<span class="${cl} text-[9px] font-medium">${ar}${Math.abs(change).toFixed(1)}%</span>
|
||||
</div>`;
|
||||
try {
|
||||
// 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 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`
|
||||
<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">
|
||||
<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._tk("VVV", this.vvv.p, this.vvv.c, this.vvv.ok)}
|
||||
<div class="w-px h-3 bg-border/40"></div>
|
||||
${this._tk("DIEM", this.diem.p, this.diem.c, this.diem.ok)}
|
||||
</div>`;
|
||||
<div class="px-2 py-1.5 flex items-center gap-2 text-[10px]" style="font-variant-numeric:tabular-nums">
|
||||
${this.tokens.map(t => html`
|
||||
<div class="flex items-center gap-1.5 px-2 py-1 rounded-lg transition-all duration-300"
|
||||
style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06)">
|
||||
<span class="font-bold" style="color:var(--color-card-foreground, hsl(0 0% 90%))">${t.symbol}</span>
|
||||
${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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, boolean>;
|
||||
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<string, boolean> = {};
|
||||
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`
|
||||
<div class="fixed inset-0 z-50 flex" @click=${() => this.hide()}>
|
||||
<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()}>
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-border/40 shrink-0">
|
||||
<button @click=${() => this.toggle()}
|
||||
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-[11px] font-medium transition-all duration-300"
|
||||
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>
|
||||
<h2 class="font-bold text-foreground text-sm">\u{1F3EA} Marketplace</h2>
|
||||
<span class="text-[11px] text-muted-foreground">${active}/${this._skills.length} skills active</span>
|
||||
<h2 class="text-base font-bold">🛒 Skills Marketplace</h2>
|
||||
<p class="text-[11px] mt-0.5" style="color:var(--color-muted-foreground, hsl(0 0% 60%))">Toggle capabilities on or off</p>
|
||||
</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 class="flex gap-1.5 px-3 py-2 overflow-x-auto border-b border-border/30 shrink-0">
|
||||
${this._cats.map(c => html`
|
||||
<button @click=${() => { this._cat = c; 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>
|
||||
<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.categories.map(cat => html`
|
||||
<button @click=${() => { this.filterCat = cat; this.requestUpdate(); }}
|
||||
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 class="flex-1 overflow-y-auto p-3 space-y-2">
|
||||
${this._filtered.map(s => 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"}">
|
||||
<span class="text-2xl shrink-0 mt-0.5">${s.icon}</span>
|
||||
<div class="flex-1 overflow-y-auto p-4 grid grid-cols-2 gap-2">
|
||||
${this.filtered.map(skill => html`
|
||||
<div class="flex items-start gap-3 p-3 rounded-xl transition-all duration-200"
|
||||
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="font-medium text-sm text-foreground">${s.name}</div>
|
||||
<div class="text-[11px] text-muted-foreground mt-0.5">${s.desc}</div>
|
||||
<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>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-semibold">${skill.name}</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>
|
||||
<button @click=${() => this._toggle(s.id)}
|
||||
class="shrink-0 w-10 h-[22px] rounded-full transition-all duration-300 relative ${s.enabled ? "bg-primary" : "bg-muted"}">
|
||||
<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>
|
||||
<p class="text-[10px] mt-0.5" style="color:var(--color-muted-foreground, hsl(0 0% 55%))">${skill.description}</p>
|
||||
</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>
|
||||
</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>
|
||||
` : nothing}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
export interface Memory {
|
||||
id: string;
|
||||
content: string;
|
||||
tags: string[];
|
||||
timestamp: 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<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);
|
||||
});
|
||||
}
|
||||
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; }
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._load();
|
||||
}
|
||||
|
||||
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();
|
||||
private _load() {
|
||||
try {
|
||||
const raw = localStorage.getItem("jae-memories");
|
||||
if (raw) this._memories = JSON.parse(raw);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
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 _save() {
|
||||
localStorage.setItem("jae-memories", JSON.stringify(this._memories));
|
||||
}
|
||||
|
||||
// === 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) {
|
||||
await memoryDelete(id);
|
||||
this.entries = this.entries.filter((e) => e.id !== id);
|
||||
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();
|
||||
}
|
||||
|
||||
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();
|
||||
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.open) return html``;
|
||||
const entries = this.filtered;
|
||||
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`
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click=${(e: Event) => {
|
||||
if (e.target === e.currentTarget) this.hide();
|
||||
}}>
|
||||
<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 justify-between px-6 py-4 border-b border-border shrink-0">
|
||||
<h2 class="font-semibold text-lg">🧠 Memory Manager</h2>
|
||||
<button class="text-muted-foreground hover:text-foreground" @click=${() => this.hide()}>✕</button>
|
||||
<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(); }}>
|
||||
<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="flex items-center gap-2">
|
||||
<span class="text-lg">\u{1F9E0}</span>
|
||||
<span class="text-sm font-semibold text-foreground">Memory Bank</span>
|
||||
<span class="text-[10px] px-1.5 py-0.5 rounded-full bg-primary/20 text-primary">${this._memories.length} memories</span>
|
||||
</div>
|
||||
<div class="px-6 py-3 border-b border-border shrink-0">
|
||||
<input type="text" placeholder="Filter memories..." class="w-full bg-secondary text-foreground rounded-lg px-3 py-2 text-sm outline-none"
|
||||
.value=${this.filter} @input=${(e: Event) => {
|
||||
this.filter = (e.target as HTMLInputElement).value;
|
||||
}} />
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 p-3 border-b border-white/5">
|
||||
<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 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="text-sm">${entry.content}</div>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<span class="text-xs text-muted-foreground">${entry.timestamp.slice(0, 10)}</span>
|
||||
${entry.tags.map((tag) => html`<span class="text-xs bg-secondary px-1.5 py-0.5 rounded">${tag}</span>`)}
|
||||
<div class="text-xs text-foreground/90">${m.text}</div>
|
||||
<div class="text-[9px] text-muted-foreground/40 mt-1">${new Date(m.timestamp).toLocaleDateString()}</div>
|
||||
</div>
|
||||
<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>
|
||||
<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">🗑</button>
|
||||
`)}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</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="p-3 border-t border-white/10">
|
||||
<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"
|
||||
.value=${this.newTags} @input=${(e: Event) => {
|
||||
this.newTags = (e.target as HTMLInputElement).value;
|
||||
}} />
|
||||
<button class="bg-primary text-primary-foreground px-4 py-2 rounded-lg text-sm font-medium hover:opacity-90"
|
||||
@click=${() => this.addEntry()}>Save</button>
|
||||
<input id="jae-mem-input" type="text" placeholder="Add a memory manually..."
|
||||
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"
|
||||
@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 = ""; } }}
|
||||
class="px-3 py-1.5 text-xs bg-primary/20 text-primary rounded-lg hover:bg-primary/30 transition-colors">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Mood, { emoji: string; label: string; color: string; glow: string }> = {
|
||||
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<typeof setInterval> | 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.triggerAnger();
|
||||
}
|
||||
if (m === "neutral" || m === "focused") {
|
||||
// User calmed JAE down
|
||||
this.shaking = false;
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
if (prev === "angry" && m !== "angry") {
|
||||
this._crackOpacity = Math.min(this._crackOpacity, 0.3);
|
||||
this.requestUpdate();
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
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() {
|
||||
const d = this._def();
|
||||
const cfg = MOOD_CONFIG[this.mood];
|
||||
|
||||
return html`
|
||||
<div class="flex items-center gap-2 px-2 py-1 rounded-lg transition-all duration-500 ${this._shaking ? "jae-shake" : ""}">
|
||||
<div class="relative w-7 h-7 transition-all duration-500" style="filter: ${d.glow}">
|
||||
<img src="${d.icon}" alt="JAE" class="w-full h-full object-contain rounded-full transition-all duration-500"
|
||||
style="${this._mood === "angry" ? "animation: jae-pulse-angry 0.3s infinite;" : ""}" />
|
||||
<!-- Mood badge -->
|
||||
<div class="flex items-center gap-1.5 px-2 py-1 rounded-full text-[10px] transition-all duration-500"
|
||||
style="background:rgba(255,255,255,0.04);border:1px solid ${cfg.color};box-shadow:${cfg.glow}">
|
||||
<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 class="flex flex-col">
|
||||
<span class="text-[10px] font-medium ${d.color} transition-colors duration-500">${d.label}</span>
|
||||
${this._mood === "angry" ? html`<span class="text-[8px] text-red-500 animate-pulse">click to calm</span>` : ""}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
<!-- Crack overlay on chat -->
|
||||
${this.cracked ? html`
|
||||
<div class="jae-crack-overlay" style="opacity:${this.crackOpacity};pointer-events:none"></div>
|
||||
` : html``}
|
||||
|
||||
<!-- Shake class injector -->
|
||||
${this.shaking ? html`<style>.jae-chat-area { animation: jae-shake 0.1s infinite alternate !important; }</style>` : html``}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
<div class="relative">
|
||||
<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">
|
||||
<span class="text-sm">${this._current.icon}</span>
|
||||
<span class="text-foreground/80 font-medium hidden sm:inline">${this._current.name}</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>${current.emoji}</span>
|
||||
<span class="text-muted-foreground">${current.label}</span>
|
||||
<span class="text-[10px] text-muted-foreground/50">${this._open ? "\u25B2" : "\u25BC"}</span>
|
||||
</button>
|
||||
${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="px-3 py-2 border-b border-border/40 text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">Persona</div>
|
||||
<div class="max-h-80 overflow-y-auto py-1">
|
||||
${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 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="p-2.5 border-b border-white/10">
|
||||
<div class="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">Personas</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>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
` : ""}
|
||||
</div>` : ""}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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`
|
||||
<div class="fixed inset-0 z-50" @click=${() => this.hide()}>
|
||||
<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"
|
||||
@click=${(e: Event) => e.stopPropagation()}>
|
||||
<div class="flex items-center gap-2 px-4 py-3 border-b border-border/40">
|
||||
<span class="text-muted-foreground text-lg font-mono">/</span>
|
||||
<input type="text" class="flex-1 bg-transparent outline-none text-sm text-foreground placeholder:text-muted-foreground"
|
||||
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 class="absolute bottom-full left-0 right-0 mb-1 mx-4 z-50">
|
||||
<div class="rounded-xl border shadow-2xl overflow-hidden jae-scale-in"
|
||||
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">
|
||||
${Array.from(groups.entries()).map(([cat, cmds]) => html`
|
||||
<div class="px-3 pt-2 pb-1">
|
||||
<span class="text-[10px] font-semibold uppercase tracking-widest" style="color:var(--color-muted-foreground, hsl(0 0% 55%))">${cat}</span>
|
||||
</div>
|
||||
<div class="max-h-64 overflow-y-auto py-1">
|
||||
${cmds.length === 0 ? html`<div class="px-4 py-6 text-center text-sm text-muted-foreground">No matching commands</div>` : ""}
|
||||
${cmds.map((c, i) => html`
|
||||
<button @click=${() => this._exec(c)}
|
||||
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"}">
|
||||
<span class="text-base shrink-0">${c.icon}</span>
|
||||
${cmds.map(cmd => {
|
||||
const idx = globalIdx++;
|
||||
const isSelected = idx === this.selectedIndex;
|
||||
return html`
|
||||
<button @click=${() => this._select(cmd)}
|
||||
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">
|
||||
<span class="text-sm font-mono font-medium">${c.cmd}</span>
|
||||
<span class="text-xs text-muted-foreground ml-2">${c.desc}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<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>
|
||||
</button>
|
||||
`;
|
||||
})}
|
||||
`)}
|
||||
</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>`;
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<any> | undefined = this.aborted
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue