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 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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"];
|
||||||
|
if (vt) {
|
||||||
|
this.tokens = this.tokens.map(t =>
|
||||||
|
t.symbol === "VVV" ? { ...t, price: vt.usd || 0, change24h: vt.usd_24h_change || 0, loading: false } : t
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
this.tokens = this.tokens.map(t => t.symbol === "VVV" ? { ...t, loading: false } : t);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const r = await fetch("https://api.dexscreener.com/latest/dex/search?q=DIEM%20venice");
|
// Try CoinGecko for DIEM
|
||||||
const j = await r.json();
|
const diemRes = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=venice-diem&vs_currencies=usd&include_24hr_change=true");
|
||||||
const pair = j?.pairs?.[0];
|
if (diemRes.ok) {
|
||||||
if (pair) this.diem = { p: parseFloat(pair.priceUsd || "0"), c: parseFloat(pair.priceChange?.h24 || "0"), ok: true };
|
const data = await diemRes.json();
|
||||||
} catch {}
|
const dt = data["venice-diem"];
|
||||||
|
if (dt) {
|
||||||
|
this.tokens = this.tokens.map(t =>
|
||||||
|
t.symbol === "DIEM" ? { ...t, price: dt.usd || 0, change24h: dt.usd_24h_change || 0, loading: false } : t
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
this.tokens = this.tokens.map(t => t.symbol === "DIEM" ? { ...t, loading: false } : t);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _tk(sym: string, price: number, change: number, ok: boolean) {
|
private formatPrice(price: number): string {
|
||||||
if (!ok) return html`
|
if (price === 0) return "--";
|
||||||
<div class="flex items-center gap-1">
|
if (price < 0.01) return `$${price.toFixed(6)}`;
|
||||||
<span class="font-bold text-[11px] text-foreground/60">${sym}</span>
|
if (price < 1) return `$${price.toFixed(4)}`;
|
||||||
<span class="text-[10px] text-muted-foreground animate-pulse">...</span>
|
return `$${price.toFixed(2)}`;
|
||||||
</div>`;
|
}
|
||||||
const up = change >= 0;
|
|
||||||
const cl = up ? "text-emerald-400" : "text-red-400";
|
private formatChange(change: number): string {
|
||||||
const ar = up ? "▲" : "▼";
|
const sign = change >= 0 ? "+" : "";
|
||||||
const f = price < 0.01 ? price.toFixed(6) : price < 1 ? price.toFixed(4) : price.toFixed(2);
|
return `${sign}${change.toFixed(1)}%`;
|
||||||
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>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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%))"
|
||||||
<div>
|
@mouseenter=${(e: Event) => (e.currentTarget as HTMLElement).style.background = "rgba(255,255,255,0.08)"}
|
||||||
<h2 class="font-bold text-foreground text-sm">\u{1F3EA} Marketplace</h2>
|
@mouseleave=${(e: Event) => (e.currentTarget as HTMLElement).style.background = "rgba(255,255,255,0.04)"}>
|
||||||
<span class="text-[11px] text-muted-foreground">${active}/${this._skills.length} skills active</span>
|
<span>🛒</span> Marketplace
|
||||||
</div>
|
</button>
|
||||||
<button @click=${() => this.hide()} class="text-muted-foreground hover:text-foreground transition-colors text-lg">\u2715</button>
|
|
||||||
</div>
|
${this.open ? html`
|
||||||
<div class="flex gap-1.5 px-3 py-2 overflow-x-auto border-b border-border/30 shrink-0">
|
<div class="fixed inset-0 z-50" @click=${(e: Event) => { if (e.target === e.currentTarget) this.hide(); }}
|
||||||
${this._cats.map(c => html`
|
style="background:rgba(0,0,0,0.5);backdrop-filter:blur(4px)">
|
||||||
<button @click=${() => { this._cat = c; this.requestUpdate(); }}
|
<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"
|
||||||
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>
|
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>
|
||||||
<div class="flex-1 overflow-y-auto p-3 space-y-2">
|
<h2 class="text-base font-bold">🛒 Skills Marketplace</h2>
|
||||||
${this._filtered.map(s => html`
|
<p class="text-[11px] mt-0.5" style="color:var(--color-muted-foreground, hsl(0 0% 60%))">Toggle capabilities on or off</p>
|
||||||
<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 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>
|
|
||||||
<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>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
`)}
|
<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 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-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="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>
|
||||||
|
<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>
|
</div>
|
||||||
</div>`;
|
` : nothing}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
async show() {
|
override connectedCallback() {
|
||||||
this.open = true;
|
super.connectedCallback();
|
||||||
this.loading = true;
|
this._load();
|
||||||
this.requestUpdate();
|
}
|
||||||
this.entries = await memoryLoad();
|
|
||||||
this.loading = false;
|
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
hide() {
|
|
||||||
this.open = false;
|
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
get filtered() {
|
private _load() {
|
||||||
if (!this.filter) return this.entries;
|
try {
|
||||||
const q = this.filter.toLowerCase();
|
const raw = localStorage.getItem("jae-memories");
|
||||||
return this.entries.filter(
|
if (raw) this._memories = JSON.parse(raw);
|
||||||
(e) => e.content.toLowerCase().includes(q) || e.tags.some((t) => t.toLowerCase().includes(q)),
|
} catch {}
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async deleteEntry(id: string) {
|
private _save() {
|
||||||
await memoryDelete(id);
|
localStorage.setItem("jae-memories", JSON.stringify(this._memories));
|
||||||
this.entries = this.entries.filter((e) => e.id !== id);
|
}
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
async addEntry() {
|
// === AUTO-LEARNING: Extract facts from messages ===
|
||||||
if (!this.newContent.trim()) return;
|
autoLearn(userMsg: string, assistantMsg: string) {
|
||||||
const tags = this.newTags
|
const facts = this._extractFacts(userMsg, assistantMsg);
|
||||||
.split(",")
|
for (const fact of facts) {
|
||||||
.map((t) => t.trim())
|
// Don't duplicate
|
||||||
.filter(Boolean);
|
const exists = this._memories.some(m =>
|
||||||
await memorySave(this.newContent.trim(), tags);
|
m.text.toLowerCase().includes(fact.text.toLowerCase().slice(0, 30))
|
||||||
this.newContent = "";
|
);
|
||||||
this.newTags = "";
|
if (!exists) {
|
||||||
this.entries = await memoryLoad();
|
this._memories.push(fact);
|
||||||
this.requestUpdate();
|
}
|
||||||
}
|
}
|
||||||
|
this._save();
|
||||||
|
}
|
||||||
|
|
||||||
override render() {
|
private _extractFacts(user: string, assistant: string): Memory[] {
|
||||||
if (!this.open) return html``;
|
const facts: Memory[] = [];
|
||||||
const entries = this.filtered;
|
const now = Date.now();
|
||||||
return html`
|
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click=${(e: Event) => {
|
// Preference patterns
|
||||||
if (e.target === e.currentTarget) this.hide();
|
const prefPatterns = [
|
||||||
}}>
|
/(?:i (?:prefer|like|love|hate|use|want|need|always))\s+(.{5,80})/gi,
|
||||||
<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]">
|
/(?:my (?:favourite|favorite|preferred))\s+(?:\w+\s+)?(?:is|are)\s+(.{3,60})/gi,
|
||||||
<div class="flex items-center justify-between px-6 py-4 border-b border-border shrink-0">
|
/(?:i(?:'m| am) (?:using|running|on))\s+(.{3,60})/gi,
|
||||||
<h2 class="font-semibold text-lg">🧠 Memory Manager</h2>
|
];
|
||||||
<button class="text-muted-foreground hover:text-foreground" @click=${() => this.hide()}>✕</button>
|
for (const pat of prefPatterns) {
|
||||||
|
let m;
|
||||||
|
while ((m = pat.exec(user)) !== null) {
|
||||||
|
facts.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
text: m[0].trim(),
|
||||||
|
category: "preference",
|
||||||
|
timestamp: now,
|
||||||
|
source: "auto",
|
||||||
|
importance: 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name / identity patterns
|
||||||
|
const namePatterns = [
|
||||||
|
/(?:my name is|i(?:'m| am) called|call me)\s+(\w[\w\s]{1,30})/gi,
|
||||||
|
/(?:i work (?:at|for|on))\s+(.{3,60})/gi,
|
||||||
|
/(?:my (?:email|address|phone|website) is)\s+(.{3,80})/gi,
|
||||||
|
];
|
||||||
|
for (const pat of namePatterns) {
|
||||||
|
let m;
|
||||||
|
while ((m = pat.exec(user)) !== null) {
|
||||||
|
facts.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
text: m[0].trim(),
|
||||||
|
category: "person",
|
||||||
|
timestamp: now,
|
||||||
|
source: "auto",
|
||||||
|
importance: 3,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Project patterns
|
||||||
|
const projPatterns = [
|
||||||
|
/(?:(?:the|my|our) project (?:is called|named|is))\s+(.{3,60})/gi,
|
||||||
|
/(?:i(?:'m| am) (?:building|developing|creating|working on))\s+(.{3,80})/gi,
|
||||||
|
];
|
||||||
|
for (const pat of projPatterns) {
|
||||||
|
let m;
|
||||||
|
while ((m = pat.exec(user)) !== null) {
|
||||||
|
facts.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
text: m[0].trim(),
|
||||||
|
category: "project",
|
||||||
|
timestamp: now,
|
||||||
|
source: "auto",
|
||||||
|
importance: 3,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return facts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Get context for system prompt ===
|
||||||
|
getContext(): string {
|
||||||
|
if (this._memories.length === 0) return "";
|
||||||
|
const sorted = [...this._memories].sort((a, b) => b.importance - a.importance);
|
||||||
|
const lines = sorted.slice(0, 20).map(m => `- [${m.category}] ${m.text}`);
|
||||||
|
return `\n\nUser memories (auto-learned):\n${lines.join("\n")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
addManual(text: string, category: string = "custom") {
|
||||||
|
this._memories.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
text,
|
||||||
|
category,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
source: "manual",
|
||||||
|
importance: 3,
|
||||||
|
});
|
||||||
|
this._save();
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _delete(id: string) {
|
||||||
|
this._memories = this._memories.filter(m => m.id !== id);
|
||||||
|
this._save();
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setImportance(id: string, val: number) {
|
||||||
|
const m = this._memories.find(x => x.id === id);
|
||||||
|
if (m) { m.importance = val; this._save(); this.requestUpdate(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
show() { this._visible = true; this.requestUpdate(); }
|
||||||
|
hide() { this._visible = false; this.requestUpdate(); }
|
||||||
|
toggle() { this._visible = !this._visible; this.requestUpdate(); }
|
||||||
|
|
||||||
|
get memoryCount() { return this._memories.length; }
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
if (!this._visible) return html``;
|
||||||
|
const filtered = this._memories.filter(m => {
|
||||||
|
if (this._catFilter !== "all" && m.category !== this._catFilter) return false;
|
||||||
|
if (this._filter && !m.text.toLowerCase().includes(this._filter.toLowerCase())) return false;
|
||||||
|
return true;
|
||||||
|
}).sort((a, b) => b.timestamp - a.timestamp);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<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>
|
||||||
<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(); }}
|
||||||
</div>
|
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" />
|
||||||
<div class="flex-1 overflow-y-auto px-6 py-4">
|
<select @change=${(e: Event) => { this._catFilter = (e.target as HTMLSelectElement).value; this.requestUpdate(); }}
|
||||||
${this.loading ? html`<div class="text-center text-muted-foreground py-8">Loading...</div>` : ""}
|
class="px-2 py-1.5 text-xs bg-white/5 border border-white/10 rounded-lg text-foreground">
|
||||||
${!this.loading && entries.length === 0 ? html`<div class="text-center text-muted-foreground py-8">No memories stored yet</div>` : ""}
|
<option value="all">All</option>
|
||||||
<div class="flex flex-col gap-2">
|
${CATEGORIES.map(c => html`<option value=${c}>${c}</option>`)}
|
||||||
${entries.map(
|
</select>
|
||||||
(entry) => html`
|
</div>
|
||||||
<div class="flex gap-3 p-3 rounded-lg border border-border bg-background group">
|
<div class="flex-1 overflow-y-auto p-3 space-y-2">
|
||||||
<div class="flex-1 min-w-0">
|
${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>` : ""}
|
||||||
<div class="text-sm">${entry.content}</div>
|
${filtered.map(m => html`
|
||||||
<div class="flex items-center gap-2 mt-1">
|
<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">
|
||||||
<span class="text-xs text-muted-foreground">${entry.timestamp.slice(0, 10)}</span>
|
<div class="flex flex-col items-center gap-1 shrink-0">
|
||||||
${entry.tags.map((tag) => html`<span class="text-xs bg-secondary px-1.5 py-0.5 rounded">${tag}</span>`)}
|
<span class="text-[10px] px-1.5 py-0.5 rounded bg-primary/15 text-primary/80">${m.category}</span>
|
||||||
</div>
|
<span class="text-[9px] text-muted-foreground/40">${m.source === "auto" ? "\u{1F916}" : "\u{270D}\uFE0F"}</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="shrink-0 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity"
|
<div class="flex-1 min-w-0">
|
||||||
@click=${() => this.deleteEntry(entry.id)} title="Delete">🗑</button>
|
<div class="text-xs text-foreground/90">${m.text}</div>
|
||||||
</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">
|
||||||
</div>
|
<select .value=${String(m.importance)} @change=${(e: Event) => this._setImportance(m.id, Number((e.target as HTMLSelectElement).value))}
|
||||||
</div>
|
class="text-[10px] bg-transparent border border-white/10 rounded px-1 py-0.5 text-foreground" title="Importance">
|
||||||
<div class="px-6 py-4 border-t border-border shrink-0">
|
${[1,2,3,4,5].map(n => html`<option value=${n} ?selected=${m.importance === n}>${"\u2B50".repeat(n)}</option>`)}
|
||||||
<div class="flex flex-col gap-2">
|
</select>
|
||||||
<textarea placeholder="New memory content..." class="w-full bg-secondary rounded-lg px-3 py-2 text-sm outline-none resize-none"
|
<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>
|
||||||
rows="2" .value=${this.newContent}
|
|
||||||
@input=${(e: Event) => {
|
|
||||||
this.newContent = (e.target as HTMLTextAreaElement).value;
|
|
||||||
}}></textarea>
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
<div class="p-3 border-t border-white/10">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<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>
|
||||||
`;
|
</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
this._crackOpacity = 1;
|
|
||||||
this.requestUpdate();
|
|
||||||
this.dispatchEvent(new CustomEvent("mood-angry", { bubbles: true, composed: true }));
|
|
||||||
if (this._crackTimer) clearInterval(this._crackTimer);
|
|
||||||
this._crackTimer = setInterval(() => {
|
|
||||||
this._crackOpacity = Math.max(0, this._crackOpacity - 0.0033);
|
|
||||||
if (this._crackOpacity <= 0) {
|
|
||||||
this._cracked = false;
|
|
||||||
clearInterval(this._crackTimer);
|
|
||||||
}
|
|
||||||
this.dispatchEvent(new CustomEvent("crack-update", { detail: { opacity: this._crackOpacity, active: this._cracked }, bubbles: true, composed: true }));
|
|
||||||
this.requestUpdate();
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
|
if (m === "neutral" || m === "focused") {
|
||||||
if (prev === "angry" && m !== "angry") {
|
// User calmed JAE down
|
||||||
this._crackOpacity = Math.min(this._crackOpacity, 0.3);
|
this.shaking = false;
|
||||||
this.requestUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent("mood-change", { detail: { mood: m }, bubbles: true, composed: true }));
|
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
analyzeSentiment(text: string) {
|
getMood(): Mood { return this.mood; }
|
||||||
const t = text.toLowerCase();
|
|
||||||
if (/calm down|relax|chill|easy|breathe/.test(t)) { this.setMood("neutral"); return; }
|
/** Analyse message text to infer mood */
|
||||||
if (/error|fail|bug|broken|crash|exception|undefined is not/.test(t)) {
|
analyseText(text: string) {
|
||||||
if (this._mood === "frustrated") this.setMood("angry");
|
const lower = text.toLowerCase();
|
||||||
else if (this._mood === "warning") this.setMood("frustrated");
|
// Check for error indicators
|
||||||
else this.setMood("warning");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (/amazing|awesome|excellent|perfect|brilliant|great job/.test(t)) { this.setMood("excited"); return; }
|
|
||||||
if (/think|analyze|consider|examine|debug|investigate/.test(t)) { this.setMood("focused"); return; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _def(): MoodDef { return MOODS.find(m => m.id === this._mood) || MOODS[0]; }
|
private triggerAnger() {
|
||||||
|
this.shaking = true;
|
||||||
|
this.cracked = true;
|
||||||
|
this.crackOpacity = 1.0;
|
||||||
|
|
||||||
|
// Shake for 2 seconds
|
||||||
|
setTimeout(() => { this.shaking = false; this.requestUpdate(); }, 2000);
|
||||||
|
|
||||||
|
// Fade crack over 5 minutes
|
||||||
|
if (this.fadeTimer) clearInterval(this.fadeTimer);
|
||||||
|
const steps = 300; // 300 seconds = 5 min
|
||||||
|
const decrement = 1.0 / steps;
|
||||||
|
this.fadeTimer = setInterval(() => {
|
||||||
|
this.crackOpacity = Math.max(0, this.crackOpacity - decrement);
|
||||||
|
if (this.crackOpacity <= 0) {
|
||||||
|
this.cracked = false;
|
||||||
|
if (this.fadeTimer) clearInterval(this.fadeTimer);
|
||||||
|
}
|
||||||
|
this.requestUpdate();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
override disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
if (this.fadeTimer) clearInterval(this.fadeTimer);
|
||||||
|
}
|
||||||
|
|
||||||
override render() {
|
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>
|
||||||
</div>
|
<span class="font-medium" style="color:${cfg.color}">${cfg.label}</span>
|
||||||
<div class="flex flex-col">
|
</div>
|
||||||
<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>` : ""}
|
<!-- Crack overlay on chat -->
|
||||||
</div>
|
${this.cracked ? html`
|
||||||
</div>`;
|
<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 {
|
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`
|
</div>
|
||||||
<button @click=${() => this._select(p)}
|
<div class="max-h-[320px] overflow-y-auto p-1.5 space-y-0.5">
|
||||||
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" : ""}">
|
${PERSONAS.map(p => html`
|
||||||
<span class="text-lg shrink-0">${p.icon}</span>
|
<button @click=${() => this.setPersona(p.id)}
|
||||||
<div class="min-w-0 flex-1">
|
class="w-full flex items-start gap-2.5 p-2.5 rounded-lg text-left transition-all duration-200
|
||||||
<div class="text-sm font-medium text-foreground">${p.name}</div>
|
${p.id === this._activeId ? "bg-primary/15 border border-primary/30" : "hover:bg-white/5 border border-transparent"}">
|
||||||
<div class="text-[11px] text-muted-foreground truncate">${p.desc}</div>
|
<span class="text-lg shrink-0 mt-0.5">${p.emoji}</span>
|
||||||
</div>
|
<div class="flex-1 min-w-0">
|
||||||
${p.id === this._current.id ? html`<span class="text-primary text-xs font-bold">\u2713</span>` : ""}
|
<div class="text-xs font-medium text-foreground">${p.label}</div>
|
||||||
</button>
|
<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>
|
</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";
|
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}
|
</div>
|
||||||
@input=${(e: Event) => { this._filter = (e.target as HTMLInputElement).value; this._sel = 0; this.requestUpdate(); }}
|
${cmds.map(cmd => {
|
||||||
@keydown=${(e: KeyboardEvent) => this._onKey(e)} />
|
const idx = globalIdx++;
|
||||||
</div>
|
const isSelected = idx === this.selectedIndex;
|
||||||
<div class="max-h-64 overflow-y-auto py-1">
|
return html`
|
||||||
${cmds.length === 0 ? html`<div class="px-4 py-6 text-center text-sm text-muted-foreground">No matching commands</div>` : ""}
|
<button @click=${() => this._select(cmd)}
|
||||||
${cmds.map((c, i) => html`
|
class="flex items-center gap-3 w-full px-3 py-2 text-left transition-all duration-150"
|
||||||
<button @click=${() => this._exec(c)}
|
style="background:${isSelected ? "rgba(255,255,255,0.08)" : "transparent"}"
|
||||||
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"}">
|
@mouseenter=${() => { this.selectedIndex = idx; this.requestUpdate(); }}>
|
||||||
<span class="text-base shrink-0">${c.icon}</span>
|
<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>
|
||||||
</div>
|
<span class="text-[10px] font-mono" style="color:var(--color-muted-foreground, hsl(0 0% 50%))">${cmd.command}</span>
|
||||||
</button>
|
</div>
|
||||||
`)}
|
<p class="text-[10px] truncate" style="color:var(--color-muted-foreground, hsl(0 0% 55%))">${cmd.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-4 py-2 border-t border-border/40 flex gap-4 text-[10px] text-muted-foreground/60">
|
</button>
|
||||||
<span>\u2191\u2193 navigate</span><span>\u21B5 select</span><span>esc close</span>
|
`;
|
||||||
</div>
|
})}
|
||||||
|
`)}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue