2026-05-31 23:58:26 +09:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" / >
< title > Odysseus Chat< / title >
< link rel = "icon" type = "image/svg+xml" href = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M16 4L16 22L6 22Z' fill='%23e06c75'/%3E%3Cpath d='M16 8L16 22L24 22Z' fill='%23e06c75' opacity='0.6'/%3E%3Cpath d='M4 24Q10 20 16 24Q22 28 28 24' stroke='%23e06c75' stroke-width='2.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E" >
< link rel = "manifest" href = "/static/manifest.json" >
< meta name = "theme-color" content = "#282c34" >
< meta name = "apple-mobile-web-app-capable" content = "yes" >
< meta name = "apple-mobile-web-app-status-bar-style" content = "black-translucent" >
<!-- Disable iOS / Android auto - linkifying of numbers (phone/date/address)
in email bodies — was wrapping random digits in < a href = "tel:..." > with
browser-default styling that didn't match the Odysseus theme. -->
< meta name = "format-detection" content = "telephone=no, date=no, address=no, email=no" >
< link rel = "apple-touch-icon" href = "/static/icon-192.png" >
< script nonce = "{{CSP_NONCE}}" >
window._odysseusLoadTime = Date.now();
(function(){
try {
var t = JSON.parse(localStorage.getItem('odysseus-theme'));
if (t & & t.colors) {
var s = document.documentElement.style;
var c = t.colors;
s.setProperty('--bg', c.bg);
s.setProperty('--fg', c.fg);
s.setProperty('--panel', c.panel);
s.setProperty('--border', c.border);
if (c.red) s.setProperty('--red', c.red);
// Set --brand-color now (= custom brand or the theme red) so the
// loading-screen ASCII wave shows its final colour from the first
// paint. Otherwise it starts on --red and visibly switches once the
// app boots and applyColors() fills --brand-color.
var _bc = (c.advanced & & c.advanced.brandColor) || c.red;
if (_bc) s.setProperty('--brand-color', _bc);
// Match the mobile browser toolbar / status bar to the saved theme bg
// from the very first paint. Otherwise the hardcoded dark meta sits
// there during load and the bar "switches" colour once the app boots.
var mtc = document.querySelector('meta[name="theme-color"]');
if (mtc & & c.bg) mtc.setAttribute('content', c.bg);
// Update favicon to match accent color
var ac = c.red || '#e06c75';
var fav = document.querySelector("link[rel='icon']");
if (fav) fav.href = 'data:image/svg+xml,' + encodeURIComponent("< svg xmlns = 'http://www.w3.org/2000/svg' viewBox = '0 0 32 32' > < path d = 'M16 4L16 22L6 22Z' fill = '" + ac + "' / > < path d = 'M16 8L16 22L24 22Z' fill = '" + ac + "' opacity = '0.6' / > < path d = 'M4 24Q10 20 16 24Q22 28 28 24' stroke = '" + ac + "' stroke-width = '2.5' fill = 'none' stroke-linecap = 'round' / > < / svg > ");
// Derive syntax highlighting colors from theme
function h2hsl(hex) {
hex = hex.replace('#','');
var r=parseInt(hex.substring(0,2),16)/255, g=parseInt(hex.substring(2,4),16)/255, b=parseInt(hex.substring(4,6),16)/255;
var mx=Math.max(r,g,b), mn=Math.min(r,g,b), h, sv, l=(mx+mn)/2;
if(mx===mn){h=sv=0}else{var d=mx-mn;sv=l>0.5?d/(2-mx-mn):d/(mx+mn);if(mx===r)h=((g-b)/d+(g< b ? 6:0 ) ) / 6 ; else if ( mx = ==g)h=((b-r)/d+2)/6;else h = ((r-g)/d+4)/6}
return[h*360,sv*100,l*100];
}
function hsl2h(h,sv,l) {
h=((h%360)+360)%360;sv=Math.max(0,Math.min(100,sv))/100;l=Math.max(0,Math.min(100,l))/100;
var a=sv*Math.min(l,1-l);function f(n){var k=(n+h/30)%12;return l-a*Math.max(-1,Math.min(k-3,9-k,1))}
function th(v){return Math.round(v*255).toString(16).padStart(2,'0')}
return'#'+th(f(0))+th(f(8))+th(f(4));
}
var fH=h2hsl(c.fg),bH=h2hsl(c.bg),rH=h2hsl(c.red||'#e06c75');
var dk=bH[2]< 50 ;
s.setProperty('--hl-bg', hsl2h(bH[0],bH[1],dk?Math.max(bH[2]-4,0):Math.min(bH[2]+4,100)));
s.setProperty('--hl-fg', c.fg);
s.setProperty('--hl-keyword', hsl2h((rH[0]+280)%360,Math.min(rH[1]+10,80),dk?70:45));
s.setProperty('--hl-string', hsl2h(40,Math.min(fH[1]+20,70),dk?72:42));
s.setProperty('--hl-comment', hsl2h(fH[0],Math.max(fH[1]-20,5),(fH[2]*0.5+bH[2]*0.5)));
s.setProperty('--hl-function', hsl2h(210,Math.min(fH[1]+20,75),dk?70:45));
s.setProperty('--hl-number', hsl2h(20,Math.min(fH[1]+15,65),dk?68:48));
s.setProperty('--hl-builtin', hsl2h(180,Math.min(fH[1]+15,60),dk?65:40));
s.setProperty('--hl-variable', hsl2h((fH[0]+30)%360,Math.min(fH[1]+5,60),fH[2]));
s.setProperty('--hl-params', hsl2h(fH[0],Math.max(fH[1]-5,10),dk?Math.min(fH[2]+8,85):Math.max(fH[2]-8,25)));
// Apply advanced overrides if present
if (c.advanced) {
var a = c.advanced;
var advMap = {userBubbleBg:'--user-bubble-bg',aiBubbleBg:'--ai-bubble-bg',bubbleBorder:'--bubble-border',sidebarBg:'--sidebar-bg',sectionAccent:'--section-accent',brandColor:'--brand-color',inputBg:'--input-bg',inputBorder:'--input-border',sendBtnBg:'--send-btn-bg',sendBtnHover:'--send-btn-hover',codeBg:'--code-bg',codeFg:'--code-fg',toggleBg:'--toggle-bg',toggleActive:'--toggle-active',accentPrimary:'--accent-primary',accentError:'--accent-error'};
for (var k in advMap) { if (a[k]) s.setProperty(advMap[k], a[k]); }
}
}
// Apply font early
if (t & & t.font) {
var fm = {mono:"'Fira Code', monospace",sans:"system-ui, -apple-system, 'Segoe UI', sans-serif",serif:"Georgia, 'Times New Roman', serif"};
if (fm[t.font]) { s.setProperty('--font-family', fm[t.font]); }
else { s.setProperty('--font-family', "'" + t.font.replace(/'/g,'') + "', sans-serif"); }
}
// Apply density class on html
if (t & & t.density & & t.density !== 'comfortable') {
document.documentElement.classList.add('density-' + t.density);
}
// Apply background pattern on body once available
if (t & & t.bgPattern & & t.bgPattern !== 'none') {
document.addEventListener('DOMContentLoaded', function() {
document.body.classList.add('bg-pattern-' + t.bgPattern);
}, {once:true});
}
} catch(e){}
})();
< / script >
<!-- Per - route favicon. Bookmarking /calendar shows a calendar icon,
/notes a notes icon, etc. Each shape is rendered in the current
theme accent color. Falls back to the boat logo on the root path. -->
< script nonce = "{{CSP_NONCE}}" >
(function(){
try {
var path = (window.location.pathname || '').toLowerCase();
var theme = (function(){ try { return JSON.parse(localStorage.getItem('odysseus-theme')); } catch(_) { return null; } })();
var ac = (theme & & (theme.red || theme.colors & & theme.colors.red)) || '#e06c75';
// Shapes are line-stroke SVGs (no emoji glyphs). Each entry returns
// the inner SVG markup rendered against currentColor=`ac`.
var SHAPES = {
'/calendar':
"< rect x = '4' y = '6' width = '24' height = '22' rx = '2' fill = 'none' stroke = '" + ac + "' stroke-width = '2.5' / > " +
"< line x1 = '4' y1 = '12' x2 = '28' y2 = '12' stroke = '" + ac + "' stroke-width = '2.5' / > " +
"< line x1 = '10' y1 = '3' x2 = '10' y2 = '9' stroke = '" + ac + "' stroke-width = '2.5' stroke-linecap = 'round' / > " +
"< line x1 = '22' y1 = '3' x2 = '22' y2 = '9' stroke = '" + ac + "' stroke-width = '2.5' stroke-linecap = 'round' / > ",
'/notes':
"< rect x = '6' y = '4' width = '20' height = '24' rx = '2' fill = 'none' stroke = '" + ac + "' stroke-width = '2.5' / > " +
"< line x1 = '10' y1 = '10' x2 = '22' y2 = '10' stroke = '" + ac + "' stroke-width = '2' / > " +
"< line x1 = '10' y1 = '15' x2 = '22' y2 = '15' stroke = '" + ac + "' stroke-width = '2' / > " +
"< line x1 = '10' y1 = '20' x2 = '18' y2 = '20' stroke = '" + ac + "' stroke-width = '2' / > ",
'/cookbook':
"< path d = 'M5 8 L5 26 A2 2 0 0 0 7 28 L25 28 A2 2 0 0 0 27 26 L27 8' fill = 'none' stroke = '" + ac + "' stroke-width = '2.5' stroke-linejoin = 'round' / > " +
"< path d = 'M9 4 L23 4 L23 8 L9 8 Z' fill = 'none' stroke = '" + ac + "' stroke-width = '2.5' stroke-linejoin = 'round' / > " +
"< line x1 = '11' y1 = '14' x2 = '21' y2 = '14' stroke = '" + ac + "' stroke-width = '2' / > " +
"< line x1 = '11' y1 = '19' x2 = '17' y2 = '19' stroke = '" + ac + "' stroke-width = '2' / > ",
'/email':
"< rect x = '4' y = '7' width = '24' height = '18' rx = '2' fill = 'none' stroke = '" + ac + "' stroke-width = '2.5' / > " +
"< path d = 'M5 9 L16 17 L27 9' fill = 'none' stroke = '" + ac + "' stroke-width = '2.5' stroke-linecap = 'round' stroke-linejoin = 'round' / > ",
'/memory':
"< path d = 'M16 5 C10 5 6 9 6 14 C6 19 10 21 11 22 L11 26 L21 26 L21 22 C22 21 26 19 26 14 C26 9 22 5 16 5 Z' fill = 'none' stroke = '" + ac + "' stroke-width = '2.5' stroke-linejoin = 'round' / > " +
"< line x1 = '12' y1 = '28' x2 = '20' y2 = '28' stroke = '" + ac + "' stroke-width = '2' / > ",
'/gallery':
"< rect x = '4' y = '4' width = '24' height = '24' rx = '2' fill = 'none' stroke = '" + ac + "' stroke-width = '2.5' / > " +
"< circle cx = '12' cy = '12' r = '2.5' fill = '" + ac + "' / > " +
"< path d = 'M4 22 L11 16 L18 21 L23 17 L28 22' fill = 'none' stroke = '" + ac + "' stroke-width = '2.5' stroke-linejoin = 'round' / > ",
'/tasks':
"< rect x = '4' y = '4' width = '24' height = '24' rx = '3' fill = 'none' stroke = '" + ac + "' stroke-width = '2.5' / > " +
"< path d = 'M9 16 L14 21 L23 11' fill = 'none' stroke = '" + ac + "' stroke-width = '2.5' stroke-linecap = 'round' stroke-linejoin = 'round' / > ",
'/library':
"< rect x = '5' y = '5' width = '5' height = '22' rx = '1' fill = 'none' stroke = '" + ac + "' stroke-width = '2.5' / > " +
"< rect x = '13' y = '5' width = '5' height = '22' rx = '1' fill = 'none' stroke = '" + ac + "' stroke-width = '2.5' / > " +
"< rect x = '21' y = '8' width = '6' height = '19' rx = '1' fill = 'none' stroke = '" + ac + "' stroke-width = '2.5' transform = 'rotate(8 24 17)' / > ",
};
var inner = SHAPES[path];
if (!inner) return; // Root path keeps the default boat icon.
var svg = "< svg xmlns = 'http://www.w3.org/2000/svg' viewBox = '0 0 32 32' > " + inner + "< / svg > ";
var href = 'data:image/svg+xml,' + encodeURIComponent(svg);
var fav = document.querySelector("link[rel='icon']");
if (!fav) { fav = document.createElement('link'); fav.rel = 'icon'; fav.type = 'image/svg+xml'; document.head.appendChild(fav); }
fav.href = href;
var apple = document.querySelector("link[rel='apple-touch-icon']");
if (!apple) { apple = document.createElement('link'); apple.rel = 'apple-touch-icon'; document.head.appendChild(apple); }
apple.href = href;
// Also customize the page title so the bookmark inherits a useful name.
var titles = {
'/calendar': 'Calendar — Odysseus',
'/notes': 'Notes — Odysseus',
'/cookbook': 'Cookbook — Odysseus',
'/email': 'Email — Odysseus',
'/memory': 'Memory — Odysseus',
'/gallery': 'Gallery — Odysseus',
'/tasks': 'Tasks — Odysseus',
'/library': 'Library — Odysseus',
};
if (titles[path]) document.title = titles[path];
// Per-route Android home-screen icon. We swap the < link rel = "manifest" >
// to a per-page Blob URL with this route's SVG icon — that way "Add to
// Home Screen" picks up the route-specific glyph instead of the shared
// boat logo. Falls back silently on browsers without Blob URL support.
try {
if (inner & & typeof Blob !== 'undefined') {
var pwa = {
name: (titles[path] || 'Odysseus'),
short_name: (titles[path] || 'Odysseus').split('—')[0].trim(),
start_url: path,
scope: '/',
display: 'standalone',
background_color: '#0e0e10',
theme_color: ac,
icons: [
{ src: href, sizes: '192x192', type: 'image/svg+xml', purpose: 'any maskable' },
{ src: href, sizes: '512x512', type: 'image/svg+xml', purpose: 'any maskable' },
],
};
var blob = new Blob([JSON.stringify(pwa)], { type: 'application/manifest+json' });
var url = URL.createObjectURL(blob);
var ml = document.querySelector("link[rel='manifest']");
if (!ml) { ml = document.createElement('link'); ml.rel = 'manifest'; document.head.appendChild(ml); }
ml.href = url;
}
} catch(_) {}
} catch(e){}
})();
< / script >
< script defer src = "/static/lib/highlight.min.js" > < / script >
<!-- Inter font — self - hosted, no Google dependencies -->
< style >
@font-face { font-family: 'Inter'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/Inter-Regular.woff2') format('woff2'); }
@font-face { font-family: 'Inter'; font-weight: 500; font-style: normal; font-display: swap; src: url('/static/fonts/Inter-Medium.woff2') format('woff2'); }
@font-face { font-family: 'Inter'; font-weight: 600; font-style: normal; font-display: swap; src: url('/static/fonts/Inter-SemiBold.woff2') format('woff2'); }
< / style >
<!-- KaTeX CSS is loaded with media="print" so it doesn't block render,
then flipped to "all" via JS after load. Mermaid init runs once the
library finishes loading. Both hooks are wired via addEventListener
below (inline onload= attrs are blocked by CSP script-src-attr). -->
< link id = "katex-css" rel = "stylesheet" href = "https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css" media = "print" >
< script async src = "https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js" > < / script >
< script id = "mermaid-script" async src = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js" > < / script >
< script nonce = "{{CSP_NONCE}}" >
(function(){
var k = document.getElementById('katex-css');
if (k) k.addEventListener('load', function(){ k.media = 'all'; }, { once: true });
var m = document.getElementById('mermaid-script');
if (m) m.addEventListener('load', function(){
if (window.odysseusInitMermaid) window.odysseusInitMermaid();
}, { once: true });
})();
< / script >
< link rel = "stylesheet" href = "/static/style.css" >
< link rel = "modulepreload" href = "/static/app.js" >
< link rel = "modulepreload" href = "/static/js/chat.js" >
< link rel = "modulepreload" href = "/static/js/ui.js" >
< link rel = "modulepreload" href = "/static/js/sessions.js" >
< link rel = "modulepreload" href = "/static/js/markdown.js" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0, interactive-widget=resizes-content, viewport-fit=cover" / >
< / head >
< body >
<!-- Loading overlay — hides all flashing until app is ready -->
< div id = "app-loader" style = "position:fixed;inset:0;z-index:99999;background:var(--bg,#282c34);display:flex;align-items:center;justify-content:center;flex-direction:column;gap:8px;transition:opacity .3s" >
< div id = "loader-wave" style = "color:var(--brand-color,var(--red,#e06c75));font-family:monospace;font-size:11px;opacity:.5" > ▁▂▃< / div >
< / div >
< script nonce = "{{CSP_NONCE}}" >
(function(){
var el=document.getElementById('loader-wave');
if(!el)return;
var frames=['▁▂▃','▂▃▄','▃▄▅','▄▅▆','▅▆▅','▆▅▄','▅▄▃','▄▃▂','▃▂▁'];
var i=0;
var iv=setInterval(function(){
if(!document.getElementById('app-loader')){clearInterval(iv);return}
el.textContent=frames[i%frames.length];
i++;
},150);
setTimeout(function(){var l=document.getElementById('app-loader');if(l){l.style.opacity='0';setTimeout(function(){l.remove()},300)}},5000);
})();
< / script >
<!-- Memory Management Modal -->
< div id = "memory-modal" class = "modal hidden" >
Add dialog accessibility semantics
Screen readers got no signal that a dialog opened — not one modal carried
role="dialog" — and several close buttons had no accessible name.
- The 6 static tool windows (Brain, Theme, Prompt, Rename session, Cookbook,
Settings) now carry role="dialog" + an accessible name. They are dockable,
tiling windows, so they are non-modal dialogs (intentionally no aria-modal).
- The four unlabelled close buttons (theme, prompt, cookbook, settings) get an
aria-label so they no longer read as just "heavy multiplication x".
- styledConfirm / styledPrompt ARE blocking modals: they get role="dialog" +
aria-modal="true" + aria-labelledby/aria-describedby, and now manage focus —
restore focus to the triggering element on close and trap Tab within the
dialog (they already moved focus in on open).
tests/test_dialog_aria.py pins the roles, labels, and focus management.
2026-06-01 23:41:25 -04:00
< div class = "modal-content memory-modal-content" role = "dialog" aria-label = "Brain" style = "background:var(--bg)" >
2026-05-31 23:58:26 +09:00
< div class = "modal-header" >
< h4 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:6px" > < path d = "M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z" / > < path d = "M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z" / > < path d = "M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4" / > < / svg > Brain< / h4 >
< button class = "close-btn" id = "close-memory-modal" aria-label = "Close memory modal" > ✖< / button >
< / div >
< div class = "modal-body memory-modal-body" >
< div class = "memory-tabs" >
< button class = "memory-tab active" data-memory-tab = "browse" > < svg width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px" > < path d = "M12 2a7 7 0 0 1 7 7c0 2.4-1.2 4.5-3 5.7V17a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-2.3C6.2 13.5 5 11.4 5 9a7 7 0 0 1 7-7z" / > < line x1 = "10" y1 = "22" x2 = "14" y2 = "22" / > < / svg > Memories < span id = "memory-count" class = "memory-count" style = "font-size:0.8em;opacity:0.6;font-weight:normal;margin-left:4px" > 0< / span > < / button >
< button class = "memory-tab" data-memory-tab = "skills" > < svg width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px" > < polygon points = "13 2 3 14 12 14 11 22 21 10 12 10 13 2" / > < / svg > Skills < span id = "skills-count" class = "memory-count" style = "font-size:0.8em;opacity:0.6;font-weight:normal;margin-left:4px" > 0< / span > < / button >
< button class = "memory-tab" data-memory-tab = "add" > < svg width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px" > < circle cx = "12" cy = "12" r = "10" / > < line x1 = "12" y1 = "8" x2 = "12" y2 = "16" / > < line x1 = "8" y1 = "12" x2 = "16" y2 = "12" / > < / svg > Add< / button >
< button class = "memory-tab" data-memory-tab = "settings" > < svg width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px" > < circle cx = "12" cy = "12" r = "3" / > < path d = "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" / > < / svg > Settings< / button >
< / div >
<!-- ── Browse tab ── -->
< div class = "memory-tab-panel" data-memory-panel = "browse" >
< div class = "admin-card" style = "display:flex;flex-direction:column;overflow:hidden;flex:1;min-height:0;" >
< div style = "display:flex;align-items:center;gap:8px;margin-bottom:2px;" >
< h2 style = "display:flex;align-items:center;gap:6px;margin:0;padding:0;line-height:1;" > Memories < span id = "memory-count-h2" class = "memory-count" style = "font-size:0.6em;opacity:0.6;font-weight:normal" > < / span > < / h2 >
< span style = "flex:1" > < / span >
< label class = "admin-switch" title = "Include memories in chat context" > < input type = "checkbox" id = "memory-enabled-header-toggle" checked / > < span class = "admin-slider" > < / span > < / label >
< / div >
< p class = "memory-desc doclib-desc" style = "margin-top:6px;" > Long-term facts the AI remembers across chats — recall, edit, or curate.< / p >
< div class = "memory-toolbar" >
< div class = "memory-toolbar-row" >
< select id = "memory-sort" class = "memory-sort-select" >
< option value = "newest" > Newest< / option >
< option value = "oldest" > Oldest< / option >
< option value = "alpha" > A-Z< / option >
< option value = "uses" > Most used< / option >
< / select >
< button id = "memory-select-btn" class = "memory-toolbar-btn" title = "Select multiple memories" > Select< / button >
< button id = "memory-tidy-btn" class = "memory-toolbar-btn" title = "AI tidy: deduplicate and clean up memories" > < svg width = "11" height = "11" viewBox = "0 0 24 24" fill = "currentColor" style = "vertical-align:-1px;margin-right:2px;" > < path d = "M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z" / > < / svg > Tidy< / button >
< / div >
< input type = "text" id = "memory-search" placeholder = "Search memories…" class = "memory-search-input" / >
< div id = "memory-category-filters" class = "memory-category-filters" >
< button class = "memory-cat-chip active" data-cat = "all" > all< / button >
< / div >
< / div >
< div id = "memory-bulk-bar" class = "memory-bulk-bar hidden" >
< label class = "memory-bulk-check-all" style = "position:relative;top:0px;" > < input type = "checkbox" id = "memory-select-all" / > All< / label >
< span id = "memory-selected-count" > 0 Selected< / span >
< button id = "memory-bulk-delete" class = "memory-toolbar-btn danger" disabled > < svg width = "11" height = "11" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-1px;margin-right:3px;" > < polyline points = "3 6 5 6 21 6" / > < path d = "M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" / > < path d = "M10 11v6" / > < path d = "M14 11v6" / > < / svg > Delete< / button >
< button id = "memory-bulk-cancel" class = "memory-toolbar-btn" title = "Cancel (Esc)" style = "margin-left:4px;padding:3px 6px;" > < svg width = "11" height = "11" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" > < line x1 = "18" y1 = "6" x2 = "6" y2 = "18" / > < line x1 = "6" y1 = "6" x2 = "18" y2 = "18" / > < / svg > < / button >
< / div >
< div id = "memory-list" class = "memory-list" > < / div >
< div id = "memory-suggestions-body" class = "memory-suggestions hidden" > < / div >
< / div >
< / div >
<!-- ── Add tab ── -->
< div class = "memory-tab-panel hidden" data-memory-panel = "add" >
< div class = "admin-card" >
< div style = "display:flex;align-items:center;gap:8px;margin-bottom:2px;" >
< h2 style = "margin:0;padding:0;line-height:1;" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:6px" > < path d = "M12 2a7 7 0 0 1 7 7c0 2.4-1.2 4.5-3 5.7V17a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-2.3C6.2 13.5 5 11.4 5 9a7 7 0 0 1 7-7z" / > < line x1 = "10" y1 = "22" x2 = "14" y2 = "22" / > < / svg > Add Memory< / h2 >
< span style = "flex:1" > < / span >
< button id = "memory-import-btn" class = "theme-io-btn" title = "Import memories from a file" style = "height:26px;font-size:12px;" > < svg width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:4px;" > < path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" / > < polyline points = "7 10 12 15 17 10" / > < line x1 = "12" y1 = "15" x2 = "12" y2 = "3" / > < / svg > Import< / button >
< button id = "memory-export-btn" class = "theme-io-btn" title = "Export all memories as JSON" style = "height:26px;font-size:12px;" > < svg width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:4px;" > < path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" / > < polyline points = "17 8 12 3 7 8" / > < line x1 = "12" y1 = "3" x2 = "12" y2 = "15" / > < / svg > Export< / button >
< input type = "file" id = "memory-import-file" accept = ".txt,.md,.pdf,.csv,.log,.json,.py,.js,.html" hidden / >
< / div >
< p class = "memory-desc doclib-desc" style = "margin:4px 0 6px;" >
2026-06-01 15:32:17 +02:00
Import a < code > .txt< / code > , < code > .md< / code > , < code > .pdf< / code > , < code > .csv< / code > , < code > .log< / code > , < code > .json< / code > , < code > .py< / code > , < code > .js< / code > , or < code > .html< / code > file — the AI reads it and suggests candidate memories you can approve.
2026-05-31 23:58:26 +09:00
< / p >
< div class = "memory-add-row" style = "margin-top:8px;" >
< div class = "skill-ph-wrap" style = "flex:1;min-width:0;" >
< input type = "text" id = "new-memory-input" placeholder = " " class = "memory-add-input skill-hint-input" / >
< span class = "skill-rich-ph" > < span class = "k" > Add a memory< / span > — e.g. 'I prefer concise replies' < svg class = "k" width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-left:4px;" aria-hidden = "true" > < polyline points = "9 10 4 15 9 20" / > < path d = "M20 4v7a4 4 0 0 1-4 4H4" / > < / svg > < / span >
< / div >
< / div >
< / div >
< div class = "admin-card" >
< div style = "display:flex;align-items:baseline;gap:8px;margin-bottom:2px;" >
< h2 style = "margin:0;padding:0;line-height:1;" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:6px" > < polygon points = "13 2 3 14 12 14 11 22 21 10 12 10 13 2" / > < / svg > Add Skill< / h2 >
< / div >
< p class = "memory-desc doclib-desc" style = "margin-top:6px;" > Create a skill by hand — title, what it solves, and an approach.< / p >
< div class = "skill-ph-wrap" style = "margin-top:4px;margin-bottom:6px;" >
< input type = "text" id = "new-skill-title" placeholder = " " class = "memory-add-input skill-hint-input" / >
< span class = "skill-rich-ph" > < span class = "k" > Title< / span > — short name, e.g. “build-vllm-wheel”< / span >
< / div >
< div class = "skill-ph-wrap" style = "margin-bottom:6px;" >
< input type = "text" id = "new-skill-problem" placeholder = " " class = "memory-add-input skill-hint-input" / >
< span class = "skill-rich-ph" > < span class = "k" > When to use< / span > — what problem does this skill solve?< / span >
< / div >
< div class = "skill-ph-wrap" style = "margin-bottom:6px;" >
< textarea id = "new-skill-solution" placeholder = " " class = "memory-add-input skill-hint-input" rows = "2" style = "resize:vertical;" > < / textarea >
< span class = "skill-rich-ph skill-rich-ph-top" > < span class = "k" > How< / span > — the approach, steps, commands, or rules to follow< / span >
< / div >
< div class = "skill-ph-wrap" style = "margin-bottom:8px;" >
< input type = "text" id = "new-skill-tags" placeholder = " " class = "memory-add-input skill-hint-input" / >
< span class = "skill-rich-ph" > < span class = "k" > Tags< / span > — comma-separated, e.g. python, build, vllm< / span >
< / div >
< div style = "display:flex;justify-content:flex-end;" >
< button id = "add-skill-btn" class = "memory-toolbar-btn" > Add Skill< / button >
< / div >
< / div >
< / div >
<!-- ── Skills tab ── -->
< div class = "memory-tab-panel hidden" data-memory-panel = "skills" >
< div class = "admin-card" style = "display:flex;flex-direction:column;overflow:hidden;flex:1;min-height:0;" >
< div style = "display:flex;align-items:center;gap:8px;margin-bottom:2px;" >
< h2 style = "margin:0;padding:0;line-height:1;" > Skills < span id = "skills-count-h2" class = "memory-count" style = "font-size:0.6em;opacity:0.6;font-weight:normal" > < / span > < / h2 >
< span style = "flex:1" > < / span >
< label class = "admin-switch" title = "Inject relevant skills into chat context" > < input type = "checkbox" id = "skills-enabled-header-toggle" checked / > < span class = "admin-slider" > < / span > < / label >
< / div >
< p class = "memory-desc doclib-desc" style = "margin-top:6px;" > Reusable procedures the AI can call via /skill — sort by confidence to surface the proven ones.< / p >
< div class = "memory-toolbar" >
< div class = "memory-toolbar-row" >
< select id = "skills-sort" class = "memory-sort-select" >
< optgroup label = "Sort" >
< option value = "sort:confidence" > Confidence< / option >
< option value = "sort:uses" > Most used< / option >
< option value = "sort:alpha" > A-Z< / option >
< option value = "sort:recent" > Recent< / option >
< / optgroup >
< optgroup label = "Filter" >
< option value = "filter:all" > All skills< / option >
< option value = "filter:drafts" > Drafts only< / option >
< option value = "filter:published" > Published only< / option >
< option value = "filter:conf95" > Confidence ≤ 95%< / option >
< option value = "filter:conf90" > Confidence ≤ 90%< / option >
< option value = "filter:conf85" > Confidence ≤ 85%< / option >
< option value = "filter:conf80" > Confidence ≤ 80%< / option >
< option value = "filter:conf75" > Confidence ≤ 75%< / option >
< option value = "filter:conf70" > Confidence ≤ 70%< / option >
< / optgroup >
< / select >
< button id = "skills-select-btn" class = "memory-toolbar-btn" title = "Select multiple skills" > Select< / button >
< button id = "skills-audit-btn" class = "memory-toolbar-btn" title = "Test every skill, auto-fix the weak ones, flag what still fails" > < svg width = "11" height = "11" viewBox = "0 0 24 24" fill = "currentColor" style = "vertical-align:-1px;margin-right:3px;" > < path d = "M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z" / > < / svg > Audit all< / button >
< / div >
< input type = "text" id = "skills-search" placeholder = "Search skills…" class = "memory-search-input" / >
< / div >
< div id = "skills-audit-panel" class = "skills-audit-panel hidden" > < / div >
< div id = "skills-bulk-bar" class = "memory-bulk-bar hidden" >
< label class = "memory-bulk-check-all" style = "position:relative;top:0px;" > < input type = "checkbox" id = "skills-select-all" / > All< / label >
< span id = "skills-selected-count" > 0 Selected< / span >
< button id = "skills-bulk-publish" class = "memory-toolbar-btn" disabled title = "Publish selected drafts" style = "margin-left:auto;" > < svg width = "11" height = "11" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-1px;margin-right:3px;" > < polyline points = "20 6 9 17 4 12" / > < / svg > Approve< / button >
< button id = "skills-bulk-audit" class = "memory-toolbar-btn" disabled title = "Audit selected draft skills" > < svg width = "11" height = "11" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-1px;margin-right:3px;" > < polygon points = "5 3 19 12 5 21 5 3" / > < / svg > Audit< / button >
< button id = "skills-bulk-delete-nonpassing" class = "memory-toolbar-btn danger" disabled title = "Delete selected duplicates, generic/irrelevant skills, failed audits, and skills below threshold" > < svg width = "11" height = "11" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-1px;margin-right:3px;" > < path d = "M3 6h18" / > < path d = "M8 6V4h8v2" / > < path d = "M19 6l-1 14H6L5 6" / > < path d = "M10 11l4 4" / > < path d = "M14 11l-4 4" / > < / svg > Delete non passing< / button >
< button id = "skills-bulk-delete" class = "memory-toolbar-btn danger" disabled > < svg width = "11" height = "11" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-1px;margin-right:3px;" > < polyline points = "3 6 5 6 21 6" / > < path d = "M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" / > < path d = "M10 11v6" / > < path d = "M14 11v6" / > < / svg > Delete< / button >
< button id = "skills-bulk-cancel" class = "memory-toolbar-btn" title = "Cancel (Esc)" style = "padding:3px 6px;" > < svg width = "11" height = "11" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" > < line x1 = "18" y1 = "6" x2 = "6" y2 = "18" / > < line x1 = "6" y1 = "6" x2 = "18" y2 = "18" / > < / svg > < / button >
< / div >
< div id = "skills-list" class = "memory-list" > < / div >
< / div >
< / div >
<!-- ── Settings tab ── -->
< div class = "memory-tab-panel hidden" data-memory-panel = "settings" >
< div class = "admin-card" >
< div style = "display:flex;align-items:center;justify-content:space-between;gap:12px" >
< h2 style = "margin:0" > Auto-extract memories< / h2 >
< label class = "admin-switch" style = "flex-shrink:0" > < input type = "checkbox" id = "auto-memory-toggle" checked / > < span class = "admin-slider" > < / span > < / label >
< / div >
< span class = "admin-toggle-sub" style = "display:block;margin-top:6px" > Automatically extract memories from conversations.< / span >
< / div >
< div class = "admin-card" >
< div style = "display:flex;align-items:center;justify-content:space-between;gap:12px" >
< h2 style = "margin:0" > Auto-extract skills< / h2 >
< label class = "admin-switch" style = "flex-shrink:0" > < input type = "checkbox" id = "auto-skills-toggle" / > < span class = "admin-slider" > < / span > < / label >
< / div >
< span class = "admin-toggle-sub" style = "display:block;margin-top:6px;opacity:0.6" > Automatically draft reusable skills from your workflows. Audit all can publish passing skills using the threshold below.< / span >
< span class = "admin-toggle-sub" style = "display:block;margin-top:6px;opacity:0.6" > The library can grow; cleanup retires weak/duplicate skills only after review.< / span >
< / div >
< div class = "admin-card" >
< div style = "display:flex;align-items:center;justify-content:space-between;gap:12px" >
< h2 style = "margin:0" > Inject Skills< / h2 >
< / div >
< span class = "admin-toggle-sub" style = "display:block;margin-top:6px;opacity:0.6" > Controls how many relevant published or approved skills are added to each agent request.< / span >
< div style = "display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:8px" >
< span class = "admin-toggle-sub" style = "margin:0" > Max skills per request< / span >
< input type = "number" id = "skill-max-input" min = "0" max = "12" step = "1" value = "3" style = "flex-shrink:0;width:72px;background:var(--input-bg,var(--panel));color:var(--fg);border:1px solid var(--border);border-radius:6px;padding:4px 6px;font-size:12px;text-align:right;font-variant-numeric:tabular-nums" / >
< / div >
< span class = "admin-toggle-sub" style = "display:block;margin-top:6px;opacity:0.5" > Set to 0 to disable skill injection.< / span >
< / div >
< div class = "admin-card" >
< div style = "display:flex;align-items:center;justify-content:space-between;gap:12px" >
< h2 style = "margin:0" > Auto-approve skills< / h2 >
< label class = "admin-switch" style = "flex-shrink:0" > < input type = "checkbox" id = "auto-approve-skills-toggle" checked / > < span class = "admin-slider" > < / span > < / label >
< / div >
< span class = "admin-toggle-sub" style = "display:block;margin-top:6px;opacity:0.6" > Audit all publishes passing, necessary skills at or above this confidence. Off = keep audit results as drafts unless manually approved.< / span >
< div style = "display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:6px" >
< span class = "admin-toggle-sub" style = "margin:0" > Minimum confidence< / span >
< span style = "display:flex;align-items:center;gap:8px;flex-shrink:0" >
< span id = "skill-confidence-label" class = "admin-toggle-sub" style = "margin:0;min-width:42px;text-align:right;font-variant-numeric:tabular-nums" > ≥ 85%< / span >
< input type = "range" id = "skill-confidence-slider" class = "preset-range" min = "50" max = "100" step = "5" value = "85" style = "width:120px;margin:0" / >
< / span >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- Theme Popup (floating panel) -->
< div id = "theme-modal" class = "modal hidden" >
Add dialog accessibility semantics
Screen readers got no signal that a dialog opened — not one modal carried
role="dialog" — and several close buttons had no accessible name.
- The 6 static tool windows (Brain, Theme, Prompt, Rename session, Cookbook,
Settings) now carry role="dialog" + an accessible name. They are dockable,
tiling windows, so they are non-modal dialogs (intentionally no aria-modal).
- The four unlabelled close buttons (theme, prompt, cookbook, settings) get an
aria-label so they no longer read as just "heavy multiplication x".
- styledConfirm / styledPrompt ARE blocking modals: they get role="dialog" +
aria-modal="true" + aria-labelledby/aria-describedby, and now manage focus —
restore focus to the triggering element on close and trap Tab within the
dialog (they already moved focus in on open).
tests/test_dialog_aria.py pins the roles, labels, and focus management.
2026-06-01 23:41:25 -04:00
< div id = "theme-popup" class = "modal-content admin-modal-content" role = "dialog" aria-label = "Theme" style = "background:var(--bg)" >
2026-05-31 23:58:26 +09:00
< div class = "modal-header theme-popup-header" id = "theme-popup-header" >
< h4 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:6px" > < circle cx = "12" cy = "12" r = "10" / > < path d = "M12 2a7 7 0 0 0 0 20 4 4 0 0 1 0-8 4 4 0 0 0 0-8" / > < circle cx = "8" cy = "9" r = "1.5" fill = "currentColor" / > < circle cx = "15" cy = "14" r = "1.5" fill = "currentColor" / > < circle cx = "9" cy = "15" r = "1.5" fill = "currentColor" / > < / svg > Theme< / h4 >
< button type = "button" class = "theme-opacity-wrap theme-opacity-toggle hidden" id = "theme-opacity-wrap" title = "Fade this window to preview the page behind it" aria-pressed = "false" >
< svg width = "11" height = "11" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" aria-hidden = "true" > < path d = "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" / > < circle cx = "12" cy = "12" r = "3" / > < / svg >
< span class = "theme-opacity-label" > Peek< / span >
< / button >
Add dialog accessibility semantics
Screen readers got no signal that a dialog opened — not one modal carried
role="dialog" — and several close buttons had no accessible name.
- The 6 static tool windows (Brain, Theme, Prompt, Rename session, Cookbook,
Settings) now carry role="dialog" + an accessible name. They are dockable,
tiling windows, so they are non-modal dialogs (intentionally no aria-modal).
- The four unlabelled close buttons (theme, prompt, cookbook, settings) get an
aria-label so they no longer read as just "heavy multiplication x".
- styledConfirm / styledPrompt ARE blocking modals: they get role="dialog" +
aria-modal="true" + aria-labelledby/aria-describedby, and now manage focus —
restore focus to the triggering element on close and trap Tab within the
dialog (they already moved focus in on open).
tests/test_dialog_aria.py pins the roles, labels, and focus management.
2026-06-01 23:41:25 -04:00
< button class = "close-btn" id = "close-theme-popup" aria-label = "Close theme" > ✖ < / button >
2026-05-31 23:58:26 +09:00
< / div >
<!-- Theme tabs -->
< div class = "admin-tabs" id = "theme-tabs" >
< button class = "admin-tab active" data-tab = "theme-tab-browse" > < svg width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px" > < circle cx = "12" cy = "12" r = "10" / > < path d = "M12 2a7 7 0 0 0 0 20 4 4 0 0 1 0-8 4 4 0 0 0 0-8" / > < circle cx = "8" cy = "9" r = "1.4" fill = "currentColor" / > < circle cx = "15" cy = "14" r = "1.4" fill = "currentColor" / > < circle cx = "9" cy = "15" r = "1.4" fill = "currentColor" / > < / svg > Themes< / button >
< button class = "admin-tab" data-tab = "theme-tab-customize" > < svg width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px" > < path d = "M9.06 11.9l-3.5 3.5a2.85 2.85 0 1 0 4.03 4.03l8.49-8.49a4.5 4.5 0 1 0-6.36-6.36L3.18 12.62" / > < path d = "M14 7l3 3" / > < / svg > Customize< / button >
< / div >
<!-- Tab: Browse themes -->
< div id = "theme-tab-browse" class = "theme-tab-panel" >
< div class = "admin-card" >
< h2 > Default Themes< / h2 >
< div class = "theme-grid" id = "themeGrid" > < / div >
< / div >
< div class = "admin-card" id = "themeUserCard" style = "display:none" >
< h2 > Your Themes< / h2 >
< div class = "theme-grid" id = "themeUserGrid" > < / div >
< / div >
< / div >
<!-- Tab: Customize -->
< div id = "theme-tab-customize" class = "theme-tab-panel" style = "display:none" >
< div class = "admin-card" >
< h2 > Colors< / h2 >
< div class = "theme-custom" id = "themeCustom" >
< div class = "color-row" > < label > Background< / label > < input type = "color" id = "clr-bg" > < button class = "color-reset-btn" data-reset = "bg" title = "Reset this color" > ↺ < / button > < / div >
< div class = "color-row" > < label > Text< / label > < input type = "color" id = "clr-fg" > < button class = "color-reset-btn" data-reset = "fg" title = "Reset this color" > ↺ < / button > < / div >
< div class = "color-row" > < label > Panel< / label > < input type = "color" id = "clr-panel" > < button class = "color-reset-btn" data-reset = "panel" title = "Reset this color" > ↺ < / button > < / div >
< div class = "color-row" > < label > Sidebar< / label > < input type = "color" id = "adv-sidebarBg" > < button class = "color-reset-btn" data-reset-adv = "sidebarBg" title = "Reset this color" > ↺ < / button > < / div >
< div class = "color-row" > < label > Border< / label > < input type = "color" id = "clr-border" > < button class = "color-reset-btn" data-reset = "border" title = "Reset this color" > ↺ < / button > < / div >
< div class = "color-row" > < label > Accent< / label > < input type = "color" id = "clr-red" > < button class = "color-reset-btn" data-reset = "red" title = "Reset this color" > ↺ < / button > < / div >
< / div >
< / div >
< div class = "theme-adv-toggle" id = "theme-adv-toggle" >
< span class = "theme-adv-arrow" > ▶ < / span > More Colors
< / div >
< div class = "theme-adv-section hidden" id = "themeAdvanced" >
< div class = "theme-adv-group" >
< div class = "theme-adv-group-label" > Chat Bubbles< / div >
< div class = "theme-custom" >
< div class = "color-row" > < label > User Chat Bubble< / label > < input type = "color" id = "adv-userBubbleBg" > < button class = "color-reset-btn" data-reset-adv = "userBubbleBg" title = "Reset this color" > ↺ < / button > < / div >
< div class = "color-row" > < label > AI Chat Bubble< / label > < input type = "color" id = "adv-aiBubbleBg" > < button class = "color-reset-btn" data-reset-adv = "aiBubbleBg" title = "Reset this color" > ↺ < / button > < / div >
< div class = "color-row" > < label > Border Chat Bubble< / label > < input type = "color" id = "adv-bubbleBorder" > < button class = "color-reset-btn" data-reset-adv = "bubbleBorder" title = "Reset this color" > ↺ < / button > < / div >
< / div >
< / div >
< div class = "theme-adv-group" >
< div class = "theme-adv-group-label" > Sidebar< / div >
< div class = "theme-custom" >
< div class = "color-row" > < label > Odysseus Logo< / label > < input type = "color" id = "adv-brandColor" > < button class = "color-reset-btn" data-reset-adv = "brandColor" title = "Reset this color" > ↺ < / button > < / div >
< div class = "color-row" > < label title = "Hamburger menu" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" style = "vertical-align:-2px;" > < line x1 = "3" y1 = "6" x2 = "21" y2 = "6" / > < line x1 = "3" y1 = "12" x2 = "21" y2 = "12" / > < line x1 = "3" y1 = "18" x2 = "21" y2 = "18" / > < / svg > < / label > < input type = "color" id = "adv-hamburgerColor" > < button class = "color-reset-btn" data-reset-adv = "hamburgerColor" title = "Reset this color" > ↺ < / button > < / div >
< / div >
< / div >
< div class = "theme-adv-group" >
< div class = "theme-adv-group-label" > Chat Input / Prompt Area< / div >
< div class = "theme-custom" >
< div class = "color-row" > < label > Input Bg< / label > < input type = "color" id = "adv-inputBg" > < button class = "color-reset-btn" data-reset-adv = "inputBg" title = "Reset this color" > ↺ < / button > < / div >
< div class = "color-row" > < label > Input Border< / label > < input type = "color" id = "adv-inputBorder" > < button class = "color-reset-btn" data-reset-adv = "inputBorder" title = "Reset this color" > ↺ < / button > < / div >
< div class = "color-row" > < label > Send Btn< / label > < input type = "color" id = "adv-sendBtnBg" > < button class = "color-reset-btn" data-reset-adv = "sendBtnBg" title = "Reset this color" > ↺ < / button > < / div >
< div class = "color-row" > < label > Send Hover< / label > < input type = "color" id = "adv-sendBtnHover" > < button class = "color-reset-btn" data-reset-adv = "sendBtnHover" title = "Reset this color" > ↺ < / button > < / div >
< / div >
< / div >
< div class = "theme-adv-group" >
< div class = "theme-adv-group-label" > Code Blocks< / div >
< div class = "theme-custom" >
< div class = "color-row" > < label > Code Bg< / label > < input type = "color" id = "adv-codeBg" > < button class = "color-reset-btn" data-reset-adv = "codeBg" title = "Reset this color" > ↺ < / button > < / div >
< div class = "color-row" > < label > Code Text< / label > < input type = "color" id = "adv-codeFg" > < button class = "color-reset-btn" data-reset-adv = "codeFg" title = "Reset this color" > ↺ < / button > < / div >
< / div >
< / div >
< div class = "theme-adv-group" >
< div class = "theme-adv-group-label" > Controls< / div >
< div class = "theme-custom" >
< div class = "color-row" > < label > Toggle On< / label > < input type = "color" id = "adv-toggleActive" > < button class = "color-reset-btn" data-reset-adv = "toggleActive" title = "Reset this color" > ↺ < / button > < / div >
< / div >
< / div >
< div class = "theme-adv-group" >
< div class = "theme-adv-group-label" > Custom Fonts< / div >
< div class = "theme-fd-label" style = "opacity:0.4;" > Drop < code > .woff2< / code > , < code > .ttf< / code > , or < code > .otf< / code > files into < code > static/fonts/custom/< / code > and reload — they'll appear in the Font dropdown above.< / div >
< / div >
< button class = "theme-adv-clear-btn" id = "theme-adv-clear" > Clear Advanced Overrides< / button >
< / div >
<!-- Color Harmony Generator (its own card) -->
< div class = "admin-card" id = "theme-harmony-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:6px" > < circle cx = "13.5" cy = "6.5" r = "2.5" / > < circle cx = "19" cy = "13" r = "2.5" / > < circle cx = "6" cy = "12" r = "2.5" / > < circle cx = "10" cy = "20" r = "2.5" / > < / svg > Color Harmony< / h2 >
< div class = "theme-harmony-row" >
< div class = "theme-fd-group" >
< label class = "theme-fd-label" > Accent Color< / label >
< div class = "color-row" style = "justify-content:flex-start;gap:8px;" >
< input type = "color" id = "harmony-accent" value = "#e06c75" >
< span id = "harmony-accent-hex" style = "font-size:11px;opacity:0.65;font-family:ui-monospace,monospace;letter-spacing:0.02em;" > #e06c75< / span >
< / div >
< / div >
< div class = "theme-fd-group" >
< label class = "theme-fd-label" > Harmony< / label >
< select id = "harmony-type" class = "theme-fd-select" >
< option value = "complementary" > Complementary< / option >
< option value = "analogous" > Analogous< / option >
< option value = "triadic" > Triadic< / option >
< option value = "monochromatic" > Monochromatic< / option >
< / select >
< / div >
< / div >
< div class = "theme-harmony-row" style = "margin-top:6px;" >
< div class = "theme-fd-group" >
< label class = "theme-fd-label" > Mode< / label >
< select id = "harmony-mode" class = "theme-fd-select" >
< option value = "dark" > Dark< / option >
< option value = "light" > Light< / option >
< / select >
< / div >
< div class = "theme-fd-group" style = "justify-content:flex-end;" >
< button id = "harmony-generate-btn" class = "harmony-generate-btn" > Generate< / button >
< / div >
< / div >
< div id = "harmony-preview" class = "harmony-preview" > < / div >
< / div >
< div class = "admin-card" >
< h2 > Font & Layout< / h2 >
< div class = "theme-fd-row" >
< div class = "theme-fd-group" >
< label class = "theme-fd-label" > Font< / label >
< select id = "theme-font-select" class = "theme-fd-select" >
< option value = "mono" > Monospace< / option >
< option value = "sans" > Sans-serif< / option >
< option value = "serif" > Serif< / option >
< / select >
< / div >
< div class = "theme-fd-group" >
< label class = "theme-fd-label" > Density< / label >
< select id = "theme-density-select" class = "theme-fd-select" >
< option value = "compact" > Compact< / option >
< option value = "comfortable" > Comfortable< / option >
< option value = "spacious" > Spacious< / option >
< / select >
< / div >
< div class = "theme-fd-group" id = "theme-frosted-group" >
< label class = "theme-fd-label" for = "theme-frosted-toggle" > Frosted< / label >
< label class = "admin-switch" style = "margin-top:4px;" >
< input type = "checkbox" id = "theme-frosted-toggle" >
< span class = "admin-slider" > < / span >
< / label >
< / div >
< / div >
< div class = "theme-fd-row" >
< div class = "theme-fd-group" style = "flex:1 1 0;" >
< label class = "theme-fd-label" > Background / Effect< / label >
< select id = "theme-bg-pattern-select" class = "theme-fd-select" >
< option value = "none" > Solid< / option >
< option value = "dots" > Dots< / option >
< option value = "synapse" > Synapse< / option >
< option value = "rain" > Rain< / option >
< option value = "constellations" > Constellations< / option >
< option value = "perlin-flow" > Perlin Flow< / option >
< option value = "petals" > Petals< / option >
< option value = "sparkles" > Sparkles< / option >
< option value = "embers" > Embers< / option >
< / select >
< / div >
< div class = "theme-fd-group" style = "flex:0 0 auto;align-items:center;" >
< label class = "theme-fd-label" > < / label >
< div class = "color-row" style = "margin:0;padding:0;" >
< input type = "color" id = "theme-bg-effect-color" value = "#9cdef2" title = "Effect color" >
< button class = "color-reset-btn" data-reset-effect title = "Reset to text color" > ↺ < / button >
< / div >
< / div >
< / div >
< div class = "theme-fd-row" >
< div class = "theme-fd-group" id = "theme-bg-intensity-group" style = "flex:1 1 0;" >
< label class = "theme-fd-label" > Intensity< / label >
< input type = "range" id = "theme-bg-intensity" class = "theme-fd-range" min = "0" max = "100" step = "5" value = "100" >
< / div >
< div class = "theme-fd-group" id = "theme-bg-size-group" style = "flex:1 1 0;" >
< label class = "theme-fd-label" > Size< / label >
< input type = "range" id = "theme-bg-size" class = "theme-fd-range" min = "30" max = "250" step = "10" value = "100" >
< / div >
< / div >
< / div >
< div class = "admin-card" style = "margin-top:8px;" >
< h2 > Save / Share< / h2 >
< div class = "theme-save-row" id = "theme-save-row" >
< input type = "text" id = "theme-save-name" placeholder = "Theme name..." maxlength = "32" >
< button id = "theme-save-go" > Save< / button >
< / div >
< div class = "theme-save-error" id = "theme-save-error" > < / div >
< div class = "theme-io-row" style = "margin-top:6px;" >
< button id = "theme-import-btn" class = "theme-io-btn" title = "Import a theme from JSON" > ⤒ Import< / button >
< button id = "theme-export-btn" class = "theme-io-btn" title = "Export current colors as JSON" > ⤓ Export< / button >
< / div >
< textarea id = "theme-import-area" class = "theme-import-area hidden" placeholder = "Paste theme JSON here..." rows = "3" > < / textarea >
< div class = "theme-import-actions hidden" id = "theme-import-actions" >
< button id = "theme-import-go" class = "theme-io-btn" > Apply< / button >
< button id = "theme-import-cancel" class = "theme-io-btn" > Cancel< / button >
< / div >
< / div >
< button id = "theme-reset-btn" > Reset to Default< / button >
< / div >
< / div >
< / div >
< div id = "mobile-backdrop" > < / div >
< button id = "mobile-menu-btn" aria-label = "Toggle sidebar" > ☰ < / button >
< button class = "hamburger-btn" id = "hamburger-btn" title = "Show sidebar" > < svg width = "18" height = "18" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" > < line x1 = "3" y1 = "6" x2 = "21" y2 = "6" / > < line x1 = "3" y1 = "12" x2 = "21" y2 = "12" / > < line x1 = "3" y1 = "18" x2 = "21" y2 = "18" / > < / svg > < / button >
<!-- new session button removed — use + in send button instead -->
< div class = "icon-rail" id = "icon-rail" >
< div class = "rail-resize-handle" id = "rail-resize-handle" > < / div >
<!-- Static: core actions -->
< button class = "icon-rail-btn" id = "rail-search-btn" title = "Search conversations (Ctrl+K)" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" > < circle cx = "10" cy = "10" r = "7" / > < path d = "M21 21l-4.35-4.35" / > < / svg > < / button >
< button class = "icon-rail-btn rail-new-chat" id = "rail-new-session" title = "New chat" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" > < line x1 = "12" y1 = "5" x2 = "12" y2 = "19" / > < line x1 = "5" y1 = "12" x2 = "19" y2 = "12" / > < / svg > < / button >
< button class = "icon-rail-btn" id = "rail-delete-session" title = "Delete session" > ✕ < / button >
< div class = "rail-separator" > < / div >
<!-- Dynamic contextual indicators (shown only while active) -->
< button class = "icon-rail-btn rail-dynamic" id = "rail-chats" title = "Chat ready" style = "display:none" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" / > < / svg > < / button >
< button class = "icon-rail-btn rail-dynamic" id = "rail-documents" title = "Documents" style = "display:none" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" / > < polyline points = "14 2 14 8 20 8" / > < line x1 = "8" y1 = "13" x2 = "16" y2 = "13" / > < line x1 = "8" y1 = "17" x2 = "13" y2 = "17" / > < / svg > < / button >
<!-- Tool launchers — always visible, alphabetical -->
< button class = "icon-rail-btn" id = "rail-calendar" title = "Calendar" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect x = "3" y = "4" width = "18" height = "18" rx = "2" / > < line x1 = "16" y1 = "2" x2 = "16" y2 = "6" / > < line x1 = "8" y1 = "2" x2 = "8" y2 = "6" / > < line x1 = "3" y1 = "10" x2 = "21" y2 = "10" / > < / svg > < / button >
< button class = "icon-rail-btn" id = "rail-compare" title = "Compare" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < circle cx = "18" cy = "18" r = "3" / > < circle cx = "6" cy = "6" r = "3" / > < path d = "M13 6h3a2 2 0 0 1 2 2v7" / > < path d = "M11 18H8a2 2 0 0 1-2-2V9" / > < / svg > < / button >
< button class = "icon-rail-btn" id = "rail-cookbook" title = "Cookbook" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "1.4" stroke-linecap = "round" stroke-linejoin = "round" style = "opacity:0.7" > < path d = "M12 7v14" / > < path d = "M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" / > < / svg > < / button >
< button class = "icon-rail-btn" id = "rail-research" title = "Deep Research" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < circle cx = "11" cy = "11" r = "8" / > < path d = "M21 21l-4.35-4.35" / > < line x1 = "11" y1 = "8" x2 = "11" y2 = "14" / > < line x1 = "8" y1 = "11" x2 = "14" y2 = "11" / > < / svg > < / button >
< button class = "icon-rail-btn" id = "rail-email" title = "Email" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect x = "2" y = "4" width = "20" height = "16" rx = "2" / > < path d = "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" / > < / svg > < / button >
< button class = "icon-rail-btn" id = "rail-gallery" title = "Gallery" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect x = "3" y = "3" width = "18" height = "18" rx = "2" / > < circle cx = "8.5" cy = "8.5" r = "1.5" / > < path d = "M21 15l-5-5L5 21" / > < / svg > < / button >
< button class = "icon-rail-btn" id = "rail-archive" title = "Library" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M4 19.5A2.5 2.5 0 0 1 6.5 17H20" / > < path d = "M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" / > < path d = "M9 7h6M9 11h4" / > < / svg > < / button >
< button class = "icon-rail-btn" id = "rail-memory" title = "Brain" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z" / > < path d = "M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z" / > < path d = "M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4" / > < / svg > < / button >
< button class = "icon-rail-btn" id = "rail-notes" title = "Notes" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M5 3h10l4 4v14H5z" / > < path d = "M15 3v5h5" / > < path d = "M8 17.5 15.5 10l2.5 2.5L10.5 20H8z" / > < / svg > < / button >
< button class = "icon-rail-btn" id = "rail-tasks" title = "Tasks" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect x = "3" y = "4" width = "18" height = "18" rx = "2" / > < line x1 = "16" y1 = "2" x2 = "16" y2 = "6" / > < line x1 = "8" y1 = "2" x2 = "8" y2 = "6" / > < line x1 = "3" y1 = "10" x2 = "21" y2 = "10" / > < path d = "M9 16l2 2 4-4" / > < / svg > < / button >
< button class = "icon-rail-btn" id = "rail-theme" title = "Theme" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < circle cx = "12" cy = "12" r = "10" / > < path d = "M12 2a10 10 0 0 0 0 20 5 5 0 0 0 5-5 3 3 0 0 0-3-3h-2a3 3 0 0 1-3-3 5 5 0 0 1 5-5" / > < / svg > < / button >
< div style = "flex:1" > < / div >
<!-- Static: bottom -->
< button class = "icon-rail-btn" id = "rail-settings" title = "Settings" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < circle cx = "12" cy = "12" r = "3" / > < path d = "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" / > < / svg > < / button >
< / div >
< nav class = "sidebar" id = "sidebar" role = "navigation" aria-label = "Sidebar" >
< div class = "sidebar-resize-handle" id = "sidebar-resize-handle" > < / div >
< div class = "sidebar-header" >
< button class = "sidebar-hamburger" id = "sidebar-toggle-btn" title = "Toggle sidebar" > < svg width = "18" height = "18" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" > < line x1 = "3" y1 = "6" x2 = "21" y2 = "6" / > < line x1 = "3" y1 = "12" x2 = "21" y2 = "12" / > < line x1 = "3" y1 = "18" x2 = "21" y2 = "18" / > < / svg > < / button >
< div class = "sidebar-brand" id = "sidebar-brand-btn" style = "cursor:pointer;" title = "New chat" >
< span class = "sidebar-brand-title" > Odysseus< / span >
< / div >
< / div >
< div class = "sidebar-inner" >
< div class = "list-item" id = "sidebar-new-chat-btn" title = "New chat" >
< svg class = "sidebar-action-icon" width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" style = "position:relative;left:-2px;" > < line x1 = "12" y1 = "5" x2 = "12" y2 = "19" / > < line x1 = "5" y1 = "12" x2 = "19" y2 = "12" / > < / svg >
< span class = "grow" style = "position:relative;left:-3px;" > New Chat< / span >
< / div >
< div class = "list-item" id = "sidebar-search-btn" title = "Search conversations (Ctrl+K)" >
< svg class = "sidebar-action-icon" width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" > < circle cx = "10" cy = "10" r = "7" / > < path d = "M21 21l-4.35-4.35" / > < / svg >
< span class = "grow" style = "position:relative;left:-1px;" > Search< / span >
< / div >
<!-- Assistant sidebar entry removed — its log + settings live inside
the Tasks modal (Activity + Settings tabs). The backend
assistant session is still maintained for log_to_assistant()
writes from scheduled tasks. -->
< div class = "section" id = "sessions-section" >
< div class = "section-header-flex" >
2026-06-01 03:52:10 +01:00
< span class = "section-title" id = "chats-section-title" > < svg class = "section-icon" width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" / > < / svg > < span id = "chats-section-label" class = "section-title-label" > Chats< / span > < span id = "chats-notif-dot" class = "sidebar-notif-dot" style = "display:none" > < / span > < / span >
< div style = "position:relative; display:inline-block; display:flex; gap:4px; align-items:center;" >
< button type = "button" class = "section-header-btn chats-manage-btn" id = "chats-library-btn" title = "Manage Chats (Library)" >
< svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >
2026-06-01 20:56:11 +09:00
< path d = "M4 19.5A2.5 2.5 0 0 1 6.5 17H20" / >
< path d = "M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" / >
< path d = "M9 7h6M9 11h4" / >
2026-06-01 03:52:10 +01:00
< / svg >
< / button >
2026-05-31 23:58:26 +09:00
< button type = "button" class = "section-header-btn" id = "session-sort-btn" title = "Sort sessions" >
< svg class = "sort-icon" width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >
< line x1 = "4" y1 = "6" x2 = "20" y2 = "6" / >
< line x1 = "4" y1 = "12" x2 = "14" y2 = "12" / >
< line x1 = "4" y1 = "18" x2 = "9" y2 = "18" / >
< / svg >
< svg class = "sort-spinner" width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" style = "display:none; animation: spin 0.7s linear infinite;" >
< path d = "M12 2a10 10 0 0 1 10 10" / >
< / svg >
< / button >
< div id = "session-sort-dropdown" class = "dropdown sort-dropdown" style = "display:none;" >
< div class = "dropdown-item sort-option sort-dropdown-item" data-sort = "active" > Last Active< / div >
< div class = "dropdown-item sort-option sort-dropdown-item" data-sort = "newest" > Newest First< / div >
< div class = "dropdown-item sort-option sort-dropdown-item" data-sort = "group" > By Folder< / div >
< div class = "dropdown-item sort-dropdown-item sort-dropdown-sep" id = "auto-sort-sessions-row" style = "display:flex;align-items:center;padding:0;" >
< span id = "auto-sort-sessions-btn" style = "flex:1;padding:5px 10px;cursor:pointer;display:inline-flex;align-items:center;gap:4px;" >
< span class = "auto-sort-icon" > < svg width = "11" height = "11" viewBox = "0 0 24 24" fill = "currentColor" style = "vertical-align:-1px;margin-right:2px;" > < path d = "M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z" / > < / svg > Tidy< / span >
< span class = "auto-sort-spinner" style = "display:none;" > Sorting...< / span >
< / span >
< button type = "button" id = "auto-sort-sessions-more" title = "Tidy options" aria-label = "Tidy options" style = "background:none;border:none;border-left:1px solid var(--border);color:inherit;cursor:pointer;padding:5px 8px;font-size:9px;opacity:0.7;" > < svg width = "8" height = "8" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "3" stroke-linecap = "round" stroke-linejoin = "round" > < polyline points = "6 9 12 15 18 9" / > < / svg > < / button >
< / div >
< div class = "dropdown-item sort-dropdown-item" id = "auto-sort-sessions-noai-btn" style = "display:none;padding-left:24px;" >
Tidy < span class = "auto-sort-noai-spinner" style = "display:none;font-size:9px;opacity:0.6;margin-left:4px;" > Cleaning...< / span >
< / div >
< div class = "dropdown-item rearrange-toggle sort-dropdown-item sort-dropdown-sep" id = "session-rearrange-toggle" >
↑ ↓ Rearrange < span class = "rearrange-check" style = "float:right; opacity:0;" > • < / span >
< / div >
< div class = "dropdown-item sort-dropdown-item" id = "session-select-from-dropdown" >
● Select
< / div >
< / div >
< / div >
< / div >
< div id = "session-bulk-bar" class = "session-bulk-bar hidden" >
< span id = "session-select-all-dot" style = "cursor:pointer;font-size:14px;opacity:0.4;user-select:none;" > ○< / span > < input type = "checkbox" id = "session-select-all" style = "display:none;" > < span style = "font-size:10px;cursor:pointer;opacity:0.5;" id = "session-select-all-label" > All< / span >
< div style = "margin-left:auto;display:flex;gap:2px;align-items:center;" >
< button class = "session-bulk-btn" id = "session-bulk-archive" title = "Archive selected" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect x = "2" y = "3" width = "20" height = "5" rx = "1" / > < path d = "M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8" / > < path d = "M10 12h4" / > < / svg > < / button >
< button class = "session-bulk-btn session-bulk-btn-danger" id = "session-bulk-delete" title = "Delete selected" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M3 6h18" / > < path d = "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" / > < path d = "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" / > < / svg > < / button >
< button class = "session-bulk-btn" id = "session-bulk-cancel" title = "Cancel" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" > < line x1 = "18" y1 = "6" x2 = "6" y2 = "18" / > < line x1 = "6" y1 = "6" x2 = "18" y2 = "18" / > < / svg > < / button >
< / div >
< / div >
< div id = "session-list" role = "listbox" > < / div >
< / div >
<!-- Hidden dropdown for session actions -->
< div id = "session-actions-dropdown" class = "dropdown hidden" >
< div class = "dropdown-item" id = "rename-session-option" >
< h4 > Rename< / h4 >
< p > Change the session name< / p >
< / div >
< div class = "dropdown-item" id = "delete-session-option" >
< h4 > Delete< / h4 >
< p > Remove this session permanently< / p >
< / div >
< div class = "dropdown-item" id = "memory-session-option" >
< h4 > Memory< / h4 >
< p > Extract memories from this session< / p >
< / div >
< / div >
< div class = "section" id = "email-section" >
< div class = "section-header-flex" >
< span class = "section-title" id = "email-section-title" style = "cursor:pointer;" title = "Open email inbox" > < svg class = "section-icon" width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect x = "2" y = "4" width = "20" height = "16" rx = "2" / > < path d = "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" / > < / svg > < span > Email< / span > < / span >
< button type = "button" class = "section-header-btn list-item-plus-btn" id = "email-compose-btn" title = "Compose email" >
< svg class = "list-item-plus-icon" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" > < line x1 = "12" y1 = "5" x2 = "12" y2 = "19" / > < line x1 = "5" y1 = "12" x2 = "19" y2 = "12" / > < / svg >
< span class = "list-item-plus-label" > new< / span >
< / button >
<!-- Unread dot is a direct header child placed AFTER the compose
button (it used to live inside the title, before the button).
With the title's flex:1 eating the free space, this lands the
dot at the far-right corner with the + just to its left —
i.e. the dot and + swap places when the dot appears. -->
< span id = "email-unread-dot" class = "sidebar-notif-dot" style = "display:none" > < / span >
< / div >
< / div >
< div class = "section" id = "models-section" >
< div class = "section-header-flex" >
< span class = "section-title" > Models< / span >
< div style = "position:relative; display:inline-block;" >
< button type = "button" class = "section-header-btn" id = "model-sort-btn" title = "Sort models" >
< svg class = "sort-icon" width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >
< line x1 = "4" y1 = "6" x2 = "20" y2 = "6" / >
< line x1 = "4" y1 = "12" x2 = "14" y2 = "12" / >
< line x1 = "4" y1 = "18" x2 = "9" y2 = "18" / >
< / svg >
< / button >
< div id = "model-sort-dropdown" class = "dropdown sort-dropdown" style = "display:none;" >
< div class = "dropdown-item sort-option sort-dropdown-item" data-sort = "alpha" > A-Z< / div >
< div class = "dropdown-item sort-option sort-dropdown-item" data-sort = "last-used" > Last used< / div >
< div class = "dropdown-item sort-option sort-dropdown-item" data-sort = "most-used" > Most used< / div >
< div class = "dropdown-item rearrange-toggle sort-dropdown-item sort-dropdown-sep" id = "model-rearrange-toggle" >
↑ ↓ Rearrange < span class = "rearrange-check" style = "float:right; opacity:0;" > • < / span >
< / div >
< / div >
< / div >
< / div >
< div id = "models" >
< div class = "models-row" >
< select id = "model-select" aria-label = "Select model" style = "flex: 1; padding: 6px 8px; border-radius: 4px; border: 1px solid var(--border); background: var(--bg); color: var(--fg);" > < / select >
< button type = "button" id = "btn-model-chat" class = "model-chat-btn" aria-label = "Add model chat" style = "transition: all 0.2s ease;" > < span class = "model-chat-btn-label" > + Chat< / span > < / button >
< / div >
< / div >
< / div >
< div class = "section" id = "tools-section" >
< div class = "section-header-flex" >
< span class = "section-title" > < svg class = "section-icon" width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" / > < / svg > Tools< / span >
< / div >
< div class = "list-item" id = "tool-memory-btn" >
< svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
style="flex-shrink:0;opacity:0.5;">
< path d = "M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z" / >
< path d = "M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z" / >
< path d = "M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4" / >
< / svg >
< span class = "grow" > Brain< / span >
< / div >
< div class = "list-item" id = "tool-calendar-btn" >
< svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
style="flex-shrink:0;opacity:0.5;">
< rect x = "3" y = "4" width = "18" height = "18" rx = "2" / >
< line x1 = "16" y1 = "2" x2 = "16" y2 = "6" / >
< line x1 = "8" y1 = "2" x2 = "8" y2 = "6" / >
< line x1 = "3" y1 = "10" x2 = "21" y2 = "10" / >
< / svg >
< span class = "grow" > Calendar< / span >
< / div >
< div class = "list-item" id = "tool-compare-btn" >
< svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "flex-shrink:0;opacity:0.5;" > < circle cx = "18" cy = "18" r = "3" / > < circle cx = "6" cy = "6" r = "3" / > < path d = "M13 6h3a2 2 0 0 1 2 2v7" / > < path d = "M11 18H8a2 2 0 0 1-2-2V9" / > < / svg >
< span class = "grow" > Compare< / span >
< / div >
< div class = "list-item" id = "tool-cookbook-btn" >
< svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor"
stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"
style="flex-shrink:0;opacity:0.5;">
< path d = "M12 7v14" / >
< path d = "M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" / >
< / svg >
< span class = "grow" > Cookbook< / span >
< span id = "cookbook-bg-status" style = "display:none;font-size:9px;opacity:0.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-left:6px;flex-shrink:1;min-width:0;position:relative;top:-1px;" > < / span >
< span class = "cookbook-notif-dot" id = "cookbook-notif-dot" style = "display:none;margin-left:6px;margin-right:4px;position:relative;top:-1px;left:0px;" > < / span >
< / div >
< div class = "list-item" id = "tool-research-btn" >
< svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" style = "flex-shrink:0;opacity:0.5;" > < circle cx = "11" cy = "11" r = "8" / > < path d = "M21 21l-4.35-4.35" / > < line x1 = "11" y1 = "8" x2 = "11" y2 = "14" / > < line x1 = "8" y1 = "11" x2 = "14" y2 = "11" / > < / svg >
< span class = "grow" > Deep Research< / span >
< / div >
< div class = "list-item" id = "tool-gallery-btn" >
< svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
style="flex-shrink:0;opacity:0.5;">
< rect x = "3" y = "3" width = "18" height = "18" rx = "2" / >
< circle cx = "8.5" cy = "8.5" r = "1.5" / >
< path d = "M21 15l-5-5L5 21" / >
< / svg >
< span class = "grow" > Gallery< / span >
< / div >
< div class = "list-item" id = "tool-library-btn" >
< svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "flex-shrink:0;opacity:0.5;" >
< path d = "M4 19.5A2.5 2.5 0 0 1 6.5 17H20" / > < path d = "M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" / > < path d = "M9 7h6M9 11h4" / >
< / svg >
< span class = "grow" > Library< / span >
< button type = "button" class = "list-item-plus-btn" id = "library-new-doc-btn" title = "New document" >
< svg class = "list-item-plus-icon" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" style = "width:11px;height:11px;" > < line x1 = "12" y1 = "5" x2 = "12" y2 = "19" / > < line x1 = "5" y1 = "12" x2 = "19" y2 = "12" / > < / svg >
< span class = "list-item-plus-label" > new< / span >
< / button >
< / div >
< div class = "list-item" id = "tool-notes-btn" >
< svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
style="flex-shrink:0;opacity:0.5;">
< path d = "M5 3h10l4 4v14H5z" / >
< path d = "M15 3v5h5" / >
< path d = "M8 17.5 15.5 10l2.5 2.5L10.5 20H8z" / >
< / svg >
< span class = "grow" > Notes< / span >
< / div >
< div class = "list-item" id = "tool-tasks-btn" >
< svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
style="flex-shrink:0;opacity:0.5;">
< rect x = "3" y = "4" width = "18" height = "18" rx = "2" / >
< line x1 = "16" y1 = "2" x2 = "16" y2 = "6" / >
< line x1 = "8" y1 = "2" x2 = "8" y2 = "6" / >
< line x1 = "3" y1 = "10" x2 = "21" y2 = "10" / >
< path d = "M9 16l2 2 4-4" / >
< / svg >
< span class = "grow" > Tasks< / span >
< span id = "assistant-notif-dot" class = "sidebar-notif-dot" style = "display:none" > < / span >
< / div >
< div class = "list-item" id = "tool-theme-btn" >
< svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
style="flex-shrink:0;opacity:0.5;">
< circle cx = "12" cy = "12" r = "10" / >
< path d = "M12 2a7 7 0 0 0 0 20 4 4 0 0 1 0-8 4 4 0 0 0 0-8" / >
< circle cx = "8" cy = "9" r = "1.5" fill = "currentColor" / >
< circle cx = "15" cy = "14" r = "1.5" fill = "currentColor" / >
< circle cx = "9" cy = "15" r = "1.5" fill = "currentColor" / >
< / svg >
< span class = "grow" > Theme< / span >
< / div >
< / div >
< / div >
< div class = "sidebar-user-bar" id = "sidebar-user-bar" >
< div class = "user-bar-left" id = "user-bar-profile" >
< div class = "user-bar-avatar" id = "user-bar-avatar" > < / div >
< span class = "user-bar-name" id = "user-bar-name" > User< / span >
< / div >
< div class = "user-bar-actions" >
< button type = "button" class = "user-bar-btn" id = "user-bar-settings" title = "Settings" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < circle cx = "12" cy = "12" r = "3" / > < path d = "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" / > < / svg >
< / button >
< / div >
< / div >
< / nav >
Improve accessibility across core flows (#86)
First incremental pass at issue #86, focused on the universal entry
points and primary navigation. All changes verified in-browser with the
axe-core engine (0 violations on the surfaces below) plus manual keyboard
testing, on both desktop (1280px) and mobile (390px).
Login / first-run setup (static/login.html)
- Add a real <h1>, wrap content in <main> + <footer> landmarks.
- Mark the decorative boat SVG aria-hidden.
- Errors now use role="alert" so screen readers announce them.
- "Remember me" checkbox is keyboard-focusable (was display:none) with an
accessible name and a focus ring; dynamic 2FA field gets a linked label.
- Darken the brand-red submit button so white text clears WCAG AA 4.5:1
(was ~3.2:1); add visible :focus-visible rings.
App shell (static/index.html, static/style.css)
- Remove invalid role="region" from the <main> chat container (it was
overriding the implicit main landmark).
- Add a persistent, visually-hidden <h1> inside <main> so the page always
exposes one logical level-1 heading — works even on mobile where the
sidebar (with the visible brand) is hidden off-canvas.
- Add a reusable .a11y-visually-hidden utility.
- Raise chat-title, model-picker, settings-helper and notes text contrast
above 4.5:1 (were 2.8-3.9:1).
Keyboard nav + dialogs (static/js/a11y.js - new)
- Make the click-only <div> sidebar navigation (New Chat, Search, Brain,
Calendar, Compare, Cookbook, Deep Research, Gallery, Library, Notes,
Tasks, Theme, account) focusable and Enter/Space-activatable, announced
as buttons (skipping role=button where a nested control would create a
nested-interactive violation). Visible focus ring reused from existing
.list-item:focus-visible.
- Upgrade modals (.modal-content and the docked .notes-pane) to labelled
role="dialog" + aria-modal, and normalise their title to heading level 2
so heading order stays valid. A MutationObserver covers runtime-rendered
rows and modals.
Decorative background canvases (static/js/theme.js)
- Mark all 7 bg-effect canvases aria-hidden.
Notes & Tasks (static/js/notes.js, static/js/tasks.js)
- Label the icon-only Note/To-do toggle pills (fixes a critical
button-name issue) and track aria-pressed state.
- Improve Notes header-button + empty-state contrast.
- Give the Tasks sort <select> an accessible name (fixes a critical
select-name issue).
Remaining data-dense tool modals (Tasks cards, Calendar, Gallery, Email,
Cookbook, Compare, Deep Research) still have muted-text contrast to polish
and are the next incremental step, per the issue's own guidance.
2026-06-01 21:05:43 +02:00
< main class = "chat-container welcome-active" id = "chat-container" aria-label = "Chat area" aria-busy = "false" >
<!-- Persistent page heading for assistive tech. Visually hidden so it
never affects layout, but always present inside the main landmark
(the sidebar that shows the visible brand is hidden off-canvas on
mobile) so the page always exposes a single level-1 heading. -->
< h1 class = "a11y-visually-hidden" > Odysseus< / h1 >
2026-05-31 23:58:26 +09:00
< div class = "chat-top-bar" >
< button type = "button" class = "incognito-indicator" id = "incognito-indicator" title = "Nobody mode active — click to deactivate" style = "display:none;" > < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" / > < line x1 = "8" y1 = "16" x2 = "16" y2 = "8" / > < line x1 = "8" y1 = "8" x2 = "16" y2 = "16" / > < / svg > < / button >
< div class = "chat-meta-overlay" > < span id = "current-meta" > Odysseus Chat< / span > < span id = "current-meta-count" class = "chat-meta-count" aria-hidden = "true" > < / span > < span id = "session-cost-display" class = "session-cost-display" style = "display:none;" > < / span > < span class = "export-dropdown-wrap" id = "export-dropdown-wrap" > < button type = "button" class = "export-dl-btn" id = "export-dl-btn" title = "More" > < svg width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" > < polyline points = "6 9 12 15 18 9" / > < / svg > < / button > < div class = "export-dropdown-menu" id = "export-dropdown-menu" > < div class = "export-dropdown-item" id = "export-rename-btn" > < span class = "dropdown-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" / > < / svg > < / span > < span > Rename< / span > < / div > < div class = "export-dropdown-item" id = "export-copy-btn" > < span class = "dropdown-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect x = "9" y = "9" width = "13" height = "13" rx = "2" / > < path d = "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" / > < / svg > < / span > < span > Copy Chat< / span > < / div > < div class = "export-dropdown-item" id = "export-pdf-btn" > < span class = "dropdown-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" / > < polyline points = "14 2 14 8 20 8" / > < path d = "M9 15v-2h2a1.5 1.5 0 0 1 0 3H9z" / > < / svg > < / span > < span > PDF< / span > < / div > < div class = "export-dropdown-item" id = "export-doc-btn" > < span class = "dropdown-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" / > < polyline points = "14 2 14 8 20 8" / > < line x1 = "16" y1 = "13" x2 = "8" y2 = "13" / > < line x1 = "16" y1 = "17" x2 = "8" y2 = "17" / > < polyline points = "10 9 9 9 8 9" / > < / svg > < / span > < span > Save to Documents< / span > < / div > < / div > < / span > < / div > < / div >
< div id = "welcome-screen" >
< div class = "welcome-name" > < svg class = "welcome-boat" viewBox = "0 0 32 32" > < path d = "M16 4L16 22L6 22Z" fill = "currentColor" / > < path d = "M16 8L16 22L24 22Z" fill = "currentColor" opacity = "0.6" / > < path d = "M4 24Q10 20 16 24Q22 28 28 24" stroke = "currentColor" stroke-width = "2.5" fill = "none" stroke-linecap = "round" / > < / svg > Odysseus< / div >
2026-06-01 21:11:47 +03:00
< div class = "welcome-sub" id = "welcome-sub" > Welcome, < span class = "setup-trigger-link" style = "color:var(--accent,var(--red));font-weight:600;cursor:pointer;text-decoration:underline;" title = "Click to launch setup" > type /setup< / span > to get started.< / div >
2026-05-31 23:58:26 +09:00
< div class = "welcome-tip" id = "welcome-tip" > < / div >
< button type = "button" class = "incognito-btn" id = "incognito-btn" title = "Enable Nobody mode — no memory, no history saved" >
< svg class = "eye-open" width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >
< path d = "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" / > < circle cx = "12" cy = "12" r = "3" / >
< / svg >
< svg class = "eye-blinded" width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "display:none" >
< path d = "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" / > < line x1 = "8" y1 = "16" x2 = "16" y2 = "8" / > < line x1 = "8" y1 = "8" x2 = "16" y2 = "16" / >
< / svg >
< span class = "incognito-label" > Nobody< / span >
< / button >
< div id = "welcome-setup" style = "display:none" > < / div >
< / div >
< script nonce = "{{CSP_NONCE}}" >
(function(){
var mobile = window.matchMedia('(max-width: 768px)').matches;
var desktop = [
'Tip: Press Ctrl+K to search across all your conversations.',
'Tip: Press Ctrl+B to quickly toggle the sidebar.',
'Tip: Shift-click the sidebar toggle to swap it to the other side.',
'Tip: Drag and drop files onto the chat to attach them.',
'Tip: Right-click a session for rename, delete, and memory options.',
];
var phone = [
'Tip: Long-press a session for rename, delete, and memory options.',
'Tip: Tap the eye icon for Nobody mode — no history saved.',
'Tip: Switch to Agent mode for web search and code execution.',
'Tip: Use Compare mode to test different models side by side.',
'Tip: Attach images or files using the + button next to the input.',
];
var tips = mobile ? phone : desktop;
var el = document.getElementById('welcome-tip');
if (el) {
el.textContent = 'Type /setup, then choose Local models or API.';
}
fetch('/api/version').then(function(r){return r.json()}).then(function(d){
if (d.version) window._appVersion = d.version;
}).catch(function(){});
})();
< / script >
< div id = "chat-history" class = "chat-history" role = "log" aria-live = "polite" > < / div >
<!-- Attachments strip -->
< div id = "attach-strip" class = "attach-strip" > < / div >
<!-- Hidden elements for form logic -->
< input type = "checkbox" id = "research-toggle" style = "display:none;" >
< input type = "checkbox" id = "rag-toggle" style = "display:none;" >
< input type = "checkbox" id = "incognito-toggle" style = "display:none;" >
< input type = "file" id = "file-input" class = "hidden" multiple accept = "image/*,application/pdf,video/*,.txt,.py,.html,.htm,.md,.json,.csv,.log,audio/*" / >
<!-- Unified chat input bar -->
< div class = "chat-input-bar" >
< div class = "chat-input-top" >
< div id = "message-ghost" class = "ghost-text-overlay" aria-hidden = "true" > < / div >
< textarea id = "message" placeholder = "Message Odysseus..." required autocomplete = "off" aria-label = "Message input" rows = "1" autofocus > < / textarea >
<!-- Model picker (inside chatbox, top - right) -->
< div class = "model-picker-wrap" id = "model-picker-wrap" >
< button type = "button" class = "model-picker-btn" id = "model-picker-btn" title = "Switch model" > < span id = "model-picker-label" > Select model< / span > < svg width = "10" height = "10" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "3" stroke-linecap = "round" stroke-linejoin = "round" > < polyline points = "6 15 12 9 18 15" / > < / svg > < / button >
< div class = "model-picker-menu hidden" id = "model-picker-menu" >
2026-06-01 10:00:15 +09:00
< div class = "model-picker-search-row" >
< input type = "text" id = "model-picker-search" placeholder = "Search models..." autocomplete = "off" >
< button type = "button" class = "model-picker-action-btn primary" id = "model-picker-add-models-btn" title = "Add model endpoints" aria-label = "Add model endpoints" >
< svg width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.4" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M12 5v14" / > < path d = "M5 12h14" / > < / svg >
< / button >
< / div >
2026-05-31 23:58:26 +09:00
< div class = "model-picker-list" id = "model-picker-list" > < / div >
< / div >
< / div >
< / div >
< div id = "pinned-tools-bar" > < / div >
< div class = "chat-input-bottom" style = "visibility:hidden" >
< div class = "chat-input-left" >
<!-- Overflow menu (+) — always first/left -->
< div class = "overflow-wrapper" >
< button type = "button" class = "input-icon-btn overflow-plus-btn" id = "overflow-plus-btn" title = "More tools" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" >
< polyline points = "6 15 12 9 18 15" / >
< / svg >
< span class = "plus-active-dot" > < / span >
< / button >
< div id = "overflow-menu" class = "overflow-menu hidden" >
< button type = "button" class = "overflow-menu-item" id = "overflow-attach-btn" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" / > < / svg >
< span > Attach files< / span >
< / button >
< button type = "button" class = "overflow-menu-item" id = "overflow-doc-btn" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" / > < polyline points = "14 2 14 8 20 8" / > < line x1 = "16" y1 = "13" x2 = "8" y2 = "13" / > < line x1 = "16" y1 = "17" x2 = "8" y2 = "17" / > < polyline points = "10 9 9 9 8 9" / >
< / svg >
< span > Documents< / span >
< / button >
< button type = "button" class = "overflow-menu-item" id = "overflow-rag-btn" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< ellipse cx = "12" cy = "5" rx = "9" ry = "3" / > < path d = "M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" / > < path d = "M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" / >
< / svg >
< span > RAG< / span >
< span class = "overflow-active-dot" > < / span >
< / button >
<!-- Inline "deep research mode" toggle removed (superseded by the
Deep Research sidebar / trigger_research). The hidden
#research-toggle checkbox is kept inert so existing JS refs
don't break; without this entry point it can't be enabled. -->
<!-- Group Chat moved to Characters modal Group tab -->
<!-- TTS Mode hidden — read - aloud feature is off in this build. -->
< button type = "button" class = "overflow-menu-item" id = "overflow-tts-btn" hidden style = "display:none" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < polygon points = "11 5 6 9 2 9 2 15 6 15 11 19 11 5" / > < path d = "M19.07 4.93a10 10 0 0 1 0 14.14" / > < path d = "M15.54 8.46a5 5 0 0 1 0 7.07" / > < / svg >
< span > TTS Mode< / span >
< span class = "overflow-active-dot" > < / span >
< / button >
< button type = "button" class = "overflow-menu-item" id = "overflow-preset-btn" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >
< path d = "m18 2 4 4" / > < path d = "m17 7 3-3" / > < path d = "M19 9 8.7 19.3c-1 1-2.5 1-3.4 0l-.6-.6c-1-1-1-2.5 0-3.4L15 5" / > < path d = "m9 11 4 4" / > < path d = "m5 19-3 3" / > < path d = "m14 4 6 6" / >
< / svg >
< span > Prompt< / span >
< / button >
< / div >
< / div >
<!-- Web search (magnifying glass) -->
< button type = "button" class = "input-icon-btn" title = "Web search" id = "web-toggle-btn" data-mode-tool = "true" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >
< circle cx = "11" cy = "11" r = "8" / > < line x1 = "21" y1 = "21" x2 = "16.65" y2 = "16.65" / >
< / svg >
< / button >
<!-- Shell commands (terminal) -->
< button type = "button" class = "input-icon-btn" title = "Shell Access" id = "bash-toggle-btn" data-mode-tool = "true" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >
< polyline points = "4 17 10 11 4 5" / > < line x1 = "12" y1 = "19" x2 = "20" y2 = "19" / >
< / svg >
< / button >
<!-- RAG toolbar indicator (hidden until active) -->
< button type = "button" class = "input-icon-btn tool-indicator" title = "RAG active — click to deactivate" id = "rag-indicator-btn" style = "display:none;" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< ellipse cx = "12" cy = "5" rx = "9" ry = "3" / > < path d = "M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" / > < path d = "M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" / >
< / svg >
< span style = "font-size:11px;margin-left:2px;" > RAG< / span >
< svg class = "tool-indicator-x" width = "10" height = "10" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "3" stroke-linecap = "round" > < line x1 = "6" y1 = "6" x2 = "18" y2 = "18" / > < line x1 = "18" y1 = "6" x2 = "6" y2 = "18" / > < / svg >
< / button >
<!-- 6. Deep Research (hidden until active) -->
< button type = "button" class = "input-icon-btn tool-indicator" title = "Deep Research active — click to deactivate" id = "research-toggle-btn" style = "display:none;" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < circle cx = "11" cy = "11" r = "8" / > < path d = "M21 21l-4.35-4.35" / > < line x1 = "11" y1 = "8" x2 = "11" y2 = "14" / > < line x1 = "8" y1 = "11" x2 = "14" y2 = "11" / > < / svg >
< span style = "font-size:11px;margin-left:2px;" > Research< / span >
< svg class = "tool-indicator-x" width = "10" height = "10" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "3" stroke-linecap = "round" > < line x1 = "6" y1 = "6" x2 = "18" y2 = "18" / > < line x1 = "18" y1 = "6" x2 = "6" y2 = "18" / > < / svg >
< / button >
<!-- 7. Group Chat (hidden until active) -->
< button type = "button" class = "input-icon-btn tool-indicator" title = "Group Chat active — click to deactivate" id = "group-toggle-btn" style = "display:none;" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" / > < circle cx = "9" cy = "7" r = "4" / > < path d = "M23 21v-2a4 4 0 0 0-3-3.87" / > < path d = "M16 3.13a4 4 0 0 1 0 7.75" / > < / svg >
< span style = "font-size:11px;margin-left:2px;" > Group< / span >
< svg class = "tool-indicator-x" width = "10" height = "10" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "3" stroke-linecap = "round" > < line x1 = "6" y1 = "6" x2 = "18" y2 = "18" / > < line x1 = "18" y1 = "6" x2 = "6" y2 = "18" / > < / svg >
< / button >
< input type = "checkbox" id = "group-toggle" style = "display:none;" >
<!-- Character indicator (hidden until active) -->
Rename Character copy to Persona
Issue #234: the "Character" tab and its "Style of response" label made it
unclear that this is where a system prompt is set. Rename the user-facing
labels for clarity:
- "Character" tab + section heading -> "Persona"
- "Style of response" -> "System prompt"
- supporting strings: select placeholder, name placeholder, button/title
text, toasts, confirm/notice text, the chat-bar indicator tooltip, the
settings visibility toggle, and the assistant personality picker
("Characters" optgroup -> "Personas").
Used "Persona" rather than the issue's suggested "Preset" because the app
already has a distinct, user-facing "Presets" concept (built-in presets
like Code Analyze/Brainstorm/Reason, shown as their own group in the
assistant picker). "Persona" matches what this tab actually creates -- a
named persona with its own memories -- without colliding with that term.
Internal identifiers (element IDs, data-chartab attributes, function names)
and the character_name backend field are intentionally left unchanged so
existing saved presets and JS wiring keep working.
2026-06-02 05:42:15 +02:00
< button type = "button" class = "input-icon-btn tool-indicator" title = "Persona active — click to deactivate" id = "character-indicator-btn" style = "display:none;" >
2026-05-31 23:58:26 +09:00
< svg id = "char-indicator-icon" width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" / > < circle cx = "12" cy = "7" r = "4" / > < / svg >
< span id = "character-indicator-name" style = "font-size:11px;margin-left:2px;max-width:80px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" > < / span >
< svg class = "tool-indicator-x" width = "10" height = "10" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "3" stroke-linecap = "round" > < line x1 = "6" y1 = "6" x2 = "18" y2 = "18" / > < line x1 = "18" y1 = "6" x2 = "6" y2 = "18" / > < / svg >
< / button >
<!-- Compare toolbar indicator (hidden until active) -->
< button type = "button" class = "input-icon-btn tool-indicator" title = "Compare active — click to deactivate" id = "compare-indicator-btn" style = "display:none;" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < circle cx = "18" cy = "18" r = "3" / > < circle cx = "6" cy = "6" r = "3" / > < path d = "M13 6h3a2 2 0 0 1 2 2v7" / > < path d = "M11 18H8a2 2 0 0 1-2-2V9" / > < / svg >
< span style = "font-size:11px;margin-left:2px;" > Compare< / span >
< svg class = "tool-indicator-x" width = "10" height = "10" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "3" stroke-linecap = "round" > < line x1 = "6" y1 = "6" x2 = "18" y2 = "18" / > < line x1 = "18" y1 = "6" x2 = "6" y2 = "18" / > < / svg >
< / button >
< / div >
< div class = "chat-input-right" >
<!-- Agent / Chat mode toggle -->
< div class = "mode-toggle" >
< button type = "button" class = "mode-toggle-btn active" id = "mode-agent-btn" > Agent< / button >
< button type = "button" class = "mode-toggle-btn" id = "mode-chat-btn" > Chat< / button >
< / div >
< button type = "submit" form = "chat-form" class = "send-btn newchat-mode" data-mode = "newchat" aria-label = "New chat" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" > < line x1 = "12" y1 = "5" x2 = "12" y2 = "19" / > < line x1 = "5" y1 = "12" x2 = "19" y2 = "12" / > < / svg > < span class = "send-btn-label" > + New< / span >
< / button >
< / div >
< / div >
<!-- Hidden checkboxes for state -->
< input type = "checkbox" id = "web-toggle" style = "display:none;" >
< input type = "checkbox" id = "bash-toggle" style = "display:none;" >
< / div >
< form id = "chat-form" autocomplete = "off" action = "javascript:void(0);" style = "display:none;" > < / form >
<!-- Character (custom preset) modal -->
< div id = "custom-preset-modal" class = "modal hidden" >
Add dialog accessibility semantics
Screen readers got no signal that a dialog opened — not one modal carried
role="dialog" — and several close buttons had no accessible name.
- The 6 static tool windows (Brain, Theme, Prompt, Rename session, Cookbook,
Settings) now carry role="dialog" + an accessible name. They are dockable,
tiling windows, so they are non-modal dialogs (intentionally no aria-modal).
- The four unlabelled close buttons (theme, prompt, cookbook, settings) get an
aria-label so they no longer read as just "heavy multiplication x".
- styledConfirm / styledPrompt ARE blocking modals: they get role="dialog" +
aria-modal="true" + aria-labelledby/aria-describedby, and now manage focus —
restore focus to the triggering element on close and trap Tab within the
dialog (they already moved focus in on open).
tests/test_dialog_aria.py pins the roles, labels, and focus management.
2026-06-01 23:41:25 -04:00
< div class = "modal-content preset-modal-content" role = "dialog" aria-label = "Prompt" style = "background:var(--bg)" >
2026-05-31 23:58:26 +09:00
< div class = "modal-header" >
< h4 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:6px" > < path d = "m18 2 4 4" / > < path d = "m17 7 3-3" / > < path d = "M19 9 8.7 19.3c-1 1-2.5 1-3.4 0l-.6-.6c-1-1-1-2.5 0-3.4L15 5" / > < path d = "m9 11 4 4" / > < path d = "m5 19-3 3" / > < path d = "m14 4 6 6" / > < / svg > Prompt< / h4 >
Add dialog accessibility semantics
Screen readers got no signal that a dialog opened — not one modal carried
role="dialog" — and several close buttons had no accessible name.
- The 6 static tool windows (Brain, Theme, Prompt, Rename session, Cookbook,
Settings) now carry role="dialog" + an accessible name. They are dockable,
tiling windows, so they are non-modal dialogs (intentionally no aria-modal).
- The four unlabelled close buttons (theme, prompt, cookbook, settings) get an
aria-label so they no longer read as just "heavy multiplication x".
- styledConfirm / styledPrompt ARE blocking modals: they get role="dialog" +
aria-modal="true" + aria-labelledby/aria-describedby, and now manage focus —
restore focus to the triggering element on close and trap Tab within the
dialog (they already moved focus in on open).
tests/test_dialog_aria.py pins the roles, labels, and focus management.
2026-06-01 23:41:25 -04:00
< button class = "close-btn" id = "close-custom-preset" aria-label = "Close prompt" > ✖< / button >
2026-05-31 23:58:26 +09:00
< / div >
< div class = "modal-body preset-modal-body" >
< div id = "char-fields-wrap" >
< div class = "preset-tabs" >
< button class = "preset-tab active" data-chartab = "inject" > < svg class = "preset-tab-icon" width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "m18 2 4 4" / > < path d = "m17 7 3-3" / > < path d = "M19 9 8.7 19.3c-1 1-2.5 1-3.4 0l-.6-.6c-1-1-1-2.5 0-3.4L15 5" / > < path d = "m9 11 4 4" / > < path d = "m5 19-3 3" / > < path d = "m14 4 6 6" / > < / svg > < span > Inject< / span > < / button >
Rename Character copy to Persona
Issue #234: the "Character" tab and its "Style of response" label made it
unclear that this is where a system prompt is set. Rename the user-facing
labels for clarity:
- "Character" tab + section heading -> "Persona"
- "Style of response" -> "System prompt"
- supporting strings: select placeholder, name placeholder, button/title
text, toasts, confirm/notice text, the chat-bar indicator tooltip, the
settings visibility toggle, and the assistant personality picker
("Characters" optgroup -> "Personas").
Used "Persona" rather than the issue's suggested "Preset" because the app
already has a distinct, user-facing "Presets" concept (built-in presets
like Code Analyze/Brainstorm/Reason, shown as their own group in the
assistant picker). "Persona" matches what this tab actually creates -- a
named persona with its own memories -- without colliding with that term.
Internal identifiers (element IDs, data-chartab attributes, function names)
and the character_name backend field are intentionally left unchanged so
existing saved presets and JS wiring keep working.
2026-06-02 05:42:15 +02:00
< button class = "preset-tab" data-chartab = "character" > < svg class = "preset-tab-icon" width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" / > < circle cx = "12" cy = "7" r = "4" / > < / svg > < span > Persona< / span > < / button >
2026-05-31 23:58:26 +09:00
< button class = "preset-tab" data-chartab = "group" > < svg class = "preset-tab-icon" width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" / > < circle cx = "9" cy = "7" r = "4" / > < path d = "M22 21v-2a4 4 0 0 0-3-3.87" / > < path d = "M16 3.13a4 4 0 0 1 0 7.75" / > < / svg > < span > Group< / span > < / button >
< / div >
<!-- Inject tab (also holds model tuning: temperature + max tokens) -->
< div class = "preset-chartab" data-chartab-panel = "inject" >
< label for = "inject-prefix" > Prefix< / label >
< textarea id = "inject-prefix" rows = "2" placeholder = "Added before your message" style = "margin-bottom:8px" > < / textarea >
< label for = "inject-suffix" > Suffix< / label >
< textarea id = "inject-suffix" rows = "2" placeholder = "Added after your message" style = "margin-bottom:12px" > < / textarea >
< div class = "preset-slider-row" >
< label > Temperature < span class = "preset-hint-icon" title = "Controls randomness. Lower values give focused, deterministic answers (good for code). Higher values give more creative, varied responses." > ?< / span > < / label >
< span class = "preset-slider-value" id = "temp-value" > 1.0< / span >
< / div >
< input type = "range" class = "preset-range" id = "custom-temperature" min = "0" max = "2" step = "0.1" value = "1.0" >
< div class = "preset-temp-hints" >
< span > Precise / Code< / span >
< span > Balanced< / span >
< span > Creative< / span >
< / div >
< div class = "preset-slider-row" >
< label > Max Tokens < span class = "preset-hint-icon" title = "Maximum length of the AI response. 'No limit' lets the model decide when to stop." > ?< / span > < / label >
< span class = "preset-slider-value" id = "tokens-value" > No limit< / span >
< / div >
< input type = "range" class = "preset-range" id = "custom-max-tokens" min = "256" max = "8448" step = "256" value = "8448" >
< / div >
<!-- Prompt (character/persona) tab -->
< div class = "preset-chartab" data-chartab-panel = "character" style = "display:none" >
Rename Character copy to Persona
Issue #234: the "Character" tab and its "Style of response" label made it
unclear that this is where a system prompt is set. Rename the user-facing
labels for clarity:
- "Character" tab + section heading -> "Persona"
- "Style of response" -> "System prompt"
- supporting strings: select placeholder, name placeholder, button/title
text, toasts, confirm/notice text, the chat-bar indicator tooltip, the
settings visibility toggle, and the assistant personality picker
("Characters" optgroup -> "Personas").
Used "Persona" rather than the issue's suggested "Preset" because the app
already has a distinct, user-facing "Presets" concept (built-in presets
like Code Analyze/Brainstorm/Reason, shown as their own group in the
assistant picker). "Persona" matches what this tab actually creates -- a
named persona with its own memories -- without colliding with that term.
Internal identifiers (element IDs, data-chartab attributes, function names)
and the character_name backend field are intentionally left unchanged so
existing saved presets and JS wiring keep working.
2026-06-02 05:42:15 +02:00
< label > Persona< / label >
2026-05-31 23:58:26 +09:00
< div class = "char-name-combo" >
< select id = "char-template-select" class = "char-template-select" >
Rename Character copy to Persona
Issue #234: the "Character" tab and its "Style of response" label made it
unclear that this is where a system prompt is set. Rename the user-facing
labels for clarity:
- "Character" tab + section heading -> "Persona"
- "Style of response" -> "System prompt"
- supporting strings: select placeholder, name placeholder, button/title
text, toasts, confirm/notice text, the chat-bar indicator tooltip, the
settings visibility toggle, and the assistant personality picker
("Characters" optgroup -> "Personas").
Used "Persona" rather than the issue's suggested "Preset" because the app
already has a distinct, user-facing "Presets" concept (built-in presets
like Code Analyze/Brainstorm/Reason, shown as their own group in the
assistant picker). "Persona" matches what this tab actually creates -- a
named persona with its own memories -- without colliding with that term.
Internal identifiers (element IDs, data-chartab attributes, function names)
and the character_name backend field are intentionally left unchanged so
existing saved presets and JS wiring keep working.
2026-06-02 05:42:15 +02:00
< option value = "" > Select persona...< / option >
2026-05-31 23:58:26 +09:00
< / select >
Rename Character copy to Persona
Issue #234: the "Character" tab and its "Style of response" label made it
unclear that this is where a system prompt is set. Rename the user-facing
labels for clarity:
- "Character" tab + section heading -> "Persona"
- "Style of response" -> "System prompt"
- supporting strings: select placeholder, name placeholder, button/title
text, toasts, confirm/notice text, the chat-bar indicator tooltip, the
settings visibility toggle, and the assistant personality picker
("Characters" optgroup -> "Personas").
Used "Persona" rather than the issue's suggested "Preset" because the app
already has a distinct, user-facing "Presets" concept (built-in presets
like Code Analyze/Brainstorm/Reason, shown as their own group in the
assistant picker). "Persona" matches what this tab actually creates -- a
named persona with its own memories -- without colliding with that term.
Internal identifiers (element IDs, data-chartab attributes, function names)
and the character_name backend field are intentionally left unchanged so
existing saved presets and JS wiring keep working.
2026-06-02 05:42:15 +02:00
< button type = "button" id = "char-new-btn" class = "char-action-btn" title = "Create a new persona" > + New< / button >
2026-05-31 23:58:26 +09:00
< / div >
< div id = "char-name-row" >
< label for = "custom-character-name" > Name< / label >
< div class = "char-name-combo" >
Rename Character copy to Persona
Issue #234: the "Character" tab and its "Style of response" label made it
unclear that this is where a system prompt is set. Rename the user-facing
labels for clarity:
- "Character" tab + section heading -> "Persona"
- "Style of response" -> "System prompt"
- supporting strings: select placeholder, name placeholder, button/title
text, toasts, confirm/notice text, the chat-bar indicator tooltip, the
settings visibility toggle, and the assistant personality picker
("Characters" optgroup -> "Personas").
Used "Persona" rather than the issue's suggested "Preset" because the app
already has a distinct, user-facing "Presets" concept (built-in presets
like Code Analyze/Brainstorm/Reason, shown as their own group in the
assistant picker). "Persona" matches what this tab actually creates -- a
named persona with its own memories -- without colliding with that term.
Internal identifiers (element IDs, data-chartab attributes, function names)
and the character_name backend field are intentionally left unchanged so
existing saved presets and JS wiring keep working.
2026-06-02 05:42:15 +02:00
< input type = "text" id = "custom-character-name" maxlength = "50" placeholder = "Give your persona a name..." autocomplete = "off" style = "flex:1" >
< button type = "button" id = "char-delete-template-btn" class = "char-action-btn" title = "Delete this persona and its memories" style = "display:none;margin-top:-6px !important" > < svg width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:4px" > < polyline points = "3 6 5 6 21 6" / > < path d = "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" / > < line x1 = "10" y1 = "11" x2 = "10" y2 = "17" / > < line x1 = "14" y1 = "11" x2 = "14" y2 = "17" / > < / svg > Delete< / button >
2026-05-31 23:58:26 +09:00
< button type = "button" id = "reset-character-btn" class = "char-action-btn" title = "Reset to default" style = "margin-top:-6px !important" > ↺ Reset< / button >
< / div >
< / div >
Rename Character copy to Persona
Issue #234: the "Character" tab and its "Style of response" label made it
unclear that this is where a system prompt is set. Rename the user-facing
labels for clarity:
- "Character" tab + section heading -> "Persona"
- "Style of response" -> "System prompt"
- supporting strings: select placeholder, name placeholder, button/title
text, toasts, confirm/notice text, the chat-bar indicator tooltip, the
settings visibility toggle, and the assistant personality picker
("Characters" optgroup -> "Personas").
Used "Persona" rather than the issue's suggested "Preset" because the app
already has a distinct, user-facing "Presets" concept (built-in presets
like Code Analyze/Brainstorm/Reason, shown as their own group in the
assistant picker). "Persona" matches what this tab actually creates -- a
named persona with its own memories -- without colliding with that term.
Internal identifiers (element IDs, data-chartab attributes, function names)
and the character_name backend field are intentionally left unchanged so
existing saved presets and JS wiring keep working.
2026-06-02 05:42:15 +02:00
< label for = "custom-system-prompt" > System prompt< / label >
2026-05-31 23:58:26 +09:00
< div class = "char-prompt-wrap" >
< textarea id = "custom-system-prompt" rows = "4" placeholder = "Write rough notes and click Expand, or leave empty" > < / textarea >
Rename Character copy to Persona
Issue #234: the "Character" tab and its "Style of response" label made it
unclear that this is where a system prompt is set. Rename the user-facing
labels for clarity:
- "Character" tab + section heading -> "Persona"
- "Style of response" -> "System prompt"
- supporting strings: select placeholder, name placeholder, button/title
text, toasts, confirm/notice text, the chat-bar indicator tooltip, the
settings visibility toggle, and the assistant personality picker
("Characters" optgroup -> "Personas").
Used "Persona" rather than the issue's suggested "Preset" because the app
already has a distinct, user-facing "Presets" concept (built-in presets
like Code Analyze/Brainstorm/Reason, shown as their own group in the
assistant picker). "Persona" matches what this tab actually creates -- a
named persona with its own memories -- without colliding with that term.
Internal identifiers (element IDs, data-chartab attributes, function names)
and the character_name backend field are intentionally left unchanged so
existing saved presets and JS wiring keep working.
2026-06-02 05:42:15 +02:00
< button type = "button" id = "char-expand-btn" class = "char-expand-btn" title = "AI expand — turn your notes into a full system prompt" >
2026-05-31 23:58:26 +09:00
< svg width = "11" height = "11" viewBox = "0 0 24 24" fill = "currentColor" style = "vertical-align:-1px;margin-right:2px;" > < path d = "M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z" / > < / svg >
Expand
< / button >
< / div >
< / div >
<!-- Group tab -->
< div class = "preset-chartab" data-chartab-panel = "group" style = "display:none" >
< div style = "display:flex;gap:4px;margin-bottom:8px;" >
< button type = "button" class = "compare-parallel-toggle" id = "group-mode-btn" style = "flex:1;height:auto;width:auto;" >
< svg width = "18" height = "18" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" > < line x1 = "8" y1 = "6" x2 = "20" y2 = "6" / > < line x1 = "8" y1 = "12" x2 = "20" y2 = "12" / > < line x1 = "8" y1 = "18" x2 = "20" y2 = "18" / > < circle cx = "4" cy = "6" r = "1.5" fill = "currentColor" / > < circle cx = "4" cy = "12" r = "1.5" fill = "currentColor" / > < circle cx = "4" cy = "18" r = "1.5" fill = "currentColor" / > < / svg >
< span class = "compare-toggle-label" > Sequential< / span >
< / button >
< / div >
< div id = "group-participants" style = "display:flex;flex-direction:column;gap:4px;margin-bottom:0;max-height:220px;overflow-y:auto;" > < / div >
< button type = "button" id = "group-add-btn" class = "preset-save-btn" style = "width:100%;background:none;border:1px dashed var(--border);color:var(--fg);opacity:0.6;font-size:11px;padding:4px;margin-top:0;" > + Add participant< / button >
< / div >
< / div >
< / div >
< div class = "modal-footer" >
< div style = "flex:1" > < / div >
< button type = "button" id = "cancel-custom-preset" style = "margin-right:8px;display:none;" > Cancel< / button >
< button type = "button" id = "save-custom-preset" > Start< / button >
< / div >
< / div >
< / div >
< / main >
< button id = "scroll-bottom-btn" class = "scroll-nav-btn" title = "Scroll to bottom" > ▼< / button >
< script nonce = "{{CSP_NONCE}}" >
(function(){
const container = document.getElementById('chat-history');
const chatBar = document.querySelector('.chat-input-bar');
const bottomBtn = document.getElementById('scroll-bottom-btn');
if (!container || !chatBar || !bottomBtn) return;
function reposition() {
const barRect = chatBar.getBoundingClientRect();
bottomBtn.style.bottom = (window.innerHeight - barRect.top + 16) + 'px';
bottomBtn.style.right = (window.innerWidth - barRect.right + 8) + 'px';
}
function update() {
reposition();
const scrollable = container.scrollHeight > container.clientHeight + 10;
const atBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 300 ;
if (scrollable & & !atBottom) {
bottomBtn.classList.remove('slide-out');
bottomBtn.classList.add('show');
} else if (bottomBtn.classList.contains('show')) {
bottomBtn.classList.add('slide-out');
bottomBtn.classList.remove('show');
}
}
let _scrollRaf = null;
let _scrollTimeout = null;
let _scrollingToBottom = false;
function cancelAutoScroll() {
if (_scrollRaf) { cancelAnimationFrame(_scrollRaf); _scrollRaf = null; }
if (_scrollTimeout) { clearTimeout(_scrollTimeout); _scrollTimeout = null; }
_scrollingToBottom = false;
}
container.addEventListener('scroll', update, {passive:true});
container.addEventListener('wheel', () => { if (_scrollingToBottom) cancelAutoScroll(); }, {passive:true});
container.addEventListener('touchstart', () => { if (_scrollingToBottom) cancelAutoScroll(); }, {passive:true});
bottomBtn.addEventListener('click', () => {
cancelAutoScroll();
const target = container.scrollHeight - container.clientHeight;
_scrollingToBottom = true;
function step() {
if (!_scrollingToBottom) return;
const diff = target - container.scrollTop;
if (diff < = 8) { container.scrollTop = target; _scrollingToBottom = false; return; }
container.scrollTop += diff * 0.2;
_scrollRaf = requestAnimationFrame(step);
}
step();
_scrollTimeout = setTimeout(() => { if (_scrollingToBottom) { container.scrollTop = target; _scrollingToBottom = false; } }, 1500);
});
new ResizeObserver(reposition).observe(chatBar);
new MutationObserver(update).observe(container, {childList:true, subtree:true});
window.addEventListener('resize', reposition);
update();
})();
< / script >
<!-- Rename Session Modal -->
< div id = "rename-session-modal" class = "modal hidden" >
Add dialog accessibility semantics
Screen readers got no signal that a dialog opened — not one modal carried
role="dialog" — and several close buttons had no accessible name.
- The 6 static tool windows (Brain, Theme, Prompt, Rename session, Cookbook,
Settings) now carry role="dialog" + an accessible name. They are dockable,
tiling windows, so they are non-modal dialogs (intentionally no aria-modal).
- The four unlabelled close buttons (theme, prompt, cookbook, settings) get an
aria-label so they no longer read as just "heavy multiplication x".
- styledConfirm / styledPrompt ARE blocking modals: they get role="dialog" +
aria-modal="true" + aria-labelledby/aria-describedby, and now manage focus —
restore focus to the triggering element on close and trap Tab within the
dialog (they already moved focus in on open).
tests/test_dialog_aria.py pins the roles, labels, and focus management.
2026-06-01 23:41:25 -04:00
< div class = "modal-content" role = "dialog" aria-label = "Rename session" style = "width: 400px;" >
2026-05-31 23:58:26 +09:00
< div class = "modal-header" >
< h4 > Rename Session< / h4 >
< button class = "close-btn" id = "close-rename-session" aria-label = "Close rename session modal" > ✖< / button >
< / div >
< div class = "modal-body" >
< div style = "margin-bottom: 12px;" >
< label for = "session-name-input" style = "display: block; margin-bottom: 6px; font-weight: 500;" > Session Name< / label >
2026-06-01 21:11:47 +03:00
< input
type="text"
id="session-name-input"
placeholder="Enter session name"
2026-05-31 23:58:26 +09:00
style="width: 100%; padding: 8px; border-radius: 4px;"
/>
< / div >
< div style = "display: flex; justify-content: flex-end; gap: 8px;" >
< button id = "cancel-rename-session" style = "padding: 6px 12px;" > Cancel< / button >
< button id = "save-session-name" style = "padding: 6px 12px; background: var(--fg); color: var(--bg);" > Save< / button >
< / div >
< / div >
< / div >
< / div >
<!-- Cookbook Modal -->
< div id = "cookbook-modal" class = "modal hidden" >
Add dialog accessibility semantics
Screen readers got no signal that a dialog opened — not one modal carried
role="dialog" — and several close buttons had no accessible name.
- The 6 static tool windows (Brain, Theme, Prompt, Rename session, Cookbook,
Settings) now carry role="dialog" + an accessible name. They are dockable,
tiling windows, so they are non-modal dialogs (intentionally no aria-modal).
- The four unlabelled close buttons (theme, prompt, cookbook, settings) get an
aria-label so they no longer read as just "heavy multiplication x".
- styledConfirm / styledPrompt ARE blocking modals: they get role="dialog" +
aria-modal="true" + aria-labelledby/aria-describedby, and now manage focus —
restore focus to the triggering element on close and trap Tab within the
dialog (they already moved focus in on open).
tests/test_dialog_aria.py pins the roles, labels, and focus management.
2026-06-01 23:41:25 -04:00
< div class = "modal-content" role = "dialog" aria-label = "Cookbook" style = "width: min(780px, 92vw); height: 94vh; max-height: 94vh; background: var(--bg);" >
2026-05-31 23:58:26 +09:00
< div class = "modal-header" >
< h4 style = "margin:0;margin-right:auto" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:6px" > < path d = "M12 7v14" / > < path d = "M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" / > < / svg > Cookbook< / h4 >
Add dialog accessibility semantics
Screen readers got no signal that a dialog opened — not one modal carried
role="dialog" — and several close buttons had no accessible name.
- The 6 static tool windows (Brain, Theme, Prompt, Rename session, Cookbook,
Settings) now carry role="dialog" + an accessible name. They are dockable,
tiling windows, so they are non-modal dialogs (intentionally no aria-modal).
- The four unlabelled close buttons (theme, prompt, cookbook, settings) get an
aria-label so they no longer read as just "heavy multiplication x".
- styledConfirm / styledPrompt ARE blocking modals: they get role="dialog" +
aria-modal="true" + aria-labelledby/aria-describedby, and now manage focus —
restore focus to the triggering element on close and trap Tab within the
dialog (they already moved focus in on open).
tests/test_dialog_aria.py pins the roles, labels, and focus management.
2026-06-01 23:41:25 -04:00
< button class = "close-btn" id = "close-cookbook-modal" aria-label = "Close cookbook" > ✖< / button >
2026-05-31 23:58:26 +09:00
< / div >
< div class = "modal-body cookbook-body" > < / div >
< / div >
< / div >
<!-- Settings Modal (all users) -->
< div id = "settings-modal" class = "modal hidden" >
Add dialog accessibility semantics
Screen readers got no signal that a dialog opened — not one modal carried
role="dialog" — and several close buttons had no accessible name.
- The 6 static tool windows (Brain, Theme, Prompt, Rename session, Cookbook,
Settings) now carry role="dialog" + an accessible name. They are dockable,
tiling windows, so they are non-modal dialogs (intentionally no aria-modal).
- The four unlabelled close buttons (theme, prompt, cookbook, settings) get an
aria-label so they no longer read as just "heavy multiplication x".
- styledConfirm / styledPrompt ARE blocking modals: they get role="dialog" +
aria-modal="true" + aria-labelledby/aria-describedby, and now manage focus —
restore focus to the triggering element on close and trap Tab within the
dialog (they already moved focus in on open).
tests/test_dialog_aria.py pins the roles, labels, and focus management.
2026-06-01 23:41:25 -04:00
< div class = "modal-content settings-modal-content" role = "dialog" aria-label = "Settings" >
2026-05-31 23:58:26 +09:00
< div class = "modal-header" >
< h4 > < span style = "vertical-align:-1px;margin-right:6px;font-size:15px" > ⚙ < / span > Settings< / h4 >
< button type = "button" class = "theme-opacity-wrap theme-opacity-toggle hidden" id = "settings-opacity-wrap" title = "Fade this window to preview the page behind it" aria-pressed = "false" >
< svg width = "11" height = "11" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" aria-hidden = "true" > < path d = "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" > < / path > < circle cx = "12" cy = "12" r = "3" > < / circle > < / svg >
< span class = "theme-opacity-label" > Peek< / span >
< / button >
Add dialog accessibility semantics
Screen readers got no signal that a dialog opened — not one modal carried
role="dialog" — and several close buttons had no accessible name.
- The 6 static tool windows (Brain, Theme, Prompt, Rename session, Cookbook,
Settings) now carry role="dialog" + an accessible name. They are dockable,
tiling windows, so they are non-modal dialogs (intentionally no aria-modal).
- The four unlabelled close buttons (theme, prompt, cookbook, settings) get an
aria-label so they no longer read as just "heavy multiplication x".
- styledConfirm / styledPrompt ARE blocking modals: they get role="dialog" +
aria-modal="true" + aria-labelledby/aria-describedby, and now manage focus —
restore focus to the triggering element on close and trap Tab within the
dialog (they already moved focus in on open).
tests/test_dialog_aria.py pins the roles, labels, and focus management.
2026-06-01 23:41:25 -04:00
< button class = "close-btn" aria-label = "Close settings" > ✖< / button >
2026-05-31 23:58:26 +09:00
< / div >
< div class = "admin-toggle-sub" style = "padding:0 12px 8px;opacity:0.6;font-size:11px;" > Toggle on/off visibility of tools and modules across the interface.< / div >
< div class = "settings-layout" >
< div class = "settings-sidebar" >
<!-- Section 1: AI plumbing (Add Models → AI Defaults → Search) -->
< button class = "settings-nav-item active" data-settings-tab = "services" >
< svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect x = "2" y = "2" width = "20" height = "8" rx = "2" / > < rect x = "2" y = "14" width = "20" height = "8" rx = "2" / > < circle cx = "6" cy = "6" r = "1" / > < circle cx = "6" cy = "18" r = "1" / > < / svg >
< span > Add Models< / span >
< / button >
< button class = "settings-nav-item" data-settings-tab = "ai" >
< svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M12 2a4 4 0 0 0-4 4v2H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2h-2V6a4 4 0 0 0-4-4z" / > < / svg >
< span > AI Defaults< / span >
< / button >
< button class = "settings-nav-item" data-settings-tab = "search" >
< svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < circle cx = "11" cy = "11" r = "8" / > < line x1 = "21" y1 = "21" x2 = "16.65" y2 = "16.65" / > < / svg >
< span > Search< / span >
< / button >
< div class = "settings-sidebar-divider" > < / div >
<!-- Section 2: Comms (Integrations → Email → Reminders).
Integrations leads here because it's where the underlying
accounts the other two tabs depend on actually live. -->
< button class = "settings-nav-item" data-settings-tab = "integrations" >
< svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" / > < path d = "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" / > < / svg >
< span > Integrations< / span >
< / button >
< button class = "settings-nav-item" data-settings-tab = "email" >
< svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect x = "2" y = "4" width = "20" height = "16" rx = "2" / > < path d = "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" / > < / svg >
< span > Email< / span >
< / button >
< button class = "settings-nav-item" data-settings-tab = "reminders" >
< svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" / > < path d = "M13.73 21a2 2 0 0 1-3.46 0" / > < / svg >
< span > Reminders< / span >
< / button >
< div class = "settings-sidebar-divider" > < / div >
<!-- Section 3: UX (Appearance → Shortcuts) -->
< button class = "settings-nav-item" data-settings-tab = "appearance" >
< svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < circle cx = "12" cy = "12" r = "10" / > < path d = "M12 2a7 7 0 0 0 0 20 4 4 0 0 1 0-8 4 4 0 0 0 0-8" / > < / svg >
< span > Appearance< / span >
< / button >
< button class = "settings-nav-item" data-settings-tab = "shortcuts" >
< svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect x = "2" y = "4" width = "20" height = "16" rx = "2" / > < path d = "M6 8h.01M10 8h.01M14 8h.01M18 8h.01M8 12h.01M12 12h.01M16 12h.01M7 16h10" / > < / svg >
< span > Shortcuts< / span >
< / button >
< div class = "settings-sidebar-divider" > < / div >
<!-- Section 4: Account (sits at the bottom of the user section) -->
< button class = "settings-nav-item" data-settings-tab = "account" >
< svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" / > < circle cx = "12" cy = "7" r = "4" / > < / svg >
< span > Account< / span >
< / button >
< div class = "settings-sidebar-divider admin-only" > < / div >
< div class = "settings-sidebar-label admin-only" > Admin< / div >
< button class = "settings-nav-item admin-only" data-settings-tab = "tools" >
< svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" / > < / svg >
< span > Agent Tools< / span >
< / button >
< button class = "settings-nav-item admin-only" data-settings-tab = "users" >
< svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" / > < circle cx = "9" cy = "7" r = "4" / > < path d = "M23 21v-2a4 4 0 0 0-3-3.87" / > < path d = "M16 3.13a4 4 0 0 1 0 7.75" / > < / svg >
< span > Users< / span >
< / button >
< button class = "settings-nav-item admin-only" data-settings-tab = "system" >
< svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < circle cx = "12" cy = "12" r = "3" / > < path d = "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" / > < / svg >
< span > System< / span >
< / button >
< / div >
< div class = "settings-panels" >
<!-- ═══ AI TAB ═══ -->
< div data-settings-panel = "ai" class = "hidden" >
< div class = "admin-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < path d = "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" / > < / svg > Default Chat Model< / h2 >
< div class = "admin-toggle-sub" style = "margin-bottom:8px" > The model used when creating a new chat session.< / div >
< div class = "settings-col" >
< div class = "settings-row" >
< label class = "settings-label" > Endpoint< / label >
< select id = "set-defaultEpSelect" class = "settings-select" > < / select >
< / div >
< div class = "settings-row" >
< label class = "settings-label" > Model< / label >
< select id = "set-defaultModelSelect" class = "settings-select" > < / select >
< / div >
< div id = "set-defaultFallbacks" class = "settings-fallbacks" > < / div >
< button type = "button" class = "settings-fallback-add" id = "set-defaultAddFallback" title = "Add a model to try if the one above fails" > + Add fallback< / button >
< div id = "set-defaultChatMsg" style = "font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);" > < / div >
< / div >
< / div >
< div class = "admin-card" >
< h2 style = "display:flex;align-items:center;gap:6px;" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "margin-right:1px;opacity:0.6;flex-shrink:0" > < path d = "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" / > < / svg > Utility Model < span style = "font-size:0.72em;opacity:0.55;font-weight:normal;" > (Recommended: Local Endpoint)< / span > < / h2 >
2026-06-01 15:32:17 +02:00
< div class = "admin-toggle-sub" style = "margin-bottom:8px" > Runs background tasks (compaction, cleanup, auto-naming, retrieving memories from files) on a small/local model instead of your chat model. Leave blank to use the chat model.< / div >
2026-05-31 23:58:26 +09:00
< div class = "settings-col" >
< div class = "settings-row" >
< label class = "settings-label" > Endpoint< / label >
< select id = "set-utilityEpSelect" class = "settings-select" > < option value = "" > —< / option > < / select >
< / div >
< div class = "settings-row" >
< label class = "settings-label" > Model< / label >
< select id = "set-utilityModelSelect" class = "settings-select" > < option value = "" > —< / option > < / select >
< / div >
< div id = "set-utilityFallbacks" class = "settings-fallbacks" > < / div >
< button type = "button" class = "settings-fallback-add" id = "set-utilityAddFallback" title = "Add a model to try if the utility model fails" > + Add fallback< / button >
< div id = "set-utilityChatMsg" style = "font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);" > < / div >
< / div >
< / div >
< div class = "admin-card" >
< h2 style = "display:flex;align-items:center;gap:6px;" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "margin-right:1px;opacity:0.6;flex-shrink:0" > < path d = "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" / > < circle cx = "12" cy = "12" r = "3" / > < / svg > Vision< span style = "flex:1" > < / span > < label class = "admin-switch" title = "Analyze images with a vision-capable model" > < input type = "checkbox" id = "set-visionEnabledToggle" checked > < span class = "admin-slider" > < / span > < / label > < / h2 >
< div class = "admin-toggle-sub" style = "margin-bottom:8px" > Analyze images with a vision-capable model.< / div >
< div style = "display:flex;flex-direction:column;gap:0.5rem;" >
< div style = "display:flex;align-items:center;gap:0.75rem;" >
< label class = "settings-label" > Model< / label >
< select id = "set-vlModelSelect" class = "settings-select" > < option value = "" > Auto-detect< / option > < / select >
< / div >
< div id = "set-visionFallbacks" class = "settings-fallbacks" > < / div >
< button type = "button" class = "settings-fallback-add" id = "set-visionAddFallback" title = "Add a vision model to try if the one above fails" > + Add fallback< / button >
< div id = "set-visionSettingsMsg" style = "font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);" > < / div >
< / div >
< / div >
< div class = "admin-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < circle cx = "11" cy = "11" r = "8" / > < path d = "M21 21l-4.35-4.35" / > < line x1 = "11" y1 = "8" x2 = "11" y2 = "14" / > < line x1 = "8" y1 = "11" x2 = "14" y2 = "11" / > < / svg > Research Model< / h2 >
< div class = "admin-toggle-sub" style = "margin-bottom:8px" > Model used for Deep Research. Falls back to the default chat model if not set.< / div >
< div class = "settings-col" >
< div class = "settings-row" >
< label class = "settings-label" > Endpoint< / label >
< select id = "set-researchEndpoint" class = "settings-select" >
< option value = "" > Same as chat< / option >
< / select >
< / div >
< div class = "settings-row" >
< label class = "settings-label" > Model< / label >
< select id = "set-researchModel" class = "settings-select" >
< option value = "" > Same as chat< / option >
< / select >
< / div >
< div class = "settings-row" >
< label class = "settings-label" > Search< / label >
< select id = "set-researchSearch" class = "settings-select" >
< option value = "" > Same as web search< / option >
< option value = "searxng" > SearXNG< / option >
< option value = "duckduckgo" > DuckDuckGo< / option >
< option value = "tavily" > Tavily< / option >
< option value = "brave" > Brave< / option >
< option value = "google" > Google< / option >
< option value = "serper" > Serper< / option >
< / select >
< / div >
< div class = "settings-row" >
< label class = "settings-label" > Max Tokens< / label >
< input id = "set-researchMaxTokens" type = "text" inputmode = "numeric" placeholder = "8192 (default)" class = "settings-select" style = "width:120px;" >
< / div >
2026-06-01 14:55:24 +09:00
< div class = "settings-row" >
< label class = "settings-label" > Extract Timeout< / label >
< input id = "set-researchExtractTimeout" type = "text" inputmode = "numeric" placeholder = "90 sec" class = "settings-select" style = "width:120px;" >
< / div >
< div class = "settings-row" >
< label class = "settings-label" > Extract Parallel< / label >
< input id = "set-researchExtractConcurrency" type = "text" inputmode = "numeric" placeholder = "3" class = "settings-select" style = "width:120px;" >
< / div >
2026-05-31 23:58:26 +09:00
< div id = "set-researchMsg" style = "font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);" > < / div >
< / div >
< / div >
< div class = "admin-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < path d = "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" / > < / svg > Agent< / h2 >
< div class = "admin-toggle-sub" style = "margin-bottom:8px" > Controls for the agent tool loop.< / div >
< div class = "settings-col" >
< div class = "settings-row" >
< label class = "settings-label" > Tool call limit< / label >
< input id = "set-agentMaxTools" type = "text" inputmode = "numeric" placeholder = "0 = unlimited" class = "settings-select" style = "width:120px;" >
< / div >
< div id = "set-agentMsg" style = "font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);" > < / div >
< / div >
< / div >
<!-- Image Generation removed — only inpaint remains in this build,
and inpaint is configured via the gallery editor not this card.
Keeping the DOM (hidden) so JS wiring against the inputs
doesn't throw. -->
< div class = "admin-card" hidden style = "display:none" >
< h2 style = "display:flex;align-items:center;gap:6px;" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "margin-right:1px;opacity:0.6;flex-shrink:0" > < rect x = "3" y = "3" width = "18" height = "18" rx = "2" / > < circle cx = "8.5" cy = "8.5" r = "1.5" / > < path d = "M21 15l-5-5L5 21" / > < / svg > Image Generation< span style = "flex:1" > < / span > < label class = "admin-switch" > < input type = "checkbox" id = "set-imgEnabledToggle" checked > < span class = "admin-slider" > < / span > < / label > < / h2 >
< div class = "admin-toggle-sub" style = "margin-bottom:8px" > Configure which model to use for image generation.< / div >
< div style = "display:flex;flex-direction:column;gap:0.5rem;" >
< div style = "display:flex;align-items:center;gap:0.75rem;" >
< label class = "settings-label" > Model< / label >
< select id = "set-imgModelSelect" class = "settings-select" > < option value = "" > Auto-detect< / option > < / select >
< / div >
< div style = "display:flex;align-items:center;gap:0.75rem;" >
< label class = "settings-label" > Quality< / label >
< select id = "set-imgQualitySelect" class = "settings-select" >
< option value = "low" > Low (fastest, cheapest)< / option >
< option value = "medium" > Medium (default)< / option >
< option value = "high" > High (best quality)< / option >
< / select >
< / div >
< div id = "set-imgSettingsMsg" style = "font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);" > < / div >
< / div >
< / div >
<!-- TTS panel hidden — user opted out of read - aloud entirely. Kept
in DOM so the JS that wires set-ttsProviderSelect etc. doesn't
throw on missing nodes; not visible to anyone. -->
< div class = "admin-card" hidden style = "display:none" >
< h2 style = "display:flex;align-items:center;gap:6px;" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < polygon points = "11 5 6 9 2 9 2 15 6 15 11 19 11 5" / > < path d = "M19.07 4.93a10 10 0 0 1 0 14.14" / > < path d = "M15.54 8.46a5 5 0 0 1 0 7.07" / > < / svg > Text to Speech< span style = "flex:1" > < / span > < label class = "admin-switch" > < input type = "checkbox" id = "set-ttsEnabledToggle" checked > < span class = "admin-slider" > < / span > < / label > < / h2 >
< div class = "admin-toggle-sub" style = "margin-bottom:8px" > Configure TTS provider for assistant message read-aloud.< / div >
< div style = "display:flex;flex-direction:column;gap:0.5rem;" >
< div style = "display:flex;align-items:center;gap:0.75rem;" >
< label class = "settings-label" > Provider< / label >
< select id = "set-ttsProviderSelect" class = "settings-select" >
< option value = "disabled" > Disabled< / option >
< option value = "browser" > Browser (built-in)< / option >
< option value = "local" > Local (Kokoro-82M)< / option >
< / select >
< / div >
< div id = "set-ttsModelRow" style = "display:flex;align-items:center;gap:0.75rem;" >
< label class = "settings-label" > Model< / label >
< select id = "set-ttsModelSelect" class = "settings-select" >
< option value = "tts-1" > tts-1 (fast)< / option >
< option value = "tts-1-hd" > tts-1-hd (quality)< / option >
< option value = "gpt-4o-mini-tts" > gpt-4o-mini-tts (steerable)< / option >
< / select >
< input id = "set-ttsModelInput" type = "text" placeholder = "model name" style = "flex:1;padding:5px;display:none;" >
< / div >
< div id = "set-ttsVoiceRow" style = "display:flex;align-items:center;gap:0.75rem;" >
< label class = "settings-label" > Voice< / label >
< select id = "set-ttsVoiceSelect" class = "settings-select" >
< option value = "alloy" > Alloy< / option >
< option value = "ash" > Ash< / option >
< option value = "coral" > Coral< / option >
< option value = "echo" > Echo< / option >
< option value = "fable" > Fable< / option >
< option value = "nova" > Nova< / option >
< option value = "onyx" > Onyx< / option >
< option value = "sage" > Sage< / option >
< option value = "shimmer" > Shimmer< / option >
< / select >
< input id = "set-ttsVoiceInput" type = "text" placeholder = "af_heart" style = "flex:1;padding:5px;display:none;" >
< / div >
< div id = "set-ttsSpeedRow" style = "display:flex;align-items:center;gap:0.75rem;" >
< label class = "settings-label" > Speed< / label >
< select id = "set-ttsSpeedSelect" class = "settings-select" >
< option value = "0.5" > 0.5x< / option >
< option value = "0.75" > 0.75x< / option >
< option value = "1" selected > 1x (normal)< / option >
< option value = "1.25" > 1.25x< / option >
< option value = "1.5" > 1.5x< / option >
< option value = "2" > 2x< / option >
< / select >
< / div >
< button type = "button" id = "set-ttsPreviewBtn" class = "admin-btn-sm" style = "margin-top:4px;padding:5px 16px;font-size:12px;" > Preview< / button >
< div id = "set-ttsSettingsMsg" style = "font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);" > < / div >
< / div >
< / div >
< div class = "admin-card" >
< h2 style = "display:flex;align-items:center;gap:6px;" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < path d = "M22 10v6M2 10l10-5 10 5-10 5z" / > < path d = "M6 12v5c3 3 9 3 12 0v-5" / > < / svg > Teacher Model < span style = "font-size:0.72em;opacity:0.55;font-weight:normal;" > (Experimental)< / span > < span style = "flex:1" > < / span > < label class = "admin-switch" > < input type = "checkbox" id = "set-teacherEnabledToggle" > < span class = "admin-slider" > < / span > < / label > < / h2 >
< div class = "admin-toggle-sub" style = "margin-bottom:8px" > When a self-hosted student fails an agent-mode task, escalate to a SOTA teacher that writes a SKILL.md procedure so the student can do it next time. Off by default.< / div >
< div class = "settings-col" >
< div class = "settings-row" >
< label class = "settings-label" > Endpoint< / label >
< select id = "set-teacherEpSelect" class = "settings-select" > < option value = "" > —< / option > < / select >
< / div >
< div class = "settings-row" >
< label class = "settings-label" > Model< / label >
< select id = "set-teacherModelSelect" class = "settings-select" > < option value = "" > —< / option > < / select >
< / div >
< div id = "set-teacherChatMsg" style = "font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);" > < / div >
< / div >
< / div >
< / div >
<!-- ═══ SEARCH TAB ═══ -->
< div data-settings-panel = "search" class = "hidden" >
< div class = "admin-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < circle cx = "11" cy = "11" r = "8" / > < line x1 = "21" y1 = "21" x2 = "16.65" y2 = "16.65" / > < / svg > Web Search< / h2 >
< div class = "admin-toggle-sub" style = "margin-bottom:8px" > Search API used for web search and deep research.< / div >
< div class = "settings-col" >
< div class = "settings-row" >
< label class = "settings-label" > Provider< / label >
<!-- Custom picker (with logos). Hidden native <select> mirrors
its value so the existing JS that reads
set-searchProvider keeps working unchanged. -->
< div class = "adm-provider-picker search-provider-picker" id = "search-provider-picker" style = "flex:1;position:relative;top:5px;" >
< button type = "button" class = "adm-provider-btn" id = "search-provider-btn" >
< span class = "adm-provider-current" > < span class = "adm-provider-logo" > < / span > < span class = "adm-provider-name" > SearXNG< / span > < / span >
< svg class = "adm-provider-caret" width = "10" height = "10" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" > < polyline points = "6 9 12 15 18 9" / > < / svg >
< / button >
< div class = "adm-provider-menu hidden" id = "search-provider-menu" > < / div >
< / div >
< select id = "set-searchProvider" class = "settings-select" style = "display:none;" >
< option value = "searxng" data-search-logo = "searxng" > SearXNG (self-hosted)< / option >
< option value = "duckduckgo" data-search-logo = "duckduckgo" > DuckDuckGo (free, no key)< / option >
< option value = "brave" data-search-logo = "brave" > Brave Search< / option >
< option value = "google_pse" data-search-logo = "google_pse" > Google PSE< / option >
< option value = "tavily" data-search-logo = "tavily" > Tavily< / option >
< option value = "serper" data-search-logo = "serper" > Serper.dev< / option >
< option value = "disabled" data-search-logo = "disabled" > Disabled< / option >
< / select >
< button type = "button" class = "admin-btn-sm" id = "set-searchTestBtn" title = "Run a test query against the configured provider" style = "margin-left:6px;flex-shrink:0;position:relative;top:2px;" > Test< / button >
< / div >
< div class = "settings-row" >
< label class = "settings-label" > Results< / label >
< select id = "set-searchResultCount" class = "settings-select" >
< option value = "3" > 3< / option >
< option value = "5" selected > 5< / option >
< option value = "10" > 10< / option >
< option value = "20" > 20< / option >
< / select >
< / div >
< div id = "set-searchUrlRow" class = "settings-row" >
< label class = "settings-label" > URL< / label >
< input id = "set-searchUrl" type = "text" placeholder = "http://localhost:8080" class = "settings-select" >
< / div >
< div id = "set-searchKeyRow" class = "settings-row" style = "display:none;" >
< label class = "settings-label" > API Key< / label >
< input id = "set-searchApiKey" type = "password" placeholder = "API key" class = "settings-select" >
< / div >
< div id = "set-searchCxRow" class = "settings-row" style = "display:none;" >
< label class = "settings-label" > CX ID< / label >
< input id = "set-searchCx" type = "text" placeholder = "Google PSE engine ID" class = "settings-select" >
< / div >
< div class = "settings-row" >
< label class = "settings-label" title = "Providers tried in order when the primary fails or hits a rate limit" > Fallbacks< / label >
< div class = "search-fallback-chain" id = "set-searchFallbackChain" > < / div >
< / div >
< div id = "set-searchHint" class = "admin-toggle-sub" > < / div >
< div id = "set-searchMsg" style = "font-size:11px;" > < / div >
< / div >
< / div >
< / div >
<!-- ═══ APPEARANCE TAB ═══ -->
< div data-settings-panel = "appearance" class = "settings-appearance-panel hidden" >
< div class = "admin-card" style = "padding-bottom:6px;" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < rect x = "3" y = "3" width = "18" height = "18" rx = "2" / > < line x1 = "9" y1 = "3" x2 = "9" y2 = "21" / > < / svg > Sidebar< / h2 >
< div class = "vis-toggles" >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" > < circle cx = "12" cy = "12" r = "10" / > < path d = "M8 12l2.5 2.5L16 9" / > < / svg > < / span >
< span class = "vis-label" > Odysseus < span class = "vis-hint" > Brand name< / span > < / span >
< input type = "checkbox" checked data-ui-key = "sidebar-brand" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" > < circle cx = "10" cy = "10" r = "7" / > < path d = "M21 21l-4.35-4.35" / > < / svg > < / span >
< span class = "vis-label" > Search< / span >
< input type = "checkbox" checked data-ui-key = "sidebar-search" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" > < line x1 = "12" y1 = "5" x2 = "12" y2 = "19" / > < line x1 = "5" y1 = "12" x2 = "19" y2 = "12" / > < / svg > < / span >
< span class = "vis-label" > New Chat< / span >
< input type = "checkbox" checked data-ui-key = "sidebar-new-chat" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" / > < / svg > < / span >
< span class = "vis-label" > Chats < span class = "vis-hint" > Chat history list< / span > < / span >
< input type = "checkbox" checked data-ui-key = "sessions-section" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect x = "2" y = "4" width = "20" height = "16" rx = "2" / > < path d = "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" / > < / svg > < / span >
< span class = "vis-label" > Email< / span >
< input type = "checkbox" checked data-ui-key = "email-section" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" > < 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 > < / span >
< span class = "vis-label" > Models < span class = "vis-hint" > Model selector & quick-chat< / span > < / span >
< input type = "checkbox" checked data-ui-key = "models-section" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" > < path d = "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" / > < / svg > < / span >
< span class = "vis-label" > Tools < span class = "vis-hint" > Whole section (header + all tools)< / span > < / span >
< input type = "checkbox" checked data-ui-key = "tools-section" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z" / > < path d = "M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z" / > < path d = "M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4" / > < / svg > < / span >
< span class = "vis-label" > Brain< / span >
< input type = "checkbox" checked data-ui-key = "tool-memory" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect x = "3" y = "4" width = "18" height = "18" rx = "2" / > < line x1 = "16" y1 = "2" x2 = "16" y2 = "6" / > < line x1 = "8" y1 = "2" x2 = "8" y2 = "6" / > < line x1 = "3" y1 = "10" x2 = "21" y2 = "10" / > < / svg > < / span >
< span class = "vis-label" > Calendar< / span >
< input type = "checkbox" checked data-ui-key = "tool-calendar" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < circle cx = "18" cy = "18" r = "3" / > < circle cx = "6" cy = "6" r = "3" / > < path d = "M13 6h3a2 2 0 0 1 2 2v7" / > < path d = "M11 18H8a2 2 0 0 1-2-2V9" / > < / svg > < / span >
< span class = "vis-label" > Compare< / span >
< input type = "checkbox" checked data-ui-key = "tool-compare" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M4 19.5A2.5 2.5 0 0 1 6.5 17H20" / > < path d = "M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" / > < path d = "M9 7h6M9 11h4" / > < / svg > < / span >
< span class = "vis-label" > Cookbook< / span >
< input type = "checkbox" checked data-ui-key = "tool-cookbook" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < circle cx = "11" cy = "11" r = "8" / > < path d = "M21 21l-4.35-4.35" / > < line x1 = "11" y1 = "8" x2 = "11" y2 = "14" / > < line x1 = "8" y1 = "11" x2 = "14" y2 = "11" / > < / svg > < / span >
< span class = "vis-label" > Deep Research< / span >
< input type = "checkbox" checked data-ui-key = "tool-research" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect x = "3" y = "3" width = "18" height = "18" rx = "2" / > < circle cx = "8.5" cy = "8.5" r = "1.5" / > < path d = "M21 15l-5-5L5 21" / > < / svg > < / span >
< span class = "vis-label" > Gallery< / span >
< input type = "checkbox" checked data-ui-key = "tool-gallery" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" / > < polyline points = "14 2 14 8 20 8" / > < line x1 = "8" y1 = "13" x2 = "16" y2 = "13" / > < line x1 = "8" y1 = "17" x2 = "13" y2 = "17" / > < / svg > < / span >
< span class = "vis-label" > Library< / span >
< input type = "checkbox" checked data-ui-key = "tool-library" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect x = "3" y = "3" width = "18" height = "18" rx = "2" / > < path d = "M9 7h6M9 11h6M9 15h4" / > < / svg > < / span >
< span class = "vis-label" > Notes< / span >
< input type = "checkbox" checked data-ui-key = "tool-notes" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect x = "3" y = "4" width = "18" height = "18" rx = "2" / > < line x1 = "16" y1 = "2" x2 = "16" y2 = "6" / > < line x1 = "8" y1 = "2" x2 = "8" y2 = "6" / > < line x1 = "3" y1 = "10" x2 = "21" y2 = "10" / > < path d = "M9 16l2 2 4-4" / > < / svg > < / span >
< span class = "vis-label" > Tasks< / span >
< input type = "checkbox" checked data-ui-key = "tool-tasks" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < circle cx = "12" cy = "12" r = "10" / > < path d = "M12 2a10 10 0 0 0 0 20 5 5 0 0 0 5-5 3 3 0 0 0-3-3h-2a3 3 0 0 1-3-3 5 5 0 0 1 5-5" / > < / svg > < / span >
< span class = "vis-label" > Theme< / span >
< input type = "checkbox" checked data-ui-key = "tool-theme" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" / > < circle cx = "12" cy = "7" r = "4" / > < / svg > < / span >
< span class = "vis-label" > User < span class = "vis-hint" > Avatar & name< / span > < / span >
< input type = "checkbox" checked data-ui-key = "user-bar" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < circle cx = "12" cy = "12" r = "3" / > < path d = "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" / > < / svg > < / span >
< span class = "vis-label" > Settings Button < span class = "vis-hint" > Cog next to user — re-open with < code > /settings< / code > < / span > < / span >
< input type = "checkbox" checked data-ui-key = "sidebar-settings-btn" > < span class = "vis-switch" > < / span >
< / label >
< / div >
< / div >
< div class = "admin-card" style = "padding-bottom:6px;" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < path d = "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" / > < / svg > Chat Area< / h2 >
< div class = "vis-toggles" >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" > < path d = "M4 6h16" / > < path d = "M4 10h8" / > < / svg > < / span >
< span class = "vis-label" > Session Header < span class = "vis-hint" > Model name & export above chat< / span > < / span >
< input type = "checkbox" checked data-ui-key = "chat-meta" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" > < path d = "M12 3v2m0 14v2m-7-9H3m18 0h-2m-1.5-6.5L16 7m-8-1.5L6.5 7m11 11l-1.5-1.5M8 18l-1.5 1.5" / > < circle cx = "12" cy = "12" r = "4" / > < / svg > < / span >
< span class = "vis-label" > Welcome Message < span class = "vis-hint" > Logo & tips on empty chat< / span > < / span >
< input type = "checkbox" checked data-ui-key = "welcome-text" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" / > < circle cx = "12" cy = "12" r = "3" / > < / svg > < / span >
< span class = "vis-label" > Incognito Mode < span class = "vis-hint" > No memory, no history saved< / span > < / span >
< input type = "checkbox" checked data-ui-key = "incognito-btn" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" style = "font-size:13px;line-height:14px;" > Aa< / span >
< span class = "vis-label" > Text-only Emojis < span class = "vis-hint" > Strip emojis from AI replies< / span > < / span >
< input type = "checkbox" data-ui-key = "text-emojis" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" > < path d = "M12 2a7 7 0 0 0-7 7c0 3 2 5.5 4.5 7.5.7.6 1.2 1.3 1.5 2h2c.3-.7.8-1.4 1.5-2C17 14.5 19 12 19 9a7 7 0 0 0-7-7z" / > < line x1 = "10" y1 = "22" x2 = "14" y2 = "22" / > < / svg > < / span >
< span class = "vis-label" > Thinking Process < span class = "vis-hint" > Show < think> collapsible bars< / span > < / span >
< input type = "checkbox" checked data-ui-key = "show-thinking" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z" / > < circle cx = "12" cy = "12" r = "3" / > < path d = "M4 4l16 16" / > < / svg > < / span >
< span class = "vis-label" > Sensitive Blur < span class = "vis-hint" > Blur emails, tokens, and secrets in AI output< / span > < / span >
< input type = "checkbox" data-privacy-key = "sensitive-blur" > < span class = "vis-switch" > < / span >
< / label >
< / div >
< / div >
< div class = "admin-card" style = "padding-bottom:6px;" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < line x1 = "17" y1 = "10" x2 = "3" y2 = "10" / > < line x1 = "21" y1 = "6" x2 = "3" y2 = "6" / > < line x1 = "21" y1 = "14" x2 = "3" y2 = "14" / > < line x1 = "17" y1 = "18" x2 = "3" y2 = "18" / > < / svg > Chat Bar< / h2 >
< div class = "vis-toggles" >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < circle cx = "11" cy = "11" r = "8" / > < line x1 = "21" y1 = "21" x2 = "16.65" y2 = "16.65" / > < / svg > < / span >
< span class = "vis-label" > Web Search< / span >
< input type = "checkbox" checked data-ui-key = "web-toggle-btn" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" / > < polyline points = "14 2 14 8 20 8" / > < line x1 = "16" y1 = "13" x2 = "8" y2 = "13" / > < line x1 = "16" y1 = "17" x2 = "8" y2 = "17" / > < / svg > < / span >
< span class = "vis-label" > Document Editor< / span >
< input type = "checkbox" checked data-ui-key = "doc-toggle-btn" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < polyline points = "4 17 10 11 4 5" / > < line x1 = "12" y1 = "19" x2 = "20" y2 = "19" / > < / svg > < / span >
< span class = "vis-label" > Shell< / span >
< input type = "checkbox" checked data-ui-key = "bash-toggle-btn" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" > < polyline points = "18 15 12 9 6 15" / > < / svg > < / span >
< span class = "vis-label" > More Tools < span class = "vis-hint" > Overflow menu< / span > < / span >
< input type = "checkbox" checked data-ui-key = "overflow-plus-btn" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon vis-icon-text" > A|C< / span >
< span class = "vis-label" > Agent / Chat < span class = "vis-hint" > Mode switcher< / span > < / span >
< input type = "checkbox" checked data-ui-key = "mode-toggle" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" / > < / svg > < / span >
< span class = "vis-label" > Attach Files< / span >
< input type = "checkbox" checked data-ui-key = "attach-btn" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < circle cx = "11" cy = "11" r = "8" / > < path d = "M21 21l-4.35-4.35" / > < line x1 = "11" y1 = "8" x2 = "11" y2 = "14" / > < line x1 = "8" y1 = "11" x2 = "14" y2 = "11" / > < / svg > < / span >
< span class = "vis-label" > Deep Research< / span >
< input type = "checkbox" checked data-ui-key = "research-btn" > < span class = "vis-switch" > < / span >
< / label >
< label class = "vis-row" >
< span class = "vis-icon" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" / > < circle cx = "12" cy = "7" r = "4" / > < / svg > < / span >
Rename Character copy to Persona
Issue #234: the "Character" tab and its "Style of response" label made it
unclear that this is where a system prompt is set. Rename the user-facing
labels for clarity:
- "Character" tab + section heading -> "Persona"
- "Style of response" -> "System prompt"
- supporting strings: select placeholder, name placeholder, button/title
text, toasts, confirm/notice text, the chat-bar indicator tooltip, the
settings visibility toggle, and the assistant personality picker
("Characters" optgroup -> "Personas").
Used "Persona" rather than the issue's suggested "Preset" because the app
already has a distinct, user-facing "Presets" concept (built-in presets
like Code Analyze/Brainstorm/Reason, shown as their own group in the
assistant picker). "Persona" matches what this tab actually creates -- a
named persona with its own memories -- without colliding with that term.
Internal identifiers (element IDs, data-chartab attributes, function names)
and the character_name backend field are intentionally left unchanged so
existing saved presets and JS wiring keep working.
2026-06-02 05:42:15 +02:00
< span class = "vis-label" > Personas < span class = "vis-hint" > Persona picker & system prompt< / span > < / span >
2026-05-31 23:58:26 +09:00
< input type = "checkbox" checked data-ui-key = "preset-mini-btn" > < span class = "vis-switch" > < / span >
< / label >
< / div >
< / div >
< div style = "text-align:right;padding:0 4px;" >
< button type = "button" class = "admin-btn-sm" id = "set-uiVisResetBtn" style = "opacity:0.5;" > Reset All< / button >
< / div >
< / div >
<!-- ═══ THEME TAB ═══ -->
<!-- ═══ MEMORY TAB ═══ -->
<!-- ═══ SHORTCUTS TAB ═══ -->
< div data-settings-panel = "shortcuts" class = "hidden" >
< div class = "admin-card" style = "display:flex;align-items:center;justify-content:space-between;padding:10px 16px;" >
< div >
< h2 style = "margin:0;font-size:13px;" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < rect x = "2" y = "4" width = "20" height = "16" rx = "2" / > < path d = "M6 8h.01M10 8h.01M14 8h.01M18 8h.01M8 12h.01M12 12h.01M16 12h.01M7 16h10" / > < / svg > Keyboard Shortcuts< / h2 >
< p style = "font-size:10px;opacity:0.4;margin:2px 0 0;" > Click a shortcut to rebind. Press Escape to cancel.< / p >
< / div >
< button type = "button" class = "shortcut-action-btn is-reset" id = "shortcuts-reset-btn" title = "Reset Shortcuts" style = "width:28px;height:28px;font-size:15px;" > ↩ < / button >
< / div >
< div class = "admin-card" >
< div id = "shortcuts-list" > < / div >
< / div >
< / div >
<!-- ═══ ACCOUNT TAB ═══ -->
< div data-settings-panel = "account" class = "hidden" >
< div class = "admin-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < path d = "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" / > < circle cx = "12" cy = "7" r = "4" / > < / svg > Account< / h2 >
< div style = "display:flex;align-items:center;gap:10px;margin:4px 0 12px;" >
< div class = "user-bar-avatar" id = "settings-account-avatar" style = "width:32px;height:32px;font-size:14px;" > < / div >
< div style = "flex:1;" >
< div id = "settings-account-username" style = "font-size:13px;font-weight:600;" > < / div >
< div id = "settings-account-role" style = "font-size:11px;opacity:0.5;" > < / div >
< / div >
< button id = "settings-logout-btn" style = "background:color-mix(in srgb, var(--color-error) 10%, transparent);border:1px solid var(--color-error);border-radius:6px;padding:6px 12px;color:var(--color-error);font-family:inherit;font-size:12px;font-weight:500;cursor:pointer;transition:opacity 0.15s,background 0.15s;display:inline-flex;align-items:center;gap:6px;" >
< svg width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" / > < polyline points = "16 17 21 12 16 7" / > < line x1 = "21" y1 = "12" x2 = "9" y2 = "12" / > < / svg >
Logout
< / button >
< / div >
< / div >
< div class = "admin-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < rect x = "3" y = "11" width = "18" height = "11" rx = "2" ry = "2" / > < path d = "M7 11V7a5 5 0 0 1 10 0v4" / > < / svg > Change Password< / h2 >
< div class = "settings-col" >
< input id = "settings-pw-current" type = "password" placeholder = "Current password" autocomplete = "current-password" style = "padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg);font-family:inherit;font-size:12px;" >
< input id = "settings-pw-new" type = "password" placeholder = "New password (min 8)" autocomplete = "new-password" style = "padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg);font-family:inherit;font-size:12px;" >
< input id = "settings-pw-confirm" type = "password" placeholder = "Confirm new password" autocomplete = "new-password" style = "padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg);font-family:inherit;font-size:12px;" >
< div class = "settings-row" style = "margin-top:2px;justify-content:flex-end;" >
< span id = "settings-pw-msg" style = "font-size:11px;margin-right:auto;" > < / span >
< button class = "admin-btn-add" id = "settings-pw-save" > Update Password< / button >
< / div >
< / div >
< / div >
< div class = "admin-card" id = "settings-2fa-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < rect x = "3" y = "11" width = "18" height = "11" rx = "2" ry = "2" / > < path d = "M7 11V7a5 5 0 0 1 10 0v4" / > < circle cx = "12" cy = "16" r = "1" / > < / svg > Two-Factor Authentication< / h2 >
< div id = "settings-2fa-content" >
<!-- Populated by JS -->
< / div >
< / div >
< / div >
<!-- ═══ EMAIL TAB ═══ -->
< div data-settings-panel = "email" class = "hidden" >
< div class = "admin-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < rect x = "2" y = "4" width = "20" height = "16" rx = "2" / > < path d = "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" / > < / svg > Email Accounts< / h2 >
2026-06-01 13:01:33 +09:00
< div class = "settings-row" style = "align-items:center;" >
< div class = "admin-toggle-sub" style = "margin:0;flex:1;" > Add, edit, delete, and test accounts in Integrations.< / div >
< button class = "admin-btn-add" id = "set-email-open-integrations" > Manage in Integrations< / button >
2026-05-31 23:58:26 +09:00
< / div >
< / div >
< div class = "admin-card" >
2026-06-01 20:56:11 +09:00
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < rect x = "3" y = "4" width = "18" height = "18" rx = "2" / > < line x1 = "16" y1 = "2" x2 = "16" y2 = "6" / > < line x1 = "8" y1 = "2" x2 = "8" y2 = "6" / > < line x1 = "3" y1 = "10" x2 = "21" y2 = "10" / > < path d = "M9 16l2 2 4-4" / > < / svg > Email Tasks< / h2 >
< div class = "settings-row" style = "align-items:center;" >
< div class = "admin-toggle-sub" style = "margin:0;flex:1;" > Manage email background tasks in Tasks.< / div >
< button class = "admin-btn-add" id = "set-email-open-tasks" > Open Tasks< / button >
< / div >
< / div >
< div class = "admin-card" >
2026-05-31 23:58:26 +09:00
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < path d = "M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" / > < path d = "M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" / > < / svg > Writing Style< / h2 >
< div class = "admin-toggle-sub" style = "margin-bottom:8px" > AI-extracted from your sent emails. Used when AI drafts replies.< / div >
< div class = "settings-col" >
< textarea id = "set-email-style" rows = "4" class = "settings-select" style = "font-family:inherit;resize:vertical" placeholder = "e.g. I write emails in this style. I don't use exclamation marks. I sign emails with: ..." > < / textarea >
< div class = "settings-row" style = "margin-top:4px" >
< span id = "set-email-style-msg" style = "font-size:11px;" > < / span >
< button class = "admin-btn-add" id = "set-email-style-extract" style = "margin-left:auto;" > Extract from Sent (15 emails)< / button >
< button class = "admin-btn-add" id = "set-email-style-save" > Save< / button >
< / div >
< / div >
< / div >
< / div >
<!-- ═══ REMINDERS TAB ═══ -->
< div data-settings-panel = "reminders" class = "hidden" >
< div class = "admin-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < path d = "M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" / > < path d = "M13.73 21a2 2 0 0 1-3.46 0" / > < / svg > How you're reminded< / h2 >
< div class = "admin-toggle-sub" style = "margin-bottom:8px" > Controls how fired note reminders are delivered.< / div >
< div class = "settings-col" >
< div class = "settings-row" >
< label class = "settings-label" > Channel< / label >
< select id = "set-reminder-channel" class = "settings-select" >
< option value = "browser" > Browser notification (default)< / option >
< option value = "email" id = "set-reminder-channel-email-opt" > Email< / option >
< option value = "ntfy" id = "set-reminder-channel-ntfy-opt" > ntfy< / option >
< / select >
< / div >
< div id = "set-reminder-email-from-row" class = "settings-row" style = "display:none" >
< label class = "settings-label" > Send from< / label >
< select id = "set-reminder-email-account" class = "settings-select" > < / select >
< / div >
< div id = "set-reminder-email-to-row" class = "settings-row" style = "display:none" >
< label class = "settings-label" > Send to< / label >
< input id = "set-reminder-email-to" class = "settings-select" type = "email" placeholder = "you@example.com" / >
< / div >
< div id = "set-reminder-ntfy-topic-row" class = "settings-row" style = "display:none" >
< label class = "settings-label" > ntfy topic< / label >
< input id = "set-reminder-ntfy-topic" class = "settings-select" type = "text" placeholder = "reminders" / >
< / div >
< div id = "set-reminder-channel-hint" style = "font-size:11px;opacity:0.6;" > < / div >
< div style = "font-size:11px;opacity:0.6;margin-top:4px;" > Configure email account, ntfy server, etc. in < a href = "#" id = "set-reminders-open-integrations" style = "color:var(--accent, var(--red));text-decoration:none;font-weight:600;" > Integrations< / a > .< / div >
< / div >
< / div >
< div class = "admin-card" >
< h2 style = "display:flex;align-items:center;gap:6px;" > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "margin-right:1px;opacity:0.6;flex-shrink:0" > < path d = "M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z" / > < / svg > AI Synthesis< span style = "flex:1" > < / span > < label class = "admin-switch" title = "Use the utility model to write reminder messages" > < input type = "checkbox" id = "set-reminder-llm-toggle" > < span class = "admin-slider" > < / span > < / label > < / h2 >
< div class = "admin-toggle-sub" style = "margin-bottom:8px" > When on, the utility model writes a short, warm one-line reminder for browser, email, AND ntfy reminders instead of just the raw note content.< / div >
< / div >
< div class = "admin-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < path d = "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" / > < path d = "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" / > < / svg > Public App URL< / h2 >
< div class = "admin-toggle-sub" style = "margin-bottom:8px" > Used to build clickable links back to Odysseus inside outgoing reminder / urgent-email emails (e.g. < code > https://chat.yourdomain.com< / code > ). Leave blank to omit links.< / div >
< div class = "settings-col" >
< div class = "settings-row" >
< label class = "settings-label" > URL< / label >
< input id = "set-app-public-url" class = "settings-select" type = "url" placeholder = "https://chat.example.com" style = "flex:1;" / >
< / div >
< div id = "set-app-public-url-msg" style = "font-size:11px;color:color-mix(in srgb, var(--fg) 55%, transparent);" > < / div >
< / div >
< / div >
< div class = "admin-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < path d = "M22 11.08V12a10 10 0 1 1-5.93-9.14" / > < polyline points = "22 4 12 14.01 9 11.01" / > < / svg > Test< / h2 >
< div class = "admin-toggle-sub" style = "margin-bottom:8px" > Fire a test reminder using your current settings to verify everything works.< / div >
< div class = "settings-row" >
< span id = "set-reminder-test-msg" style = "font-size:11px;" > < / span >
< button class = "admin-btn-add" id = "set-reminder-test-btn" style = "margin-left:auto;" > Send Test Reminder< / button >
< / div >
< / div >
< / div >
<!-- ═══ ADMIN: USERS TAB ═══ -->
< div data-settings-panel = "users" class = "hidden" >
< div class = "admin-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < path d = "M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" / > < circle cx = "8.5" cy = "7" r = "4" / > < line x1 = "20" y1 = "8" x2 = "20" y2 = "14" / > < line x1 = "23" y1 = "11" x2 = "17" y2 = "11" / > < / svg > Registration< / h2 >
< div class = "admin-toggle-row" >
< div >
< div class = "admin-toggle-label" > Open signup< / div >
< div class = "admin-toggle-sub" > Allow anyone to create an account from the login page< / div >
< / div >
< label class = "admin-switch" > < input type = "checkbox" id = "adm-signupToggle" > < span class = "admin-slider" > < / span > < / label >
< / div >
< / div >
< div class = "admin-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < path d = "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" / > < circle cx = "9" cy = "7" r = "4" / > < path d = "M23 21v-2a4 4 0 0 0-3-3.87" / > < path d = "M16 3.13a4 4 0 0 1 0 7.75" / > < / svg > Users< / h2 >
< div id = "adm-userList" > < div class = "admin-empty" > Loading...< / div > < / div >
< / div >
< div class = "admin-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < path d = "M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" / > < circle cx = "8.5" cy = "7" r = "4" / > < line x1 = "20" y1 = "8" x2 = "20" y2 = "14" / > < line x1 = "23" y1 = "11" x2 = "17" y2 = "11" / > < / svg > Add User< / h2 >
< div class = "admin-add-form" >
< input id = "adm-newUsername" type = "text" placeholder = "Username (email)" >
< input id = "adm-newPassword" type = "password" placeholder = "Password (min 8)" >
< div class = "admin-switch-inline" title = "Grant full admin access" > < label class = "admin-switch" > < input type = "checkbox" id = "adm-newIsAdmin" > < span class = "admin-slider" > < / span > < / label > Admin< / div >
< / div >
< div class = "settings-row" style = "margin-top:6px;" >
< button class = "admin-btn-add" id = "adm-addBtn" > Add User< / button >
< span id = "adm-addMsg" style = "font-size:11px" > < / span >
< / div >
< / div >
< / div >
<!-- ═══ SERVICES TAB ═══ -->
< div data-settings-panel = "services" >
< div class = "admin-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < rect x = "2" y = "2" width = "20" height = "8" rx = "2" / > < rect x = "2" y = "14" width = "20" height = "8" rx = "2" / > < circle cx = "6" cy = "6" r = "1" / > < circle cx = "6" cy = "18" r = "1" / > < / svg > Add Models < span style = "opacity:0.45;font-weight:normal;font-size:0.82em" > (Endpoints)< / span > < / h2 >
2026-06-01 10:00:15 +09:00
< div class = "admin-toggle-sub" style = "margin-bottom:10px" > Connect local models first, or add a cloud API.< / div >
2026-05-31 23:58:26 +09:00
<!-- Local subsection -->
< div class = "adm-add-section collapsible collapsed" id = "adm-add-local" >
< div class = "adm-ep-section-head adm-section-toggle" role = "button" tabindex = "0" aria-expanded = "false" >
< svg width = "11" height = "11" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-1px;margin-right:4px;" > < rect x = "2" y = "3" width = "20" height = "14" rx = "2" / > < path d = "M8 21h8" / > < path d = "M12 17v4" / > < / svg >
< span > Local< / span >
< svg class = "adm-section-caret" width = "11" height = "11" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" > < polyline points = "6 9 12 15 18 9" / > < / svg >
< / div >
< div class = "admin-model-form" >
< div class = "admin-model-form-row" >
2026-06-01 10:00:15 +09:00
< input id = "adm-epLocalUrl" type = "text" placeholder = "Paste endpoint URL, e.g. http://localhost:11434/v1" style = "flex:1" >
< select id = "adm-epLocalType" style = "padding:5px;width:72px;flex-shrink:0;" >
2026-05-31 23:58:26 +09:00
< option value = "llm" > LLM< / option >
< option value = "image" > Image< / option >
< / select >
2026-06-01 10:00:15 +09:00
< / div >
< div class = "admin-model-form-row" >
< span style = "flex:1" > < / span >
< button class = "admin-btn-sm" id = "adm-epLocalTestBtn" style = "width:55px;text-align:center;" > Test< / button >
2026-05-31 23:58:26 +09:00
< button class = "admin-btn-add" id = "adm-epLocalAddBtn" style = "width:55px;text-align:center;" > Add< / button >
< / div >
2026-06-01 10:00:15 +09:00
< div class = "adm-quickstart-section collapsed" id = "adm-add-local-quickstart" >
< div class = "adm-quickstart-toggle" role = "button" tabindex = "0" aria-expanded = "false" >
< span > Quickstart< / span >
< svg class = "adm-section-caret" width = "11" height = "11" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" > < polyline points = "6 9 12 15 18 9" / > < / svg >
< / div >
< div class = "adm-quickstart-body" >
< button class = "admin-btn-sm" id = "adm-epDiscoverBtn" title = "Scan your network for running model servers" >
< svg width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" style = "vertical-align:-1px;margin-right:4px;" > < circle cx = "11" cy = "11" r = "8" / > < line x1 = "21" y1 = "21" x2 = "16.65" y2 = "16.65" / > < / svg > Scan for Servers
< / button >
< button class = "admin-btn-sm" id = "adm-epOllamaBtn" title = "Fill the default Ollama endpoint" > Ollama< / button >
< / div >
< / div >
< div id = "adm-epLocalMsg" class = "adm-ep-inline-msg" > < / div >
2026-05-31 23:58:26 +09:00
< / div >
< / div >
<!-- API subsection -->
< div class = "adm-add-section collapsible collapsed" id = "adm-add-api" style = "margin-top:14px" >
< div class = "adm-ep-section-head adm-section-toggle" role = "button" tabindex = "0" aria-expanded = "false" >
< svg width = "11" height = "11" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-1px;margin-right:4px;" > < circle cx = "12" cy = "12" r = "10" / > < line x1 = "2" y1 = "12" x2 = "22" y2 = "12" / > < path d = "M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" / > < / svg >
< span > API< / span >
< svg class = "adm-section-caret" width = "11" height = "11" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" > < polyline points = "6 9 12 15 18 9" / > < / svg >
< / div >
< div class = "admin-model-form" >
<!-- Custom picker (with logos). Hidden native <select> mirrors
its value so the existing JS that reads adm-epProvider
keeps working unchanged. -->
2026-06-01 10:00:15 +09:00
< div class = "adm-provider-picker adm-provider-combo" id = "adm-provider-picker" >
< input id = "adm-epUrl" type = "text" placeholder = "Base URL or pick provider" autocomplete = "off" >
< button type = "button" class = "adm-provider-btn" id = "adm-provider-btn" title = "Pick provider" >
< span class = "adm-provider-current" > < span class = "adm-provider-logo" > < / span > < span class = "adm-provider-name" > Provider< / span > < / span >
2026-05-31 23:58:26 +09:00
< svg class = "adm-provider-caret" width = "10" height = "10" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" > < polyline points = "6 9 12 15 18 9" / > < / svg >
< / button >
< div class = "adm-provider-menu hidden" id = "adm-provider-menu" > < / div >
< / div >
< select id = "adm-epProvider" style = "display:none" >
< option value = "" > Custom URL< / option >
< option value = "https://api.anthropic.com" data-logo = "anthropic" > Anthropic< / option >
2026-06-01 10:00:15 +09:00
< option value = "https://api.deepseek.com/v1" data-logo = "deepseek" selected > DeepSeek< / option >
2026-05-31 23:58:26 +09:00
< option value = "https://api.openai.com/v1" data-logo = "openai" > OpenAI< / option >
< option value = "https://openrouter.ai/api/v1" data-logo = "openrouter" > OpenRouter< / option >
2026-06-01 15:26:10 +10:00
< option value = "https://ollama.com/api" data-logo = "ollama" > Ollama Cloud< / option >
2026-05-31 23:58:26 +09:00
< option value = "https://api.groq.com/openai/v1" data-logo = "groq" > Groq< / option >
< option value = "https://api.mistral.ai/v1" data-logo = "mistral" > Mistral< / option >
< option value = "https://api.together.xyz/v1" data-logo = "together" > Together AI< / option >
< option value = "https://api.fireworks.ai/inference/v1" data-logo = "fireworks" > Fireworks AI< / option >
< option value = "https://generativelanguage.googleapis.com/v1beta/openai" data-logo = "gemini" > Google Gemini< / option >
< option value = "https://api.x.ai/v1" data-logo = "grok" > xAI Grok< / option >
< option value = "https://api.z.ai/api/paas/v4" data-logo = "zhipu" > Z.AI (Zhipu)< / option >
< / select >
< div class = "admin-model-form-row" >
< input id = "adm-epApiKey" type = "password" placeholder = "API key" >
< select id = "adm-epType" style = "padding:5px;width:80px;" >
< option value = "llm" > LLM< / option >
< option value = "image" > Image< / option >
< / select >
2026-06-01 10:00:15 +09:00
< button class = "admin-btn-sm" id = "adm-epApiTestBtn" style = "width:55px;text-align:center;" > Test< / button >
< button class = "admin-btn-sm hidden" id = "adm-epApiCancelTestBtn" style = "width:62px;text-align:center;" > Cancel< / button >
2026-05-31 23:58:26 +09:00
< button class = "admin-btn-add" id = "adm-epAddBtn" style = "width:55px;text-align:center;" > Add< / button >
< / div >
2026-06-01 10:00:15 +09:00
< div id = "adm-epApiMsg" class = "adm-ep-inline-msg" > < / div >
2026-05-31 23:58:26 +09:00
< / div >
< / div >
< / div >
< div class = "admin-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < rect x = "2" y = "3" width = "20" height = "14" rx = "2" / > < line x1 = "8" y1 = "21" x2 = "16" y2 = "21" / > < line x1 = "12" y1 = "17" x2 = "12" y2 = "21" / > < / svg > Added Models < span style = "opacity:0.45;font-weight:normal;font-size:0.82em" > (Endpoints)< / span > < / h2 >
< div class = "admin-toggle-sub" style = "margin-bottom:10px" > Manage the endpoints you've added.< / div >
< div class = "adm-ep-section" >
< div class = "adm-ep-section-head" >
< svg width = "11" height = "11" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-1px;margin-right:4px;" > < rect x = "2" y = "3" width = "20" height = "14" rx = "2" / > < path d = "M8 21h8" / > < path d = "M12 17v4" / > < / svg >
< span > Local< / span >
< / div >
< div id = "adm-epList-local" > < div class = "admin-empty" > Loading...< / div > < / div >
< / div >
< div class = "adm-ep-section" style = "margin-top:14px" >
< div class = "adm-ep-section-head" >
< svg width = "11" height = "11" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-1px;margin-right:4px;" > < circle cx = "12" cy = "12" r = "10" / > < line x1 = "2" y1 = "12" x2 = "22" y2 = "12" / > < path d = "M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" / > < / svg >
< span > API< / span >
< / div >
< div id = "adm-epList-api" > < / div >
< / div >
< / div >
< / div >
<!-- ═══ TOOLS TAB ═══ -->
<!-- ═══ INTEGRATIONS TAB ═══ -->
< div data-settings-panel = "integrations" class = "hidden" >
< div class = "admin-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < path d = "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" / > < path d = "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" / > < / svg > Connections< / h2 >
< div class = "admin-toggle-sub" style = "margin-bottom:8px" > All external service connections in one place.< / div >
< div id = "unified-integrations-list" > < / div >
< div id = "unified-intg-form" style = "display:none" > < / div >
< div style = "text-align:center;padding:8px 0;" >
< button type = "button" class = "admin-btn-sm" id = "unified-intg-add-btn" > + Add Integration< / button >
< / div >
< / div >
< / div >
<!-- ═══ TOOLS TAB ═══ -->
< div data-settings-panel = "tools" class = "hidden" >
< div class = "admin-card" style = "margin-bottom:12px;" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < path d = "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" / > < / svg > Built-in Tools< / h2 >
< div class = "admin-toggle-sub" style = "margin-bottom:8px" > Enable or disable tools available to the AI agent.< / div >
< div id = "adm-builtin-tools-list" class = "admin-user-list" > < / div >
< / div >
< / div >
<!-- ═══ SYSTEM TAB ═══ -->
< div data-settings-panel = "system" class = "hidden" >
2026-06-01 21:11:47 +03:00
2026-05-31 23:58:26 +09:00
< div class = "admin-card" >
< h2 > < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "vertical-align:-2px;margin-right:5px;opacity:0.6" > < path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" / > < polyline points = "17 8 12 3 7 8" / > < line x1 = "12" y1 = "3" x2 = "12" y2 = "15" / > < / svg > Data Backup< / h2 >
< div class = "admin-toggle-sub" style = "margin-bottom:8px" > Export or import your user data (memories, presets, settings, skills, preferences) as a JSON file.< / div >
< div style = "display:flex;gap:8px;flex-wrap:wrap;" >
< button class = "admin-btn-add" id = "adm-exportDataBtn" > Export Data< / button >
< button class = "admin-btn-add" id = "adm-importDataBtn" > Import Data< / button >
< input type = "file" id = "adm-importFile" accept = ".json" style = "display:none;" >
< / div >
< div id = "adm-backupMsg" style = "margin-top:6px;" > < / div >
< / div >
< div class = "admin-card admin-danger-card" >
< h2 style = "color:#e55;" > Danger Zone< / h2 >
< div class = "admin-toggle-sub" style = "margin-bottom:8px" > Irreversible. Each wipe targets one category — pick exactly what you want gone.< / div >
< div style = "display:flex;justify-content:space-between;align-items:center;" >
< div >
< div class = "admin-toggle-label" > Wipe all chats< / div >
< div class = "admin-toggle-sub" > Every session, message, and chat history. Documents/notes/etc. stay.< / div >
< / div >
< button class = "admin-btn-delete" data-wipe-kind = "chats" style = "white-space:nowrap;" > Wipe< / button >
< / div >
< div style = "display:flex;justify-content:space-between;align-items:center;margin-top:8px;" >
< div >
< div class = "admin-toggle-label" > Wipe all memory< / div >
< div class = "admin-toggle-sub" > Clears `memory.json`, the Memory table, and the vector store. Skills not affected.< / div >
< / div >
< button class = "admin-btn-delete" data-wipe-kind = "memory" style = "white-space:nowrap;" > Wipe< / button >
< / div >
< div style = "display:flex;justify-content:space-between;align-items:center;margin-top:8px;" >
< div >
< div class = "admin-toggle-label" > Wipe all skills< / div >
< div class = "admin-toggle-sub" > Drops `data/skills/` (all SKILL.md files). Memory not affected.< / div >
< / div >
< button class = "admin-btn-delete" data-wipe-kind = "skills" style = "white-space:nowrap;" > Wipe< / button >
< / div >
< div style = "display:flex;justify-content:space-between;align-items:center;margin-top:8px;" >
< div >
< div class = "admin-toggle-label" > Wipe all notes< / div >
< div class = "admin-toggle-sub" > Every note, todo, and checklist.< / div >
< / div >
< button class = "admin-btn-delete" data-wipe-kind = "notes" style = "white-space:nowrap;" > Wipe< / button >
< / div >
< div style = "display:flex;justify-content:space-between;align-items:center;margin-top:8px;" >
< div >
< div class = "admin-toggle-label" > Wipe all tasks< / div >
< div class = "admin-toggle-sub" > Every scheduled task and its run history (Tasks tool).< / div >
< / div >
< button class = "admin-btn-delete" data-wipe-kind = "tasks" style = "white-space:nowrap;" > Wipe< / button >
< / div >
< div style = "display:flex;justify-content:space-between;align-items:center;margin-top:8px;" >
< div >
< div class = "admin-toggle-label" > Wipe all documents< / div >
< div class = "admin-toggle-sub" > Every document and version. Drafts, exports, library — all gone.< / div >
< / div >
< button class = "admin-btn-delete" data-wipe-kind = "documents" style = "white-space:nowrap;" > Wipe< / button >
< / div >
< div style = "display:flex;justify-content:space-between;align-items:center;margin-top:8px;" >
< div >
< div class = "admin-toggle-label" > Wipe all gallery< / div >
< div class = "admin-toggle-sub" > Every image record and the upload directory on disk.< / div >
< / div >
< button class = "admin-btn-delete" data-wipe-kind = "gallery" style = "white-space:nowrap;" > Wipe< / button >
< / div >
< div style = "display:flex;justify-content:space-between;align-items:center;margin-top:8px;" >
< div >
< div class = "admin-toggle-label" > Wipe all calendar< / div >
< div class = "admin-toggle-sub" > Every event and every calendar (incl. CalDAV-synced ones; resync to restore).< / div >
< / div >
< button class = "admin-btn-delete" data-wipe-kind = "calendar" style = "white-space:nowrap;" > Wipe< / button >
< / div >
< div id = "adm-wipeMsg" style = "margin-top:8px;" > < / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- Search overlay (Ctrl+K command palette) -->
< div class = "search-overlay hidden" id = "search-overlay" >
< div class = "search-popup" >
< input type = "text" id = "search-input" placeholder = "Search conversations..." autocomplete = "off" / >
< div class = "search-results" id = "search-results" > < / div >
< / div >
< / div >
<!-- Toast container (aria - live for screen readers) -->
< div id = "toast" class = "toast" role = "status" aria-live = "polite" > < / div >
<!-- Load modules in this order -->
< script type = "module" src = "/static/js/storage.js" > < / script >
< script type = "module" src = "/static/js/ui.js" > < / script >
< script type = "module" src = "/static/js/markdown.js" > < / script >
< script type = "module" src = "/static/js/dragSort.js" > < / script >
< script type = "module" src = "/static/js/sessions.js" > < / script >
< script type = "module" src = "/static/js/memory.js" > < / script >
< script type = "module" src = "/static/js/skills.js" > < / script >
< script type = "module" src = "/static/js/tourHints.js" > < / script >
< script type = "module" src = "/static/js/tourAutoplay.js" > < / script >
< script type = "module" src = "/static/js/fileHandler.js" > < / script >
< script type = "module" src = "/static/js/voiceRecorder.js" > < / script >
< script type = "module" src = "/static/js/models.js" > < / script > <!-- This must come BEFORE app.js -->
< script type = "module" src = "/static/js/rag.js" > < / script >
< script type = "module" src = "/static/js/presets.js" > < / script >
< script type = "module" src = "/static/js/search.js" > < / script >
< script type = "module" src = "/static/js/spinner.js" > < / script >
< script type = "module" src = "/static/js/tts-ai.js" > < / script >
< script type = "module" src = "/static/js/document.js" > < / script >
< script type = "module" src = "/static/js/gallery.js" > < / script >
< script type = "module" src = "/static/js/chatRenderer.js" > < / script >
< script type = "module" src = "/static/js/codeRunner.js" > < / script >
< script type = "module" src = "/static/js/chatStream.js" > < / script >
< script type = "module" src = "/static/js/chat.js?v=20260520m" > < / script >
< script type = "module" src = "/static/js/cookbook.js" > < / script >
< script type = "module" src = "/static/js/search-chat.js" > < / script >
< script type = "module" src = "/static/js/compare/index.js" > < / script >
< script type = "module" src = "/static/js/theme.js" > < / script >
< script type = "module" src = "/static/js/censor.js" > < / script >
< script type = "module" src = "/static/js/settings.js" > < / script >
< script type = "module" src = "/static/js/admin.js" > < / script >
< script type = "module" src = "/static/js/assistant.js" > < / script >
< script type = "module" src = "/static/app.js" > < / script > <!-- app.js must be LAST -->
< script type = "module" src = "/static/js/init.js" > < / script >
Improve accessibility across core flows (#86)
First incremental pass at issue #86, focused on the universal entry
points and primary navigation. All changes verified in-browser with the
axe-core engine (0 violations on the surfaces below) plus manual keyboard
testing, on both desktop (1280px) and mobile (390px).
Login / first-run setup (static/login.html)
- Add a real <h1>, wrap content in <main> + <footer> landmarks.
- Mark the decorative boat SVG aria-hidden.
- Errors now use role="alert" so screen readers announce them.
- "Remember me" checkbox is keyboard-focusable (was display:none) with an
accessible name and a focus ring; dynamic 2FA field gets a linked label.
- Darken the brand-red submit button so white text clears WCAG AA 4.5:1
(was ~3.2:1); add visible :focus-visible rings.
App shell (static/index.html, static/style.css)
- Remove invalid role="region" from the <main> chat container (it was
overriding the implicit main landmark).
- Add a persistent, visually-hidden <h1> inside <main> so the page always
exposes one logical level-1 heading — works even on mobile where the
sidebar (with the visible brand) is hidden off-canvas.
- Add a reusable .a11y-visually-hidden utility.
- Raise chat-title, model-picker, settings-helper and notes text contrast
above 4.5:1 (were 2.8-3.9:1).
Keyboard nav + dialogs (static/js/a11y.js - new)
- Make the click-only <div> sidebar navigation (New Chat, Search, Brain,
Calendar, Compare, Cookbook, Deep Research, Gallery, Library, Notes,
Tasks, Theme, account) focusable and Enter/Space-activatable, announced
as buttons (skipping role=button where a nested control would create a
nested-interactive violation). Visible focus ring reused from existing
.list-item:focus-visible.
- Upgrade modals (.modal-content and the docked .notes-pane) to labelled
role="dialog" + aria-modal, and normalise their title to heading level 2
so heading order stays valid. A MutationObserver covers runtime-rendered
rows and modals.
Decorative background canvases (static/js/theme.js)
- Mark all 7 bg-effect canvases aria-hidden.
Notes & Tasks (static/js/notes.js, static/js/tasks.js)
- Label the icon-only Note/To-do toggle pills (fixes a critical
button-name issue) and track aria-pressed state.
- Improve Notes header-button + empty-state contrast.
- Give the Tasks sort <select> an accessible name (fixes a critical
select-name issue).
Remaining data-dense tool modals (Tasks cards, Calendar, Gallery, Email,
Cookbook, Compare, Deep Research) still have muted-text contrast to polish
and are the next incremental step, per the issue's own guidance.
2026-06-01 21:05:43 +02:00
< script type = "module" src = "/static/js/a11y.js" > < / script >
2026-05-31 23:58:26 +09:00
< script nonce = "{{CSP_NONCE}}" > if ( 'serviceWorker' in navigator ) { navigator . serviceWorker . register ( '/static/sw.js' ) . catch ( ( ) => { } ) ; } < / script >
< / body >
< / html >