<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>cAIc</title>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
    --bg-primary: #080c1a;
    --bg-secondary: #0d1428;
    --bg-tertiary: #121d3a;
    --bg-hover: #182850;
    --text-primary: #a0b8ff;
    --text-secondary: #7890d8;
    --text-muted: #5068a0;
    --accent: #33ddff;
    --accent-dim: #0088bb;
    --accent-glow: rgba(51,221,255,0.1);
    --danger: #ff3355;
    --danger-hover: #cc0022;
    --success: #00ff66;
    --warning: #ffaa00;
    --border: #1a3060;
    --scrollbar: var(--accent-dim);
    --scrollbar-hover: var(--accent);
    --scrollbar-track: rgba(0,136,187,0.08);
    --radius: 6px;
    --font-body: 'JetBrains Mono', 'Consolas', monospace;
    --font-mono: 'JetBrains Mono', 'Consolas', monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: var(--font-body); background: var(--bg-primary); color: var(--text-primary); height: 100vh; overflow: hidden; display: flex; font-size: 13px; }
::selection { background: var(--accent); color: var(--bg-primary); }
::-webkit-scrollbar { width: 10px; }
::-webkit-scrollbar-track { background: var(--scrollbar-track); }
::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 5px; border: 2px solid transparent; background-clip: content-box; }
::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-hover); border: 2px solid transparent; background-clip: content-box; }

.chat-container { scrollbar-width: auto; scrollbar-color: var(--scrollbar) var(--scrollbar-track); }

.chat-container::-webkit-scrollbar { width: 10px; }
.chat-container::-webkit-scrollbar-track { background: var(--scrollbar-track); }
.chat-container::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 5px; border: 2px solid transparent; background-clip: content-box; }
.chat-container::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-hover); border: 2px solid transparent; background-clip: content-box; }

/* --- slide-out drawer (replaces sidebar) --- */
.drawer-overlay { position:fixed; inset:0; background:rgba(0,0,0,0.55); z-index:90; display:none; }
.drawer-overlay.visible { display:block; }
.drawer { position:fixed; left:-300px; top:0; width:300px; height:100vh; background:var(--bg-secondary); border-right:1px solid var(--border); z-index:100; display:flex; flex-direction:column; transition:left 0.22s cubic-bezier(0.4,0,0.2,1); }
.drawer.open { left:0; }
.drawer-header { display:flex; align-items:center; justify-content:space-between; padding:14px 16px; border-bottom:1px solid var(--border); flex-shrink:0; }
.drawer-title { font-family:var(--font-body); font-size:12px; color:var(--text-primary); text-transform:uppercase; letter-spacing:1px; }
.drawer-title .version { color:var(--text-muted); }
.drawer-actions { display:flex; gap:4px; }
.drawer-btn { background:transparent; border:1px solid var(--border); color:var(--text-secondary); width:28px; height:28px; border-radius:var(--radius); cursor:pointer; font-family:var(--font-body); font-size:14px; display:flex; align-items:center; justify-content:center; transition:all 0.15s; }
.drawer-btn:hover { background:var(--bg-hover); border-color:var(--accent-dim); color:var(--accent); }
.drawer-btn.danger:hover { border-color:var(--danger); color:var(--danger); }
.drawer-conv-list { flex:1; overflow-y:auto; padding:6px 10px; }
.conv-item { padding:8px 10px; border-radius:var(--radius); cursor:pointer; margin-bottom:1px; display:flex; align-items:center; gap:8px; font-size:12px; color:var(--text-secondary); transition:background 0.12s; }
.conv-item:hover { background:var(--bg-hover); color:var(--text-primary); }
.conv-item.active { background:var(--bg-tertiary); color:var(--text-primary); border-left:2px solid var(--accent); border-radius:0 var(--radius) var(--radius) 0; }
.conv-item .conv-trash { color:var(--text-muted); cursor:pointer; font-size:13px; flex-shrink:0; opacity:0; transition:opacity 0.12s; }
.conv-item:hover .conv-trash { opacity:1; }
.conv-item .conv-trash:hover { color:var(--danger); }
.conv-item .conv-title { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1; min-width:0; }
.conv-attach { color:var(--text-muted); cursor:pointer; font-size:12px; flex-shrink:0; opacity:0; transition:opacity 0.12s; }
.conv-item:hover .conv-attach { opacity:1; }
.conv-attach.has-attachments { color:var(--accent-dim); opacity:1; }
.drawer-footer { padding:10px 14px; border-top:1px solid var(--border); flex-shrink:0; }
.df-row { display:flex; align-items:center; gap:6px; font-size:10px; color:var(--text-muted); margin-bottom:3px; }
.df-row .df-label { text-transform:uppercase; color:var(--text-muted); width:40px; flex-shrink:0; }
.stats-compact { display:flex; gap:12px; margin-top:8px; padding-top:8px; border-top:1px solid var(--border); flex-wrap:wrap; }
.sc-item { font-size:10px; color:var(--text-muted); display:flex; gap:4px; align-items:center; }
.sc-label { color:var(--text-muted); text-transform:uppercase; }
.cluster-panel { margin-top:6px; }
.cluster-node-row { display:flex; align-items:center; gap:6px; padding:2px 0; font-size:10px; }
.cluster-node-name { color:var(--text-primary); min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.cluster-node-model { color:var(--text-muted); }
.cluster-dot { width:6px; height:6px; border-radius:50%; flex-shrink:0; }
.cluster-dot.active { background:var(--success); }
.cluster-dot.swapping { background:var(--warning); animation:pulse 1s infinite; }
.cluster-dot.error { background:var(--danger); }
.cluster-dot.offline { background:var(--danger); }

.main { flex:1; display:flex; flex-direction:column; height:100vh; min-width:0; position:relative; }
.main::before, .main::after { content:''; position:absolute; top:0; bottom:0; width:24px; z-index:1; pointer-events:none; background-image:radial-gradient(circle at 12px 14px, rgba(0,0,0,0.25) 2px, transparent 2px),radial-gradient(circle at 12px 14px, var(--bg-primary) 5px, var(--bg-tertiary) 5.5px); background-size:24px 28px; background-repeat:repeat-y; border:1px solid var(--border); background-color:var(--bg-tertiary); }
.main::before { left:0; border-left:none; border-radius:0 4px 4px 0; }
.main::after { right:0; border-right:none; border-radius:4px 0 0 4px; }
.topbar { display:flex; align-items:center; justify-content:space-between; padding:8px 28px; border-bottom:1px solid var(--border); background:var(--bg-secondary); flex-shrink:0; gap:12px; }
.topbar-left { display:flex; align-items:center; gap:10px; min-width:0; }
.sys-stats { display:flex; align-items:center; justify-content:center; gap:16px; padding:4px 28px; background:var(--bg-primary); border-top:1px solid var(--border); font-size:11px; font-family:var(--font-mono); color:var(--text-muted); flex-shrink:0; overflow-x:auto; white-space:nowrap; }
.sys-stats .ss-item { display:flex; align-items:center; gap:3px; }
.sys-stats .ss-label { color:var(--text-muted); text-transform:uppercase; }
.sys-stats .ss-val { transition:color 0.3s; }
.sys-stats .ss-val.green { color:var(--success); }
.sys-stats .ss-val.orange { color:var(--warning); }
.sys-stats .ss-val.red { color:var(--danger); }

.topbar-right { display:flex; align-items:center; gap:6px; }
.hamburger { background:transparent; border:1px solid var(--border); color:var(--text-secondary); width:30px; height:30px; border-radius:var(--radius); cursor:pointer; font-size:16px; display:flex; align-items:center; justify-content:center; transition:all 0.15s; flex-shrink:0; }
.hamburger:hover { background:var(--bg-hover); border-color:var(--accent-dim); color:var(--accent); }
.topbar-brand { font-family:var(--font-body); font-size:16px; color:#e0e8ff; white-space:nowrap; letter-spacing:0.5px; display:flex; align-items:center; gap:6px; }
.topbar-brand .version { color:var(--text-muted); font-size:13px; }
.badge { font-size:12px; padding:3px 10px; border-radius:10px; font-family:var(--font-body); cursor:pointer; border:1px solid var(--border); background:transparent; color:var(--text-muted); transition:all 0.15s; letter-spacing:0.5px; }
.badge.on { border-color:rgba(0,255,102,0.3); color:var(--success); background:rgba(0,255,102,0.08); }
.badge.off { border-color:rgba(255,51,85,0.3); color:var(--danger); background:rgba(255,51,85,0.08); }
.badge.admin { border-color:var(--border); color:var(--text-muted); }
.badge.admin:hover { border-color:var(--accent-dim); color:var(--accent); }
.theme-picker { position:relative; }
.theme-dropdown { display:none; position:absolute; top:100%; right:0; margin-top:6px; background:var(--bg-secondary); border:1px solid var(--border); border-radius:var(--radius); min-width:150px; z-index:2000; box-shadow:0 4px 20px rgba(0,0,0,0.4); }
.theme-dropdown.open { display:block; }
.theme-option { display:flex; align-items:center; gap:8px; padding:8px 14px; font-size:12px; font-family:var(--font-mono); color:var(--text-secondary); cursor:pointer; transition:background 0.12s; }
.theme-option:hover { background:var(--bg-hover); color:var(--text-primary); }
.theme-option.active { color:var(--accent); background:var(--accent-glow); }
.status-menu { position:relative; }
.status-menu-btn { background:transparent; border:1px solid var(--border); color:var(--text-secondary); width:30px; height:30px; border-radius:var(--radius); cursor:pointer; font-size:16px; display:flex; align-items:center; justify-content:center; transition:all 0.15s; flex-shrink:0; }
.status-menu-btn:hover { background:var(--bg-hover); border-color:var(--accent-dim); color:var(--accent); }
.status-dropdown { display:none; position:absolute; top:100%; right:0; margin-top:6px; background:var(--bg-secondary); border:1px solid var(--border); border-radius:var(--radius); min-width:180px; z-index:2000; box-shadow:0 4px 20px rgba(0,0,0,0.4); font-family:var(--font-mono); font-size:12px; }
.status-dropdown.open { display:block; }
.status-dropdown-item { display:flex; align-items:center; justify-content:space-between; padding:8px 14px; color:var(--text-secondary); cursor:pointer; transition:background 0.12s; gap:10px; }
.status-dropdown-item:hover { background:var(--bg-hover); color:var(--text-primary); }
.status-dropdown-item .sdi-label { flex:1; white-space:nowrap; }
.status-dropdown-item .sdi-toggle { font-size:10px; padding:2px 8px; border-radius:8px; border:1px solid; }
.status-dropdown-item .sdi-toggle.on { border-color:rgba(0,255,102,0.3); color:var(--success); background:rgba(0,255,102,0.08); }
.status-dropdown-item .sdi-toggle.off { border-color:rgba(255,51,85,0.3); color:var(--danger); background:rgba(255,51,85,0.08); }
.status-dropdown-divider { height:1px; background:var(--border); margin:4px 0; }
.theme-swatch { width:14px; height:14px; border-radius:3px; border:1px solid var(--border); flex-shrink:0; }
.swatch-ibm { background:#0088bb; }
.swatch-green { background:#00cc33; }
.swatch-dark { background:#7b68ee; }
.swatch-light { background:#e0e0e0; }
.swatch-amber { background:#cc8800; }
.swatch-trip { background:linear-gradient(135deg,#ff00ff,#ffff00,#00ff66); }
.info-icon { display:inline-flex; align-items:center; justify-content:center; width:18px; height:18px; border-radius:50%; border:1px solid var(--border); color:var(--text-muted); font-size:11px; cursor:pointer; user-select:none; transition:all 0.15s; }
.info-icon:hover { border-color:var(--accent-dim); color:var(--accent); }
.info-popup-overlay { position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:1000; display:flex; align-items:center; justify-content:center; }
.info-popup-modal { background:var(--bg-secondary); border:1px solid var(--border); border-radius:12px; padding:24px; max-width:400px; width:90%; font-size:13px; line-height:1.7; color:var(--text-primary); position:relative; box-shadow:0 8px 32px rgba(0,0,0,0.5); }
.info-popup-close { position:absolute; top:8px; right:12px; background:none; border:none; color:var(--text-muted); font-size:22px; cursor:pointer; line-height:1; }
.info-popup-close:hover { color:var(--text-primary); }
.role-label .msg-time { color:var(--text-muted); font-weight:400; font-size:10px; margin-left:6px; }
.status-dot.offline { background:var(--danger); }
.status-dot.warning { background:var(--warning); }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }

.modal-overlay { display:none; position:fixed; top:0;left:0;right:0;bottom:0; background:rgba(0,0,0,0.7); z-index:1000; align-items:center; justify-content:center; }
.modal-overlay.visible { display:flex; }
.modal { background:var(--bg-secondary); border:1px solid var(--border); border-radius:12px; width:90%; max-width:700px; max-height:85vh; overflow-y:auto; }
.modal-header { display:flex; justify-content:space-between; align-items:center; padding:20px 24px 16px; border-bottom:1px solid var(--border); position:sticky; top:0; background:var(--bg-secondary); z-index:1; }
.modal-header h2 { font-family:var(--font-mono); font-size:16px; color:var(--accent); }
.modal-close { background:none; border:none; color:var(--text-muted); font-size:24px; cursor:pointer; }
.modal-close:hover { color:var(--text-primary); }
.modal-body { padding: 20px 24px; }
.rag-stats-bar { display:flex; flex-wrap:wrap; gap:12px 24px; padding:10px 14px; background:var(--bg-tertiary); border-radius:var(--radius); margin-bottom:14px; font-size:11px; font-family:var(--font-mono); }
.rag-stat-item { display:flex; gap:4px; align-items:center; }
.rag-stat-label { color:var(--text-muted); text-transform:uppercase; }
.rag-stat-value { color:var(--text-primary); font-weight:600; }
.rag-toolbar { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:12px; flex-wrap:wrap; }
.rag-toolbar-left { display:flex; gap:8px; align-items:center; flex:1; }
.rag-toolbar-right { display:flex; gap:8px; align-items:center; flex-shrink:0; }
.rag-search-input { flex:1; min-width:160px; background:var(--bg-tertiary); border:1px solid var(--border); color:var(--text-primary); font-family:var(--font-mono); font-size:12px; padding:8px 12px; border-radius:var(--radius); }
.rag-search-input:focus { outline:none; border-color:var(--accent-dim); }
.rag-search-hint { font-size:10px; color:var(--text-muted); margin:-6px 0 12px 0; font-family:var(--font-mono); line-height:1.4; }
.rag-source-select { background:var(--bg-tertiary); border:1px solid var(--border); color:var(--text-primary); font-family:var(--font-mono); font-size:12px; padding:7px 10px; border-radius:var(--radius); cursor:pointer; }
.rag-source-select:focus { outline:none; border-color:var(--accent-dim); }
.rag-table-wrap { overflow-x:auto; margin-bottom:12px; }
.rag-table { width:100%; border-collapse:collapse; font-size:11px; font-family:var(--font-mono); }
.rag-table th { text-align:left; padding:8px 6px; border-bottom:1px solid var(--border); color:var(--text-secondary); text-transform:uppercase; letter-spacing:0.5px; font-size:10px; white-space:nowrap; }
.rag-table td { padding:8px 6px; border-bottom:1px solid var(--border); color:var(--text-primary); vertical-align:top; }
.rag-table tr:hover td { background:var(--bg-hover); }
.rag-col-id { width:80px; }
.rag-col-text { min-width:200px; }
.rag-col-source { width:100px; }
.rag-col-type { width:80px; }
.rag-col-date { width:140px; }
.rag-col-retr { width:50px; text-align:center; }
.rag-col-actions { width:80px; text-align:center; white-space:nowrap; }
.rag-text-snippet { display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; line-height:1.4; word-break:break-word; cursor:pointer; }
.rag-text-snippet.expanded { -webkit-line-clamp:unset; }
.rag-action-btn { background:none; border:1px solid var(--border); border-radius:var(--radius); cursor:pointer; font-size:11px; padding:3px 7px; color:var(--text-muted); transition:all 0.15s; }
.rag-action-btn:hover { border-color:var(--accent-dim); color:var(--accent); }
.rag-action-btn.danger:hover { border-color:var(--danger); color:var(--danger); }
.rag-pagination { display:flex; align-items:center; justify-content:center; gap:12px; padding:8px 0; }
.rag-page-info { font-size:11px; color:var(--text-muted); font-family:var(--font-mono); }
.rag-edit-modal { max-width:700px; }
.rag-edit-textarea { width:100%; min-height:200px; background:var(--bg-tertiary); border:1px solid var(--border); color:var(--text-primary); font-family:var(--font-mono); font-size:12px; padding:12px; border-radius:var(--radius); resize:vertical; line-height:1.6; }
.rag-edit-textarea:focus { outline:none; border-color:var(--accent-dim); }
.modal-section { margin-bottom: 24px; }
.modal-section h3 { font-family:var(--font-mono); font-size:13px; color:var(--text-secondary); text-transform:uppercase; letter-spacing:1px; margin-bottom:8px; }
.modal-section p.desc { font-size:12px; color:var(--text-muted); margin-bottom:10px; line-height:1.5; }
.modal-section textarea { width:100%; background:var(--bg-tertiary); border:1px solid var(--border); color:var(--text-primary); font-family:var(--font-mono); font-size:12px; padding:12px; border-radius:var(--radius); resize:vertical; line-height:1.6; }
.modal-section textarea:focus { outline:none; border-color:var(--accent-dim); }
.token-count { font-size:11px; color:var(--text-muted); font-family:var(--font-mono); margin-top:4px; text-align:right; }
.toggle-row { display:flex; align-items:center; justify-content:space-between; padding:8px 0; }
.toggle-label { font-size:13px; }
.toggle-switch { position:relative; width:44px; height:24px; background:var(--bg-tertiary); border:1px solid var(--border); border-radius:12px; cursor:pointer; transition:background 0.2s; }
.toggle-switch.on { background:var(--accent-dim); border-color:var(--accent-dim); }
.toggle-switch::after { content:''; position:absolute; top:2px; left:2px; width:18px; height:18px; background:var(--text-primary); border-radius:50%; transition:transform 0.2s; }
.toggle-switch.on::after { transform:translateX(20px); }
.btn-small { padding:6px 14px; border-radius:var(--radius); font-family:var(--font-mono); font-size:12px; cursor:pointer; border:1px solid var(--border); transition:all 0.2s; }
.btn-save { background:var(--accent-dim); color:#fff; border-color:var(--accent-dim); }
.btn-save:hover { background:var(--accent); }
.btn-reset { background:transparent; color:var(--text-muted); }
.btn-reset:hover { color:var(--danger); border-color:var(--danger); }
.btn-bar { display:flex; gap:8px; margin-top:10px; }
.preset-item { display:flex; align-items:center; gap:8px; padding:8px 10px; background:var(--bg-tertiary); border-radius:var(--radius); margin-bottom:6px; font-size:13px; }
.preset-item .preset-name { flex:1; color:var(--text-primary); }
.preset-item button { background:none; border:none; color:var(--text-muted); cursor:pointer; font-size:13px; padding:2px 4px; }
.preset-item button:hover { color:var(--text-primary); }
.memory-item { display:flex; align-items:flex-start; gap:8px; padding:8px 10px; background:var(--bg-tertiary); border-radius:var(--radius); margin-bottom:6px; font-size:12px; }
.memory-item .memory-fact { flex:1; color:var(--text-primary); line-height:1.4; }
.memory-item .memory-topic { font-size:10px; color:var(--accent); background:var(--accent-glow); padding:2px 6px; border-radius:4px; }
.memory-item .memory-delete { color:var(--danger); cursor:pointer; opacity:0.5; }
.memory-item .memory-delete:hover { opacity:1; }
.memory-stats { font-size:11px; color:var(--text-muted); margin-bottom:10px; font-family:var(--font-mono); }
.skills-status { font-size:11px; color:var(--text-muted); margin-bottom:10px; font-family:var(--font-mono); }
.skill-item { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; padding:8px 10px; background:var(--bg-tertiary); border-radius:var(--radius); margin-bottom:6px; }
.skill-meta { min-width:0; }
.skill-name { font-size:12px; color:var(--text-primary); font-family:var(--font-mono); margin-bottom:3px; }
.skill-desc { font-size:11px; color:var(--text-muted); line-height:1.4; }
.skill-risk { display:inline-block; margin-left:8px; padding:1px 6px; border-radius:10px; font-size:10px; text-transform:uppercase; border:1px solid var(--border); color:var(--text-secondary); }
.skill-item.disabled .skill-meta { opacity:0.6; }

.chat-container { flex:1; overflow-y:auto; padding:20px 30px; display:flex; flex-direction:column; gap:8px; background-image:radial-gradient(rgba(255,255,255,0.015) 1px, transparent 1px); background-size:24px 24px; margin-right:28px; }
.msg-pair { display:flex; flex-direction:column; gap:8px; border-radius:var(--radius); padding:12px 12px 8px; border-left:3px solid transparent; }
.msg-pair:nth-child(odd) { background:rgba(255,255,255,0.015); border-left-color:rgba(0,136,187,0.30); }
.msg-pair:nth-child(even) { background:rgba(255,255,255,0.025); border-left-color:rgba(0,136,187,0.12); }
.welcome-screen { flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; color:var(--text-muted); text-align:center; gap:12px; }
.welcome-screen .logo { font-family:var(--font-mono); font-size:48px; color:var(--accent-dim); opacity:0.5; }
.welcome-screen .welcome-logo { max-width:70%; height:auto; object-fit:contain; opacity:0.85; }
.welcome-screen p { font-size:14px; max-width:420px; line-height:1.6; }
.ws-hint { font-size:12px; color:var(--text-muted); display:block; margin-top:8px; }
.ws-hint code { background:var(--bg-tertiary); padding:1px 5px; border-radius:3px; font-family:var(--font-mono); font-size:11px; }
.message { display:flex; gap:12px; max-width:900px; width:100%; margin:0 auto; animation:fadeIn 0.2s ease; }
@keyframes fadeIn { from{opacity:0;transform:translateY(6px)} to{opacity:1;transform:translateY(0)} }
.message .avatar { width:32px; height:32px; min-width:32px; border-radius:6px; display:flex; align-items:center; justify-content:center; font-family:var(--font-mono); font-size:13px; font-weight:600; margin-top:2px; }
.message.user .avatar { background:#1a3a5c; color:var(--accent); }
.message.assistant .avatar { background:var(--accent-dim); color:#fff; }
.message .content { flex:1; min-width:0; }
.message .content .role-label { font-size:11px; font-weight:600; letter-spacing:0.5px; margin-bottom:4px; color:var(--text-muted); font-family:var(--font-mono); }
.message .content .text { font-size:14px; line-height:1.65; word-wrap:break-word; overflow-wrap:break-word; }
.message .content .text pre { background:var(--bg-primary); border:1px solid var(--border); border-radius:var(--radius); padding:12px; margin:8px 0; overflow-x:auto; font-family:var(--font-mono); font-size:13px; line-height:1.5; position:relative; }
.message .content .text code { font-family:var(--font-mono); background:var(--bg-primary); padding:2px 5px; border-radius:3px; font-size:13px; }
.message .content .text pre code { background:none; padding:0; }
.copy-btn { position:absolute; top:6px; right:6px; background:var(--bg-tertiary); border:1px solid var(--border); color:var(--text-muted); font-family:var(--font-mono); font-size:11px; padding:3px 8px; border-radius:4px; cursor:pointer; }
.copy-btn:hover { color:var(--text-primary); }
.typing-indicator { display:inline-flex; gap:4px; padding:4px 0; }
.typing-indicator span { width:6px; height:6px; background:var(--accent-dim); border-radius:50%; animation:blink 1.4s infinite; }
.typing-indicator.stopped span { background:var(--text-muted); animation:none; opacity:0.4; }
.typing-indicator span:nth-child(2) { animation-delay:0.2s; }
.typing-indicator span:nth-child(3) { animation-delay:0.4s; }
@keyframes blink { 0%,80%,100%{opacity:0.3} 40%{opacity:1} }
.search-indicator { display:inline-flex; align-items:center; gap:8px; padding:8px 12px; background:rgba(243,156,18,0.15); border:1px solid rgba(243,156,18,0.3); border-radius:var(--radius); color:var(--warning); font-family:var(--font-mono); font-size:12px; margin:8px 0; }
.search-indicator .spinner { width:14px; height:14px; border:2px solid rgba(243,156,18,0.3); border-top-color:var(--warning); border-radius:50%; animation:spin 1s linear infinite; }
@keyframes spin { to{transform:rotate(360deg)} }
.search-badge-inline { display:inline-block; padding:2px 8px; background:rgba(243,156,18,0.15); border:1px solid rgba(243,156,18,0.3); border-radius:10px; color:var(--warning); font-family:var(--font-mono); font-size:10px; margin-left:8px; }
.memory-badge-inline { display:inline-block; padding:2px 8px; background:rgba(155,89,182,0.15); border:1px solid rgba(155,89,182,0.3); border-radius:10px; color:#9b59b6; font-family:var(--font-mono); font-size:10px; margin-left:8px; }
.conf-badge { display:inline-block; padding:2px 8px; border-radius:10px; font-family:var(--font-mono); font-size:10px; margin-left:8px; }
.conf-badge.high { background:rgba(46,204,113,0.15); border:1px solid rgba(46,204,113,0.3); color:var(--success); }
.conf-badge.medium { background:rgba(243,156,18,0.15); border:1px solid rgba(243,156,18,0.3); color:var(--warning); }
.conf-badge.low { background:rgba(231,76,60,0.15); border:1px solid rgba(231,76,60,0.3); color:var(--danger); }

.ttr-badge { display:inline-block; padding:2px 8px; border-radius:10px; font-family:var(--font-mono); font-size:10px; margin-left:8px; }
.ttr-badge.low { background:rgba(46,204,113,0.15); border:1px solid rgba(46,204,113,0.3); color:var(--success); }
.ttr-badge.medium { background:rgba(243,156,18,0.15); border:1px solid rgba(243,156,18,0.3); color:var(--warning); }
.ttr-badge.high { background:rgba(231,76,60,0.15); border:1px solid rgba(231,76,60,0.3); color:var(--danger); }
.ttr-badge.search { background:rgba(230,126,34,0.15); border:1px solid rgba(230,126,34,0.3); color:#e67e22; }
.tps-badge { display:inline-block; padding:2px 8px; border-radius:10px; font-family:var(--font-mono); font-size:10px; margin-left:8px; background:rgba(72,181,224,0.15); border:1px solid rgba(72,181,224,0.3); color:var(--accent); }
.tok-badge { display:inline-block; padding:2px 8px; border-radius:10px; font-family:var(--font-mono); font-size:10px; margin-left:8px; background:rgba(52,152,219,0.15); border:1px solid rgba(52,152,219,0.3); color:#3498db; }

.msg-toolbar { display:flex; gap:2px; margin-top:8px; align-items:center; }
.msg-toolbar button { background:none; border:none; color:var(--text-muted); cursor:pointer; padding:4px 6px; border-radius:4px; font-size:13px; line-height:1; display:flex; align-items:center; gap:4px; transition:color 0.15s,background 0.15s; }
.msg-toolbar button:hover { color:var(--text-primary); background:var(--bg-tertiary); }
.msg-toolbar button.active { color:var(--accent); }
.msg-toolbar .sep { width:1px; height:14px; background:var(--border); margin:0 4px; }
.msg-toolbar .toolbar-label { font-size:10px; font-family:var(--font-mono); color:var(--text-muted); margin-right:4px; }

.input-area { padding:16px 28px; border-bottom:1px solid var(--border); background:var(--bg-secondary); }
.input-row { max-width:900px; margin:0 auto; display:flex; gap:10px; align-items:flex-end; }
.input-row textarea { flex:1; background:var(--bg-tertiary); border:1px solid var(--border); color:var(--text-primary); font-family:var(--font-body); font-size:14px; padding:12px 14px; border-radius:var(--radius); resize:none; min-height:44px; max-height:200px; line-height:1.5; }
.input-row textarea:focus { outline:none; border-color:var(--accent-dim); }
.input-row textarea::placeholder { color:var(--text-muted); }
.user-copy-btn { display:inline; cursor:pointer; font-size:13px; margin-left:6px; opacity:0.4; transition:opacity 0.15s; user-select:none; }
.user-copy-btn:hover { opacity:1; }
.paperclip-btn { padding:12px 12px; background:var(--bg-tertiary); border:1px solid var(--border); border-radius:var(--radius); color:var(--text-secondary); font-size:18px; cursor:pointer; transition:all 0.2s; line-height:1; }
.paperclip-btn:hover { background:var(--bg-hover); color:var(--accent); border-color:var(--accent-dim); }
.send-btn { padding:12px 20px; background:var(--accent-dim); border:none; border-radius:var(--radius); color:#fff; font-family:var(--font-mono); font-size:13px; font-weight:600; cursor:pointer; white-space:nowrap; }
.send-btn:hover { background:var(--accent); }
.stop-btn { padding:12px 20px; background:var(--danger); border:none; border-radius:var(--radius); color:#fff; font-family:var(--font-mono); font-size:13px; font-weight:600; cursor:pointer; }
.stop-btn:hover { background:var(--danger-hover); }
.search-btn { padding:12px 16px; background:var(--warning); border:none; border-radius:var(--radius); color:var(--bg-tertiary); font-family:var(--font-mono); font-size:13px; font-weight:600; cursor:pointer; white-space:nowrap; transition:background 0.2s; }
.search-btn:hover { background:#e67e22; }
.search-btn:disabled { background:var(--text-muted); cursor:not-allowed; }
.file-preview { max-width:900px; margin:0 auto 8px; display:none; align-items:center; gap:10px; background:var(--bg-tertiary); border:1px solid var(--border); border-radius:var(--radius); padding:8px 12px; }
.file-preview.visible { display:flex; }
.file-preview-thumb { width:48px; height:48px; object-fit:cover; border-radius:4px; border:1px solid var(--border); }
.file-preview-icon { font-size:28px; flex-shrink:0; color:var(--text-muted); }
.file-preview-info { flex:1; min-width:0; }
.file-preview-name { font-size:13px; color:var(--text-primary); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.file-preview-size { font-size:11px; color:var(--text-muted); }
.file-preview-clear { background:none; border:none; color:var(--text-muted); font-size:18px; cursor:pointer; padding:4px; line-height:1; transition:color 0.15s; }
.file-preview-clear:hover { color:var(--danger); }
.file-preview-notice { font-size:11px; color:var(--warning); }
.conv-attach { color:var(--text-muted); cursor:pointer; padding:2px 2px; font-size:14px; flex-shrink:0; transition:color 0.15s; margin-left:2px; }
.conv-attach:hover { color:var(--accent); }
.conv-attach.has-attachments { color:var(--accent-dim); }
.gallery-overlay { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.85); z-index:10000; display:none; justify-content:center; align-items:center; }
.gallery-overlay.visible { display:flex; }
.gallery-panel { background:var(--bg-secondary); border:1px solid var(--border); border-radius:var(--radius); width:90%; max-width:700px; max-height:85vh; display:flex; flex-direction:column; }
.gallery-header { display:flex; align-items:center; justify-content:space-between; padding:16px 20px; border-bottom:1px solid var(--border); }
.gallery-header h3 { font-size:16px; color:var(--text-primary); font-family:var(--font-mono); }
.gallery-close { background:none; border:none; color:var(--text-muted); font-size:24px; cursor:pointer; padding:4px; line-height:1; transition:color 0.15s; }
.gallery-close:hover { color:var(--text-primary); }
.gallery-body { flex:1; overflow-y:auto; padding:12px 20px; }
.gallery-empty { text-align:center; padding:40px 20px; color:var(--text-muted); font-size:14px; }
.gallery-item { display:flex; align-items:center; gap:12px; padding:12px; border-radius:var(--radius); margin-bottom:8px; background:var(--bg-tertiary); transition:background 0.15s; }
.gallery-item:hover { background:var(--bg-hover); }
.gallery-item-thumb { width:60px; height:60px; object-fit:cover; border-radius:4px; border:1px solid var(--border); flex-shrink:0; }
.gallery-item-icon { font-size:32px; flex-shrink:0; color:var(--text-muted); text-align:center; width:60px; }
.gallery-item-info { flex:1; min-width:0; }
.gallery-item-name { font-size:14px; color:var(--text-primary); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.gallery-item-meta { font-size:11px; color:var(--text-muted); margin-top:2px; }
.gallery-item-delete { background:none; border:1px solid var(--danger); border-radius:var(--radius); color:var(--danger); font-size:12px; cursor:pointer; padding:6px 12px; transition:all 0.15s; flex-shrink:0; }
.gallery-item-delete:hover { background:var(--danger); color:#fff; }



.message.assistant.search-result { }
.message.assistant.search-result .content { background:rgba(243,156,18,0.08); border:1px solid rgba(243,156,18,0.2); border-radius:var(--radius); padding:12px; }
.message.assistant.search-result .role-label { color:var(--warning) !important; }
.message.assistant.search-result .content .text { color:var(--warning); }
.raw-results { margin-top:12px; background:var(--bg-tertiary); border:1px solid var(--border); border-radius:var(--radius); padding:8px 12px; font-size:12px; }
.raw-results summary { cursor:pointer; color:var(--accent); font-family:var(--font-mono); }
.raw-results ul { margin:8px 0 0 0; padding-left:20px; list-style:none; }
.raw-results li { margin-bottom:8px; }
.raw-results a { color:var(--accent); text-decoration:none; }
.raw-results a:hover { text-decoration:underline; }
.raw-results small { color:var(--text-muted); display:block; margin-top:2px; }

@media (max-width:768px) {
    .topbar { padding:8px 14px; gap:6px; }
    .chat-container { padding:12px 14px; }
    .input-area { padding:8px 14px; }
    .sys-stats { padding:3px 14px; gap:12px; font-size:10px; }
    .topbar-brand { font-size:14px; }
    .topbar-brand .version { font-size:11px; }
}

.auth-screen { position: fixed; inset: 0; width: 100%; height: 100vh; display: none; align-items: center; justify-content: center; background: rgba(0,0,0,0.62); z-index: 3000; }
.auth-card { width: 100%; max-width: 360px; margin: 0 16px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 12px; padding: 22px; box-shadow: 0 10px 28px rgba(0,0,0,0.35); }
.auth-title { font-family: var(--font-mono); font-size: 18px; color: var(--accent); margin-bottom: 6px; }
.auth-subtitle { font-size: 12px; color: var(--text-muted); margin-bottom: 14px; }
.auth-warning { margin-bottom: 12px; font-size: 12px; color: #ff8f8f; background: rgba(231,76,60,0.14); border: 1px solid rgba(231,76,60,0.35); border-radius: var(--radius); padding: 8px 10px; line-height: 1.4; }
.pin-input { width: 100%; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text-primary); font-family: var(--font-mono); font-size: 22px; letter-spacing: 6px; text-align: center; padding: 12px; margin-bottom: 10px; }
.pin-input:focus { outline: none; border-color: var(--accent-dim); }
.auth-btn { width: 100%; padding: 11px 14px; background: var(--accent-dim); border: none; border-radius: var(--radius); color: #fff; font-family: var(--font-mono); font-size: 13px; font-weight: 600; cursor: pointer; }
.auth-btn:hover { background: var(--accent); }
.auth-error { min-height: 18px; margin-top: 10px; font-size: 12px; color: var(--danger); text-align: center; }
.logout-btn { padding: 8px 10px; background: transparent; border: 1px solid var(--danger); border-radius: var(--radius); color: var(--danger); font-family: var(--font-mono); font-size: 11px; cursor: pointer; }
.logout-btn:hover { background: rgba(231,76,60,0.12); }
</style>
</head>
<body>

<div class="auth-screen" id="authScreen">
    <div class="auth-card">
        <div class="auth-title">cAIc Unlock</div>
        <div class="auth-subtitle">Enter 4-digit admin PIN to unlock advanced actions</div>
        <div class="auth-warning">Security warning: PIN 1234 is weak. Use a non-trivial 4-digit PIN.</div>
        <input id="pinInput" class="pin-input" type="password" inputmode="numeric" maxlength="4" autocomplete="off" />
        <button id="unlockBtn" class="auth-btn" onclick="unlockWithPin()">UNLOCK</button>
        <div id="authError" class="auth-error"></div>
    </div>
</div>

<div id="drawerOverlay" class="drawer-overlay" onclick="toggleDrawer()"></div>

<aside class="drawer" id="convDrawer">
    <div class="drawer-header">
        <span class="drawer-title">cAIc <span class="version">{{ version }}</span></span>
        <div class="drawer-actions">
            <button class="drawer-btn" onclick="newChat()" title="New chat">+</button>
            <button class="drawer-btn" onclick="openRagAdmin()" title="RAG corpus management">R</button>
            <button class="drawer-btn" onclick="openSettings()" title="Settings">&#9881;</button>
            <button class="drawer-btn danger" onclick="deleteAllConversations()" title="Delete all">&#128465;</button>
        </div>
    </div>
    <div class="drawer-conv-list" id="convList"></div>
    <div class="drawer-footer">
        <div class="df-row" id="ollamaStatus"><span class="status-dot offline"></span> <span class="df-label">llm</span> checking...</div>
        <div class="df-row" id="searchStatus"><span class="status-dot offline"></span> <span class="df-label">search</span> checking...</div>
        <div class="df-row" id="memoryStatus"><span class="status-dot"></span> <span class="df-label">memory</span> -- entries</div>
        <div class="df-row" id="clusterStatus"><span class="status-dot"></span> <span class="df-label">cluster</span> --</div>
        <div class="cluster-panel" id="clusterPanel" style="display:none"></div>
    </div>
</aside>

<div class="modal-overlay" id="ragAdminModal">
    <div class="modal" style="max-width:960px;">
        <div class="modal-header">
            <h2>RAG Corpus Management</h2>
            <button class="modal-close" onclick="closeRagAdmin()">&times;</button>
        </div>
        <div class="modal-body">
            <div class="rag-stats-bar" id="ragStatsBar">Loading stats...</div>
                <div class="rag-toolbar">
                <div class="rag-toolbar-left">
                    <input type="text" id="ragSearchInput" class="rag-search-input" placeholder="Search RAG text (semantic vector search)..." onkeydown="if(event.key==='Enter')loadRagPoints()">
                    <select id="ragSourceFilter" class="rag-source-select" onchange="loadRagPoints()">
                        <option value="">All sources</option>
                    </select>
                </div>
                <div class="rag-toolbar-right">
                    <button class="btn-small btn-save" onclick="loadRagPoints()" id="ragRefreshBtn">&#8635; Refresh</button>
                    <button class="btn-small btn-reset" onclick="flushRagCollection()" style="color:var(--danger);border-color:var(--danger);">Flush All</button>
                </div>
            </div>
            <div class="rag-search-hint">Leave search blank to browse all entries. Search uses vector similarity against uploaded/ingested document text.</div>
            <div class="rag-table-wrap">
                <table class="rag-table" id="ragTable">
                    <thead>
                        <tr>
                            <th class="rag-col-id">ID</th>
                            <th class="rag-col-text">Text</th>
                            <th class="rag-col-source">Source</th>
                            <th class="rag-col-type">Type</th>
                            <th class="rag-col-date">Date</th>
                            <th class="rag-col-retr">Retr</th>
                            <th class="rag-col-actions">Actions</th>
                        </tr>
                    </thead>
                    <tbody id="ragTableBody">
                        <tr><td colspan="7" style="text-align:center;color:var(--text-muted);padding:40px;">No points loaded</td></tr>
                    </tbody>
                </table>
            </div>
            <div class="rag-pagination" id="ragPagination" style="display:none;">
                <button class="btn-small" id="ragPrevBtn" onclick="ragPage(-1)">&#9664; Prev</button>
                <span class="rag-page-info" id="ragPageInfo">Page 1</span>
                <button class="btn-small" id="ragNextBtn" onclick="ragPage(1)">Next &#9654;</button>
            </div>
        </div>
    </div>
</div>

<div class="modal-overlay" id="settingsModal">
    <div class="modal">
        <div class="modal-header">
            <h2>Settings</h2>
            <button class="modal-close" onclick="closeSettings()">&times;</button>
        </div>
        <div class="modal-body">
            <div class="modal-section">
                <h3>Profile / Memory</h3>
                <p class="desc">This context is injected as a system prompt into every conversation.</p>
                <div class="toggle-row">
                    <span class="toggle-label">Inject profile into all chats</span>
                    <div class="toggle-switch on" id="profileToggle" onclick="toggleProfile()"></div>
                </div>
                <textarea id="profileEditor" rows="18" spellcheck="false"></textarea>
                <div class="token-count" id="profileTokenCount"></div>
                <div class="btn-bar">
                    <button class="btn-small btn-save" id="saveProfileBtn" onclick="saveProfile()">Save Profile</button>
                    <button class="btn-small btn-reset" onclick="resetProfile()">Reset to Default</button>
                </div>
            </div>
            <div class="modal-section">
                <h3>Memory System (FTS5)</h3>
                <p class="desc">Memories are automatically injected based on relevance to your message. Say "remember that..." to add memories.</p>
                <div class="toggle-row">
                    <span class="toggle-label">Enable memory injection</span>
                    <div class="toggle-switch on" id="memoryToggle" onclick="toggleMemory()"></div>
                </div>
                <div class="memory-stats" id="memoryStats">Loading...</div>
                <div id="memoryList"></div>
            </div>
            <div class="modal-section">
                <h3>Web Search (SearXNG)</h3>
                <p class="desc">When enabled, cAIc will automatically search the web if the model indicates uncertainty. Use the 🔍 button to force a web search.</p>
                <div class="toggle-row">
                    <span class="toggle-label">Enable automatic web search</span>
                    <div class="toggle-switch on" id="searchToggle" onclick="toggleSearch()"></div>
                </div>
            </div>
            <div class="modal-section">
                <h3>System Prompt Presets</h3>
                <div id="presetList"></div>
                <div class="btn-bar" style="margin-top:12px;">
                    <button class="btn-small btn-save" onclick="addPreset()">+ Add Preset</button>
                </div>
            </div>
            <div class="modal-section">
                <h3>Skills (Phase 1)</h3>
                <p class="desc">Toggle built-in local skills used for tool-aware prompt injection. Master toggle disables all skills globally.</p>
                <div class="toggle-row">
                    <span class="toggle-label">Enable skills framework</span>
                    <div class="toggle-switch on" id="skillsMasterToggle" onclick="toggleSkillsMaster()"></div>
                </div>
                <div class="skills-status" id="skillsStatus">Loading skills...</div>
                <div id="skillsList"></div>
            </div>
            <div class="modal-section">
                <h3>General</h3>
                <div class="toggle-row">
                    <span class="toggle-label">Default model</span>
                    <select id="defaultModelSetting" onchange="saveDefaultModel()"></select>
                </div>
            </div>
        </div>
    </div>
</div>

<main class="main">
    <div class="topbar">
        <div class="topbar-left">
            <button class="hamburger" id="hamburgerBtn" onclick="toggleDrawer()" title="Conversations">&#9776;</button>
            <span class="topbar-brand">cAIc <span class="version">{{ version }}</span><span class="theme-picker"><button class="badge" id="themeBtn" onclick="toggleThemePicker()" title="Color scheme">&#127912;</button><div class="theme-dropdown" id="themeDropdown"><div class="theme-option active" data-theme="ibm-blue" onclick="applyTheme('ibm-blue')"><span class="theme-swatch swatch-ibm"></span>IBM Blue</div><div class="theme-option" data-theme="green-bg" onclick="applyTheme('green-bg')"><span class="theme-swatch swatch-green"></span>Green Ln</div><div class="theme-option" data-theme="dark" onclick="applyTheme('dark')"><span class="theme-swatch swatch-dark"></span>Dark</div><div class="theme-option" data-theme="light" onclick="applyTheme('light')"><span class="theme-swatch swatch-light"></span>Light</div><div class="theme-option" data-theme="amber" onclick="applyTheme('amber')"><span class="theme-swatch swatch-amber"></span>Amber</div><div class="theme-option" data-theme="trippin" onclick="applyTheme('trippin')"><span class="theme-swatch swatch-trip"></span>Trippin</div></div></span></span>
        </div>

        <div class="topbar-right">
            <button class="badge admin" id="authActionBtn" onclick="handleAuthAction()" title="Unlock admin">ADMIN</button>
            <div class="status-menu">
                <button class="status-menu-btn" id="statusMenuBtn" onclick="toggleStatusMenu()" title="Settings &amp; status">&#8942;</button>
                <div class="status-dropdown" id="statusDropdown">
                    <div class="status-dropdown-item" onclick="toggleMemory()">
                        <span class="sdi-label">Memory</span>
                        <span class="sdi-toggle on" id="memoryBadge">MEM</span>
                    </div>
                    <div class="status-dropdown-item" onclick="toggleSearch()">
                        <span class="sdi-label">Web Search</span>
                        <span class="sdi-toggle on" id="searchBadge">SEARCH</span>
                    </div>
                    <div class="status-dropdown-item" onclick="toggleProfile()">
                        <span class="sdi-label">Profile</span>
                        <span class="sdi-toggle on" id="profileBadge">PROFILE</span>
                    </div>
                    <div class="status-dropdown-item" onclick="toggleDirection()">
                        <span class="sdi-label">Sort</span>
                        <span class="sdi-toggle on" id="directionBadge">↓ NEW</span>
                    </div>
                    <div class="status-dropdown-item" onclick="togglePrivate()">
                        <span class="sdi-label">Privacy</span>
                        <span class="sdi-toggle off" id="privateBadge">OFF</span>
                    </div>
                    <div class="status-dropdown-divider"></div>
                    <div class="status-dropdown-item" onclick="togglePrivateInfo(event)">
                        <span class="sdi-label" style="color:var(--text-muted);">&#9432; About Privacy</span>
                        <span></span>
                    </div>
                </div>
            </div>
            <div class="info-popup-overlay" id="privateInfoOverlay" style="display:none" onclick="togglePrivateInfo(event)">
                <div class="info-popup-modal" onclick="event.stopPropagation()">
                    <button class="info-popup-close" onclick="togglePrivateInfo(event)">&times;</button>
                    <strong>&#128274; Privacy Mode</strong><br><br>
                    <b>When enabled:</b><br>
                    &bull; Nothing is saved to the database<br>
                    &bull; No memory or RAG context is injected<br>
                    &bull; Web search is disabled (no external queries)<br>
                    &bull; Messages are not auto-ingested as facts<br>
                    &bull; No conversation history is kept after refresh<br><br>
                    <b>When disabled:</b><br>
                    &bull; All conversations are persisted to disk (encrypted at rest)<br>
                    &bull; Queries may be stored for context and memory<br>
                    &bull; Web search available (SearXNG)
                </div>
            </div>
        </div>
    </div>

    <div class="input-area">
        <div class="file-preview" id="filePreview">
            <img class="file-preview-thumb" id="filePreviewThumb" src="" alt="" style="display:none">
            <span class="file-preview-icon" id="filePreviewIcon">&#128196;</span>
            <div class="file-preview-info">
                <div class="file-preview-name" id="filePreviewName"></div>
                <div class="file-preview-size" id="filePreviewSize"></div>
                <div class="file-preview-notice" id="filePreviewNotice"></div>
            </div>
            <button class="file-preview-clear" onclick="clearFileSelection()" title="Remove file">&times;</button>
        </div>
        <div class="input-row">
            <input type="file" id="fileInput" style="display:none" accept=".txt,.md,.pdf,.json,.py,.html,.png,.jpg,.jpeg,.gif,.svg,.webp" onchange="onFileSelected(event)">
            <button class="paperclip-btn" id="paperclipBtn" onclick="document.getElementById('fileInput').click()" title="Attach file">&#128206;</button>
            <textarea id="userInput" placeholder="Type a message... (Ctrl+Enter to Online Search, Enter to Private Search)" rows="1" autofocus></textarea>
            <button class="send-btn" id="sendBtn" onclick="sendMessage()">SEND</button>
            <button class="search-btn" id="searchBtn" onclick="sendSearch()" title="Search the web">WEB &#128269;</button>
        </div>
    </div>

    <div class="chat-container" id="chatContainer">
        <div class="welcome-screen" id="welcomeScreen">
            <img src="/static/caic-bg.png" alt="cAIc" class="welcome-logo">
        </div>
    </div>

    <div class="sys-stats" id="sysStats">
        <span class="ss-item"><span class="ss-label">CPU</span> <span id="cpuValue" class="ss-val">--%</span></span>
        <span class="ss-item"><span class="ss-label">MEM</span> <span id="memValue" class="ss-val">--%</span></span>
        <span class="ss-item"><span class="ss-label">GPU</span> <span id="gpuValue" class="ss-val">--%</span></span>
        <span class="ss-item"><span class="ss-label">VRAM</span> <span id="vramValue" class="ss-val">--%</span></span>
        <span class="ss-item"><span class="ss-label">TOK</span> <span id="tokValue">0</span> / <span id="tokPct" class="ss-val green">0%</span></span>
    </div>

    <div class="gallery-overlay" id="galleryOverlay" onclick="closeGallery(event)">
        <div class="gallery-panel" onclick="event.stopPropagation()">
            <div class="gallery-header">
                <h3>Attachments</h3>
                <button class="gallery-close" onclick="closeGallery()">&times;</button>
            </div>
            <div class="gallery-body" id="galleryBody"></div>
        </div>
    </div>
</main>

<script>
let currentConvId = null;
let isStreaming = false;
let abortController = null;
let selectedFile = null;
let profileEnabled = true;
let searchEnabled = true;
let memoryEnabled = true;
let skillsEnabled = true;
let privateEnabled = false;
let presets = [];
let skillsRegistry = [];
let currentModel = '';
function modelLabel() {
    if (!currentModel) return 'assistant';
    return currentModel.replace(/^(\w[\w.]*)[-\/:](\d+)[bB]?[-:](.*)$/, (m, fam, size, variant) => {
        const v = variant.replace(/^(instruct|code|text|chat).*$/i, (_, v) => v === 'code' ? 'c' : 'i');
        return `${fam}:${size}B:${v}`;
    });
}
let modelContextSize = 8192;
let cachedProfile = '';
let conversationHistory = [];
let appInitialized = false;
let heartbeatIntervalId = null;
let currentRole = 'guest';

const SESSION_KEY = 'jc_session_id';

function toggleDrawer() {
    const drawer = document.getElementById('convDrawer');
    const overlay = document.getElementById('drawerOverlay');
    const isOpen = drawer.classList.toggle('open');
    overlay.classList.toggle('visible', isOpen);
}

document.addEventListener('DOMContentLoaded', async () => {
    document.getElementById('pinInput').addEventListener('keydown', e => { if (e.key === 'Enter') unlockWithPin(); });
    await bootstrapAuth();
});

window.addEventListener('pagehide', () => {
    // Best-effort server-side revoke on tab close; session timeout is the fallback if beacon is dropped.
    const sid = sessionStorage.getItem(SESSION_KEY);
    if (!sid) return;
    navigator.sendBeacon('/api/auth/logout', sid);
    sessionStorage.removeItem(SESSION_KEY);
});

function showAuthScreen() {
    document.getElementById('authScreen').style.display = 'flex';
    document.getElementById('pinInput').value = '';
    document.getElementById('authError').textContent = '';
    document.getElementById('pinInput').focus();
}

function hideAuthScreen() {
    document.getElementById('authScreen').style.display = 'none';
}

function showMainScreen() {}

function applyRoleUI() {
    // Guest mode keeps chat available while hiding controls that mutate system state.
    const isAdmin = currentRole === 'admin';
    const authBtn = document.getElementById('authActionBtn');
    const memoryBadge = document.getElementById('memoryBadge');
    const searchBadge = document.getElementById('searchBadge');
    const profileBadge = document.getElementById('profileBadge');

    authBtn.textContent = isAdmin ? 'LOGOUT' : 'ADMIN';
    authBtn.title = isAdmin ? 'Logout admin mode' : 'Unlock admin mode';
    memoryBadge.style.display = isAdmin ? 'inline-block' : 'none';
    searchBadge.style.display = isAdmin ? 'inline-block' : 'none';
    profileBadge.style.display = isAdmin ? 'inline-block' : 'none';

    if (!isAdmin) closeSettings();
}

function requireAdminNotice() {
    // Reuse the same PIN modal as a capability escalation prompt.
    showAuthScreen();
    document.getElementById('authError').textContent = 'Admin PIN required for this action';
}

function handleAuthExpired() {
    sessionStorage.removeItem(SESSION_KEY);
    currentRole = 'guest';
    if (heartbeatIntervalId) {
        clearInterval(heartbeatIntervalId);
        heartbeatIntervalId = null;
    }
    bootstrapAuth();
}

async function authFetch(url, options = {}) {
    const sid = sessionStorage.getItem(SESSION_KEY);
    const headers = { ...(options.headers || {}) };
    if (sid) headers['X-Session-ID'] = sid;
    const response = await fetch(url, { ...options, headers });
    if (response.status === 401) {
        // Session missing/expired/revoked: return to bootstrap flow and recreate guest session.
        handleAuthExpired();
        throw new Error('Authentication required');
    }
    if (response.status === 403) {
        // Authenticated but insufficient capability (guest hitting admin action).
        requireAdminNotice();
        throw new Error('Admin PIN required');
    }
    return response;
}

async function createGuestSession() {
    // Guest session is the default so conversational UX works without PIN friction.
    const resp = await fetch('/api/auth/guest', { method: 'POST' });
    const data = await resp.json();
    if (!resp.ok) throw new Error(data.detail || 'Unable to create guest session');
    sessionStorage.setItem(SESSION_KEY, data.session_id);
    currentRole = data.role || 'guest';
}

async function bootstrapAuth() {
    const sid = sessionStorage.getItem(SESSION_KEY);
    try {
        if (sid) {
            // Restore prior tab session when possible to avoid unnecessary re-prompts.
            const resp = await fetch('/api/auth/session', {
                headers: { 'X-Session-ID': sid }
            });
            const data = await resp.json();
            if (resp.ok && data.authenticated) {
                currentRole = data.role || 'guest';
            } else {
                sessionStorage.removeItem(SESSION_KEY);
                await createGuestSession();
            }
        } else {
            await createGuestSession();
        }

        showMainScreen();
        hideAuthScreen();
        applyRoleUI();
        startHeartbeat();
        await initializeMainApp();
    } catch (e) {
        showMainScreen();
        showAuthScreen();
    }
}

async function unlockWithPin() {
    const pinInput = document.getElementById('pinInput');
    const authError = document.getElementById('authError');
    const unlockBtn = document.getElementById('unlockBtn');
    const pin = (pinInput.value || '').trim();

    if (!/^\d{4}$/.test(pin)) {
        authError.textContent = 'PIN must be 4 digits';
        return;
    }

    unlockBtn.disabled = true;
    authError.textContent = '';
    try {
        const resp = await fetch('/api/auth/login', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ pin })
        });
        const data = await resp.json();
        if (!resp.ok) {
            authError.textContent = data.detail || 'Login failed';
            unlockBtn.disabled = false;
            pinInput.select();
            return;
        }
        sessionStorage.setItem(SESSION_KEY, data.session_id);
        // Admin session replaces guest session in this tab only.
        currentRole = data.role || 'admin';
        showMainScreen();
        hideAuthScreen();
        applyRoleUI();
        startHeartbeat();
        await initializeMainApp();
    } catch (e) {
        authError.textContent = 'Unable to reach auth endpoint';
    } finally {
        unlockBtn.disabled = false;
    }
}

async function logoutToGuest() {
    try {
        await authFetch('/api/auth/logout', { method: 'POST' });
    } catch (e) {
        // Ignore; local client state cleanup still proceeds.
    }
    // Drop elevated session and immediately re-issue a guest token for continued chat use.
    sessionStorage.removeItem(SESSION_KEY);
    await createGuestSession();
    currentRole = 'guest';
    applyRoleUI();
    hideAuthScreen();
}

function handleAuthAction() {
    if (currentRole === 'admin') {
        logoutToGuest();
        return;
    }
    showAuthScreen();
}

async function sendHeartbeat() {
    try {
        await authFetch('/api/auth/heartbeat', { method: 'POST' });
    } catch (e) {
        // authFetch handles invalid session case.
    }
}

function startHeartbeat() {
    if (heartbeatIntervalId) clearInterval(heartbeatIntervalId);
    heartbeatIntervalId = setInterval(sendHeartbeat, 30000);
}

async function initializeMainApp() {
    if (appInitialized) return;
    appInitialized = true;
    const savedTheme = localStorage.getItem('caic-theme') || 'ibm-blue';
    applyTheme(savedTheme);
    await loadModels();
    await loadSettings();
    await loadSkills();
    await loadProfile();
    await loadPresets();
    await loadConversations();
    await loadMemoryStats();
    checkOllamaStatus();
    checkSearchStatus();
    updateClusterStatus();
    updateSystemStats();
    setInterval(checkOllamaStatus, 30000);
    setInterval(checkSearchStatus, 60000);
    setInterval(updateClusterStatus, 15000);
    setInterval(updateSystemStats, 2000);
}

async function loadMemoryStats() {
    try {
        const resp = await authFetch('/api/memories/stats');
        const data = await resp.json();
        document.getElementById('memoryStats').textContent = `Total: ${data.total} memories`;
        document.getElementById('memoryStatus').innerHTML = `<span class="status-dot"></span> memory: ${data.total} entries`;

        const listResp = await authFetch('/api/memories?limit=20');
        const listData = await listResp.json();
        const container = document.getElementById('memoryList');
        container.innerHTML = '';
        listData.memories.slice(0, 10).forEach(m => {
            const div = document.createElement('div');
            div.className = 'memory-item';
            div.innerHTML = `<span class="memory-topic">${m.topic}</span><span class="memory-fact">${m.fact}</span><span class="memory-delete" data-id="${m.rowid}" onclick="deleteMemory(${m.rowid})">×</span>`;
            container.appendChild(div);
        });
    } catch(e) { console.log('Memory stats error:', e); }
}

async function deleteMemory(rowid) {
    if (currentRole !== 'admin') { requireAdminNotice(); return; }
    if (!confirm('Delete this memory?')) return;
    try { await authFetch(`/api/memories/${rowid}`, { method: 'DELETE' }); } catch(e) { return; }
    showToast('Memory deleted');
    await loadMemoryStats();
}

async function updateSystemStats() {
    try {
        const resp = await authFetch('/api/stats');
        const data = await resp.json();
        const colorClass = v => v >= 95 ? 'red' : v >= 80 ? 'orange' : 'green';
        document.getElementById('cpuValue').textContent = data.cpu_percent + '%';
        document.getElementById('cpuValue').className = 'ss-val ' + colorClass(data.cpu_percent);
        document.getElementById('memValue').textContent = data.memory_percent + '%';
        document.getElementById('memValue').className = 'ss-val ' + colorClass(data.memory_percent);
        if (data.gpu_available) {
            document.getElementById('gpuValue').textContent = data.gpu_percent + '%';
            document.getElementById('gpuValue').className = 'ss-val ' + colorClass(data.gpu_percent);
            document.getElementById('vramValue').textContent = data.vram_percent + '%';
            document.getElementById('vramValue').className = 'ss-val ' + colorClass(data.vram_percent);
        }
    } catch(e) {}
}

async function checkOllamaStatus() {
    try {
        const resp = await authFetch('/api/ps');
        const data = await resp.json();
        const el = document.getElementById('ollamaStatus');
        const models = data.models || [];
        el.innerHTML = models.length > 0 ? '<span class="status-dot"></span> ' + models.map(m => m.name).join(', ') : '<span class="status-dot"></span> Ollama ready';
    } catch(e) {
        document.getElementById('ollamaStatus').innerHTML = '<span class="status-dot offline"></span> Ollama offline';
    }
}

async function checkSearchStatus() {
    try {
        const resp = await authFetch('/api/search/status');
        const data = await resp.json();
        document.getElementById('searchStatus').innerHTML = data.available ? '<span class="status-dot"></span> search: ready' : '<span class="status-dot warning"></span> search: unavailable';
        const searchBtn = document.getElementById('searchBtn');
        if (searchBtn) searchBtn.disabled = !data.available;
    } catch(e) {
        document.getElementById('searchStatus').innerHTML = '<span class="status-dot offline"></span> search: error';
        const searchBtn = document.getElementById('searchBtn');
        if (searchBtn) searchBtn.disabled = true;
    }
}

async function updateClusterStatus() {
    try {
        const resp = await authFetch('/api/cluster');
        const data = await resp.json();
        const statusEl = document.getElementById('clusterStatus');
        const panel = document.getElementById('clusterPanel');
        const count = data.node_count || 0;
        statusEl.innerHTML = `<span class="status-dot${count === 0 ? ' offline' : ''}"></span> cluster: ${count} node${count !== 1 ? 's' : ''}`;
        if (count === 0) {
            panel.style.display = 'none';
            return;
        }
        panel.style.display = 'block';
        const now = Date.now();
        let html = '';
        for (const [name, node] of Object.entries(data.nodes)) {
            const lastSeen = node.last_seen ? new Date(node.last_seen).getTime() : 0;
            const ageSec = (now - lastSeen) / 1000;
            let dotClass = 'active';
            if (node.status === 'swapping') dotClass = 'swapping';
            else if (node.status === 'error') dotClass = 'error';
            else if (ageSec > 60) dotClass = 'offline';
            const modelName = node.active_model ? (node.active_model.name || 'unknown') : 'idle';
            html += `<div class="cluster-node-row"><span class="cluster-dot ${dotClass}"></span><span class="cluster-node-name">${escapeHtml(name)}</span><span class="cluster-node-model">${escapeHtml(modelName)}</span></div>`;
        }
        panel.innerHTML = html;
    } catch(e) {
        document.getElementById('clusterStatus').innerHTML = '<span class="status-dot offline"></span> cluster: error';
    }
}

async function loadModels() {
    try {
        const resp = await authFetch('/api/models');
        const data = await resp.json();
        const settingSelect = document.getElementById('defaultModelSetting');
        settingSelect.innerHTML = '';
        (data.models || []).forEach(m => {
            settingSelect.add(new Option(m.name, m.name));
        });
        if (!currentModel && data.models?.length) {
            currentModel = data.models[0].name;
            await fetchModelContextSize();
        }
    } catch(e) {}
}

async function fetchModelContextSize() {
    const model = currentModel;
    if (!model) return;
    try {
        const resp = await authFetch('/api/show', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ name: model }) });
        const data = await resp.json();
        if (data.model_info && data.model_info['context_length']) modelContextSize = data.model_info['context_length'];
        else if (data.parameters) { const match = data.parameters.match(/num_ctx\s+(\d+)/); if (match) modelContextSize = parseInt(match[1]); }
    } catch(e) {}
}

async function loadSettings() {
    try {
        const resp = await authFetch('/api/settings');
        const s = await resp.json();
        profileEnabled = s.profile_enabled !== 'false';
        searchEnabled = s.search_enabled !== 'false';
        memoryEnabled = s.memory_enabled !== 'false';
        skillsEnabled = s.skills_enabled !== 'false';
        updateProfileUI();
        updateSearchUI();
        updateMemoryUI();
        updateSkillsUI();
        if (s.default_model) {
            currentModel = s.default_model;
            document.getElementById('defaultModelSetting').value = s.default_model;
        }
    } catch(e) {}
}

async function saveSettings() {
    if (currentRole !== 'admin') { requireAdminNotice(); return; }
    try { await authFetch('/api/settings', { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ profile_enabled: profileEnabled ? 'true' : 'false', search_enabled: searchEnabled ? 'true' : 'false', memory_enabled: memoryEnabled ? 'true' : 'false', skills_enabled: skillsEnabled ? 'true' : 'false' }) }); } catch(e) {}
}

async function saveDefaultModel() {
    if (currentRole !== 'admin') { requireAdminNotice(); return; }
    const model = document.getElementById('defaultModelSetting').value;
    try { await authFetch('/api/settings', { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ default_model: model }) }); } catch(e) {}
}

async function loadProfile() {
    try {
        const resp = await authFetch('/api/profile');
        const data = await resp.json();
        cachedProfile = data.content || '';
        document.getElementById('profileEditor').value = cachedProfile;
        updateTokenCount();
    } catch(e) {}
}

async function saveProfile() {
    if (currentRole !== 'admin') { requireAdminNotice(); return; }
    const content = document.getElementById('profileEditor').value;
    try { await authFetch('/api/profile', { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ content: content }) }); } catch(e) { return; }
    cachedProfile = content;
    updateTokenCount();
    showToast('Profile saved');
    const btn = document.getElementById('saveProfileBtn');
    btn.textContent = 'Saved!';
    setTimeout(() => btn.textContent = 'Save Profile', 1500);
}

async function resetProfile() {
    if (currentRole !== 'admin') { requireAdminNotice(); return; }
    if (!confirm('Reset profile to default?')) return;
    try {
        const resp = await authFetch('/api/profile/default');
        const data = await resp.json();
        document.getElementById('profileEditor').value = data.content;
        await saveProfile();
        showToast('Profile reset');
    } catch(e) { showToast('Profile reset failed'); }
}

function toggleProfile() { if (currentRole !== 'admin') { requireAdminNotice(); return; } profileEnabled = !profileEnabled; updateProfileUI(); saveSettings(); }
function toggleSearch() { if (currentRole !== 'admin') { requireAdminNotice(); return; } searchEnabled = !searchEnabled; updateSearchUI(); saveSettings(); }
function toggleMemory() { if (currentRole !== 'admin') { requireAdminNotice(); return; } memoryEnabled = !memoryEnabled; updateMemoryUI(); saveSettings(); }
function togglePrivate() {
    privateEnabled = !privateEnabled;
    const badge = document.getElementById('privateBadge');
    badge.className = 'sdi-toggle ' + (privateEnabled ? 'on' : 'off');
    badge.textContent = privateEnabled ? 'ON' : 'OFF';
    const searchBtn = document.getElementById('searchBtn');
    if (privateEnabled) {
        if (searchBtn) searchBtn.disabled = true;
        showToast('Privacy: nothing stored, no external queries');
    } else {
        if (searchBtn && !isStreaming) searchBtn.disabled = false;
        showToast('Standard mode: conversations persisted (encrypted at rest)');
    }
}

function togglePrivateInfo(e) {
    const overlay = document.getElementById('privateInfoOverlay');
    overlay.style.display = overlay.style.display === 'none' ? 'flex' : 'none';
}

// ---------- Theme system ----------
const THEMES = {
    'ibm-blue': {
        '--bg-primary': '#080c1a', '--bg-secondary': '#0d1428', '--bg-tertiary': '#121d3a', '--bg-hover': '#182850',
        '--text-primary': '#a0b8ff', '--text-secondary': '#7890d8', '--text-muted': '#5068a0',
        '--accent': '#33ddff', '--accent-dim': '#0088bb', '--accent-glow': 'rgba(51,221,255,0.1)',
        '--danger': '#ff3355', '--danger-hover': '#cc0022', '--success': '#00ff66', '--warning': '#ffaa00',
        '--border': '#1a3060',
    },
    'green-bg': {
        '--bg-primary': '#000000', '--bg-secondary': '#0a0a0a', '--bg-tertiary': '#111111', '--bg-hover': '#1a1a1a',
        '--text-primary': '#00ff41', '--text-secondary': '#00cc33', '--text-muted': '#008800',
        '--accent': '#00ff41', '--accent-dim': '#00aa22', '--accent-glow': 'rgba(0,255,65,0.1)',
        '--danger': '#ff3355', '--danger-hover': '#cc0022', '--success': '#00ff66', '--warning': '#ffaa00',
        '--border': '#003300',
    },
    'dark': {
        '--bg-primary': '#1a1a2e', '--bg-secondary': '#222240', '--bg-tertiary': '#2a2a50', '--bg-hover': '#333366',
        '--text-primary': '#e0e0ff', '--text-secondary': '#b0b0dd', '--text-muted': '#8080aa',
        '--accent': '#bb86fc', '--accent-dim': '#8855cc', '--accent-glow': 'rgba(187,134,252,0.1)',
        '--danger': '#cf6679', '--danger-hover': '#aa4455', '--success': '#00ff66', '--warning': '#ffaa00',
        '--border': '#333366',
    },
    'light': {
        '--bg-primary': '#f5f5f5', '--bg-secondary': '#ffffff', '--bg-tertiary': '#e8e8e8', '--bg-hover': '#dddddd',
        '--text-primary': '#222222', '--text-secondary': '#555555', '--text-muted': '#888888',
        '--accent': '#0066cc', '--accent-dim': '#004488', '--accent-glow': 'rgba(0,102,204,0.1)',
        '--danger': '#cc0033', '--danger-hover': '#990022', '--success': '#008844', '--warning': '#cc8800',
        '--border': '#cccccc',
    },
    'amber': {
        '--bg-primary': '#000000', '--bg-secondary': '#0d0800', '--bg-tertiary': '#1a1000', '--bg-hover': '#2a1a00',
        '--text-primary': '#ffb000', '--text-secondary': '#cc8800', '--text-muted': '#886600',
        '--accent': '#ffb000', '--accent-dim': '#cc8800', '--accent-glow': 'rgba(255,176,0,0.1)',
        '--danger': '#ff3355', '--danger-hover': '#cc0022', '--success': '#00ff66', '--warning': '#ffaa00',
        '--border': '#332200',
    },
    'trippin': {
        '--bg-primary': '#0a001a', '--bg-secondary': '#15002a', '--bg-tertiary': '#20003a', '--bg-hover': '#300050',
        '--text-primary': '#ff66ff', '--text-secondary': '#ff33cc', '--text-muted': '#cc0099',
        '--accent': '#ff00ff', '--accent-dim': '#cc00cc', '--accent-glow': 'rgba(255,0,255,0.15)',
        '--danger': '#ff0066', '--danger-hover': '#cc0044', '--success': '#00ff66', '--warning': '#ffff00',
        '--border': '#440066',
    },
};

function applyTheme(name) {
    const theme = THEMES[name];
    if (!theme) return;
    const root = document.documentElement;
    for (const [key, val] of Object.entries(theme)) {
        root.style.setProperty(key, val);
    }
    const scrollbarTrack = name === 'light' ? 'rgba(0,68,136,0.08)' :
        name === 'trippin' ? 'rgba(204,0,204,0.08)' :
        name === 'green-bg' ? 'rgba(0,170,34,0.08)' :
        name === 'amber' ? 'rgba(204,136,0,0.08)' :
        name === 'dark' ? 'rgba(136,85,204,0.08)' :
        'rgba(0,136,187,0.08)';
    root.style.setProperty('--scrollbar-track', scrollbarTrack);
    root.style.setProperty('--scrollbar', theme['--accent-dim']);
    root.style.setProperty('--scrollbar-hover', theme['--accent']);

    document.querySelectorAll('.theme-option').forEach(el => {
        el.classList.toggle('active', el.dataset.theme === name);
    });
    localStorage.setItem('caic-theme', name);
}

function toggleThemePicker() {
    document.getElementById('themeDropdown').classList.toggle('open');
}
function toggleStatusMenu() {
    document.getElementById('statusDropdown').classList.toggle('open');
    document.getElementById('privateInfoOverlay').style.display = 'none';
}
document.addEventListener('click', function(e) {
    const picker = document.querySelector('.theme-picker');
    if (picker && !picker.contains(e.target)) {
        document.getElementById('themeDropdown')?.classList.remove('open');
    }
    const menu = document.querySelector('.status-menu');
    if (menu && !menu.contains(e.target)) {
        document.getElementById('statusDropdown')?.classList.remove('open');
    }
});

function updateProfileUI() {
    const badge = document.getElementById('profileBadge');
    const toggle = document.getElementById('profileToggle');
    badge.className = 'sdi-toggle ' + (profileEnabled ? 'on' : 'off');
    badge.textContent = profileEnabled ? 'ON' : 'OFF';
    if (toggle) toggle.className = 'toggle-switch' + (profileEnabled ? ' on' : '');
}

function updateSearchUI() {
    const badge = document.getElementById('searchBadge');
    const toggle = document.getElementById('searchToggle');
    badge.className = 'sdi-toggle ' + (searchEnabled ? 'on' : 'off');
    badge.innerHTML = searchEnabled ? 'ON' : 'OFF';
    if (toggle) toggle.className = 'toggle-switch' + (searchEnabled ? ' on' : '');
}

function updateMemoryUI() {
    const badge = document.getElementById('memoryBadge');
    const toggle = document.getElementById('memoryToggle');
    badge.className = 'sdi-toggle ' + (memoryEnabled ? 'on' : 'off');
    badge.innerHTML = memoryEnabled ? 'ON' : 'OFF';
    if (toggle) toggle.className = 'toggle-switch' + (memoryEnabled ? ' on' : '');
}

function updateSkillsUI() {
    const master = document.getElementById('skillsMasterToggle');
    if (master) master.className = 'toggle-switch' + (skillsEnabled ? ' on' : '');
}

async function loadSkills() {
    try {
        const resp = await authFetch('/api/skills');
        const data = await resp.json();
        skillsRegistry = data.skills || [];
        renderSkills();
    } catch(e) {
        const status = document.getElementById('skillsStatus');
        if (status) status.textContent = 'Unable to load skills';
    }
}

function renderSkills() {
    const container = document.getElementById('skillsList');
    const status = document.getElementById('skillsStatus');
    if (!container || !status) return;

    container.innerHTML = '';
    if (!skillsRegistry.length) {
        status.textContent = 'No skills registered';
        return;
    }

    const enabledCount = skillsRegistry.filter(s => s.enabled).length;
    status.textContent = `${enabledCount}/${skillsRegistry.length} skills enabled${skillsEnabled ? '' : ' (master toggle OFF)'}`;

    skillsRegistry.forEach(skill => {
        const row = document.createElement('div');
        row.className = 'skill-item' + (skillsEnabled ? '' : ' disabled');

        const meta = document.createElement('div');
        meta.className = 'skill-meta';

        const name = document.createElement('div');
        name.className = 'skill-name';
        const risk = (skill.risk || 'low').toUpperCase();
        name.innerHTML = `${skill.name} <span class="skill-risk">${risk}</span>`;

        const desc = document.createElement('div');
        desc.className = 'skill-desc';
        desc.textContent = `${skill.key} - ${skill.description || ''}`;

        meta.appendChild(name);
        meta.appendChild(desc);

        const toggle = document.createElement('div');
        const active = !!skill.enabled && skillsEnabled;
        toggle.className = 'toggle-switch' + (active ? ' on' : '');
        toggle.title = skillsEnabled ? `Toggle ${skill.name}` : 'Enable skills framework first';
        toggle.addEventListener('click', () => toggleSkill(skill.key));

        row.appendChild(meta);
        row.appendChild(toggle);
        container.appendChild(row);
    });
}

async function toggleSkillsMaster() {
    if (currentRole !== 'admin') { requireAdminNotice(); return; }
    skillsEnabled = !skillsEnabled;
    updateSkillsUI();
    renderSkills();
    await saveSettings();
}

async function toggleSkill(skillKey) {
    if (currentRole !== 'admin') { requireAdminNotice(); return; }
    if (!skillsEnabled) return;
    const skill = skillsRegistry.find(s => s.key === skillKey);
    if (!skill) return;

    const nextEnabled = !skill.enabled;
    try { await authFetch(`/api/skills/${encodeURIComponent(skillKey)}`, {
        method: 'PUT',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({ enabled: nextEnabled })
    }); } catch(e) { return; }
    skill.enabled = nextEnabled;
    renderSkills();
}

function updateTokenCount() {
    const text = document.getElementById('profileEditor').value;
    cachedProfile = text;
    const tokens = Math.round(text.length / 4);
    document.getElementById('profileTokenCount').textContent = '~' + tokens + ' tokens';
}

function estimateTokens(text) { return Math.round((text || '').length / 4); }

document.getElementById('profileEditor').addEventListener('input', updateTokenCount);

async function loadPresets() {
    try {
        const resp = await authFetch('/api/presets');
        presets = await resp.json();
        renderPresetList();
        renderPresetSelect();
    } catch(e) {}
}

function renderPresetList() {
    const container = document.getElementById('presetList');
    container.innerHTML = '';
    presets.forEach(p => {
        const div = document.createElement('div');
        div.className = 'preset-item';
        div.innerHTML = `<span class="preset-name">${p.name}</span><button onclick="editPreset('${p.id}')">✎</button>${!p.is_default ? `<button onclick="deletePreset('${p.id}')">×</button>` : ''}`;
        container.appendChild(div);
    });
}

function renderPresetSelect() {}

async function addPreset() {
    if (currentRole !== 'admin') { requireAdminNotice(); return; }
    const name = prompt('Preset name:');
    if (!name) return;
    const p = prompt('System prompt text:');
    if (!p) return;
    try { await authFetch('/api/presets', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({name, prompt: p}) }); } catch(e) { return; }
    showToast('Preset added');
    await loadPresets();
}

async function editPreset(id) {
    if (currentRole !== 'admin') { requireAdminNotice(); return; }
    const preset = presets.find(p => p.id === id);
    if (!preset) return;
    const name = prompt('Preset name:', preset.name);
    if (!name) return;
    const p = prompt('System prompt:', preset.prompt);
    if (p === null) return;
    try { await authFetch(`/api/presets/${id}`, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({name, prompt: p}) }); } catch(e) { return; }
    showToast('Preset saved');
    await loadPresets();
}

async function deletePreset(id) {
    if (currentRole !== 'admin') { requireAdminNotice(); return; }
    if (!confirm('Delete this preset?')) return;
    try { await authFetch(`/api/presets/${id}`, { method: 'DELETE' }); } catch(e) { return; }
    showToast('Preset deleted');
    await loadPresets();
}

function getSelectedPresetPrompt() { return ''; }

function openSettings() {
    if (currentRole !== 'admin') { requireAdminNotice(); return; }
    document.getElementById('settingsModal').classList.add('visible');
    loadProfile();
    loadMemoryStats();
    loadSkills();
}
function closeSettings() { document.getElementById('settingsModal').classList.remove('visible'); }
document.getElementById('settingsModal').addEventListener('click', e => { if (e.target.id === 'settingsModal') closeSettings(); });

// ---------- RAG Admin ----------
let _ragPageOffset = null;
let _ragPrevOffsets = [];
let _ragNextOffset = null;
let _ragTotal = 0;

function openRagAdmin() {
    if (currentRole !== 'admin') { requireAdminNotice(); return; }
    document.getElementById('ragAdminModal').classList.add('visible');
    _ragPageOffset = null;
    _ragPrevOffsets = [];
    _ragNextOffset = null;
    _ragTotal = 0;
    loadRagStats();
    loadRagPoints();
    populateRagSourceFilter();
}
function closeRagAdmin() { document.getElementById('ragAdminModal').classList.remove('visible'); }
document.getElementById('ragAdminModal').addEventListener('click', e => { if (e.target.id === 'ragAdminModal') closeRagAdmin(); });

async function loadRagStats() {
    try {
        const resp = await authFetch('/api/rag/stats');
        const data = await resp.json();
        const bar = document.getElementById('ragStatsBar');
        bar.innerHTML = `
            <span class="rag-stat-item"><span class="rag-stat-label">Vectors</span> <span class="rag-stat-value">${data.vector_count}</span></span>
            <span class="rag-stat-item"><span class="rag-stat-label">Max</span> <span class="rag-stat-value">${data.max_vectors}</span></span>
            <span class="rag-stat-item"><span class="rag-stat-label">Full</span> <span class="rag-stat-value">${data.percent_full}%</span></span>
            <span class="rag-stat-item"><span class="rag-stat-label">Pinned</span> <span class="rag-stat-value">${data.pinned_count}</span></span>
            <span class="rag-stat-item"><span class="rag-stat-label">Avg Retr</span> <span class="rag-stat-value">${data.avg_retrieval_count}</span></span>
            <span class="rag-stat-item"><span class="rag-stat-label">At Risk</span> <span class="rag-stat-value">${data.at_risk_count}</span></span>
            <span class="rag-stat-item"><span class="rag-stat-label">Evictions (1m)</span> <span class="rag-stat-value">${data.eviction_counts_last_1m}</span></span>
        `;
    } catch(e) {
        document.getElementById('ragStatsBar').textContent = 'Failed to load stats';
    }
}

async function populateRagSourceFilter() {
    const sel = document.getElementById('ragSourceFilter');
    if (sel.options.length > 1) return;
    try {
        const resp = await authFetch('/api/rag/points?limit=100');
        const data = await resp.json();
        const sources = new Set();
        (data.points || []).forEach(p => { if (p.source) sources.add(p.source); });
        const sorted = Array.from(sources).sort();
        sorted.forEach(s => {
            const opt = document.createElement('option');
            opt.value = s;
            opt.textContent = s;
            sel.appendChild(opt);
        });
    } catch(e) {}
}

async function loadRagPoints() {
    const table = document.getElementById('ragTableBody');
    table.innerHTML = '<tr><td colspan="7" style="text-align:center;color:var(--text-muted);padding:40px;">Loading...</td></tr>';
    try {
        const search = document.getElementById('ragSearchInput').value.trim();
        const source = document.getElementById('ragSourceFilter').value;
        let url = `/api/rag/points?limit=20`;
        if (search) url += `&search=${encodeURIComponent(search)}`;
        if (source) url += `&source=${encodeURIComponent(source)}`;
        if (_ragPageOffset !== null && !search) url += `&offset=${encodeURIComponent(_ragPageOffset)}`;

        const resp = await authFetch(url);
        const data = await resp.json();
        _ragTotal = data.total || 0;
        _ragNextOffset = data.next_offset || null;

        table.innerHTML = '';
        if (!data.points || data.points.length === 0) {
            const emptyMsg = document.getElementById('ragSearchInput').value.trim()
                ? 'No results match your search query.'
                : 'RAG corpus is empty. Upload a file with the 📎 button (mode=ingest or both), or use /api/ingest from the terminal.';
            table.innerHTML = `<tr><td colspan="7" style="text-align:center;color:var(--text-muted);padding:30px;line-height:1.6;">${emptyMsg}</td></tr>`;
            document.getElementById('ragPagination').style.display = 'none';
            return;
        }

        data.points.forEach(p => {
            const tr = document.createElement('tr');
            const textSnippet = (p.text || '').substring(0, 200);
            tr.innerHTML = `
                <td class="rag-col-id" title="${escapeHtml(p.id)}">${escapeHtml((p.id || '').substring(0, 24))}${(p.id || '').length > 24 ? '...' : ''}</td>
                <td class="rag-col-text"><div class="rag-text-snippet" onclick="this.classList.toggle('expanded')" title="Click to expand/collapse">${escapeHtml(textSnippet)}</div></td>
                <td class="rag-col-source">${escapeHtml(p.source || '')}</td>
                <td class="rag-col-type">${escapeHtml(p.type || '')}</td>
                <td class="rag-col-date" style="white-space:nowrap;">${escapeHtml((p.date || '').substring(0, 16))}</td>
                <td class="rag-col-retr">${p.retrieval_count || 0}</td>
                <td class="rag-col-actions">
                    <button class="rag-action-btn" onclick="editRagPoint('${escapeHtml(p.id)}', this)" title="Edit">&#9998;</button>
                    <button class="rag-action-btn danger" onclick="deleteRagPoint('${escapeHtml(p.id)}')" title="Delete">&#10005;</button>
                </td>
            `;
            table.appendChild(tr);
        });

        const pagination = document.getElementById('ragPagination');
        if (!search) {
            pagination.style.display = 'flex';
            document.getElementById('ragPageInfo').textContent = `Page ${_ragPrevOffsets.length + 1} (${_ragTotal} total)`;
            document.getElementById('ragPrevBtn').disabled = _ragPrevOffsets.length === 0;
            document.getElementById('ragNextBtn').disabled = !_ragNextOffset;
        } else {
            pagination.style.display = 'flex';
            document.getElementById('ragPageInfo').textContent = `${data.points.length} results`;
            document.getElementById('ragPrevBtn').disabled = true;
            document.getElementById('ragNextBtn').disabled = true;
        }
    } catch(e) {
        table.innerHTML = '<tr><td colspan="7" style="text-align:center;color:var(--danger);padding:40px;">Failed to load points</td></tr>';
    }
}

function ragPage(dir) {
    if (dir === -1 && _ragPrevOffsets.length > 0) {
        _ragPageOffset = _ragPrevOffsets.pop();
        loadRagPoints();
    } else if (dir === 1 && _ragNextOffset) {
        _ragPrevOffsets.push(_ragPageOffset);
        _ragPageOffset = _ragNextOffset;
        _ragNextOffset = null;
        loadRagPoints();
    }
}

async function deleteRagPoint(pointId) {
    if (!confirm('Delete this RAG point?')) return;
    try {
        const resp = await authFetch(`/api/rag/point/${encodeURIComponent(pointId)}`, { method: 'DELETE' });
        if (!resp.ok) { showToast('Delete failed'); return; }
        showToast('Point deleted');
        loadRagStats();
        loadRagPoints();
    } catch(e) { showToast('Delete error'); }
}

async function editRagPoint(pointId, btn) {
    const row = btn.closest('tr');
    const currentText = row ? row.querySelector('.rag-text-snippet')?.textContent : '';
    const newText = prompt('Edit RAG text:', currentText || '');
    if (newText === null || newText.trim() === '' || newText === currentText) return;
    try {
        const resp = await authFetch(`/api/rag/point/${encodeURIComponent(pointId)}`, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ text: newText }),
        });
        if (!resp.ok) { showToast('Update failed'); return; }
        showToast('Point updated, re-embedded');
        loadRagStats();
        loadRagPoints();
    } catch(e) { showToast('Update error'); }
}

async function flushRagCollection() {
    if (!confirm('PERMANENTLY DELETE ALL RAG VECTORS? This cannot be undone.')) return;
    if (!confirm('Are you absolutely sure? All RAG corpus entries will be lost.')) return;
    try {
        const resp = await authFetch('/api/rag/flush', { method: 'POST' });
        const data = await resp.json();
        showToast(`Flushed ${data.deleted_count} points`);
        loadRagStats();
        loadRagPoints();
    } catch(e) { showToast('Flush failed'); }
}

async function loadConversations() {
    try {
        const resp = await authFetch('/api/conversations');
        const convs = await resp.json();
        const list = document.getElementById('convList');
        list.innerHTML = '';
        convs.forEach(c => {
            const div = document.createElement('div');
            div.className = 'conv-item' + (c.id === currentConvId ? ' active' : '');
            const attachIcon = c.attachment_count > 0 ? `<span class="conv-attach has-attachments" onclick="event.stopPropagation();showGallery('${c.id}')" title="${c.attachment_count} attachment(s)">📎</span>` : '';
            div.innerHTML = `<span class="conv-trash" onclick="event.stopPropagation();deleteConversation('${c.id}')" title="Delete conversation">🗑</span>${attachIcon}<span class="conv-title" onclick="loadConversation('${c.id}')">${c.title}</span>`;
            list.appendChild(div);
        });
    } catch(e) {}
}

async function loadConversation(convId) {
    try {
        const resp = await authFetch(`/api/conversations/${convId}`);
        const data = await resp.json();
        currentConvId = convId;
        currentModel = data.conversation.model;
        fetchModelContextSize();
        const container = document.getElementById('chatContainer');
        container.innerHTML = '';
        conversationHistory = [];
        data.messages.forEach(msg => { appendMessage(msg.role, msg.content, false, false, null, msg.perplexity); conversationHistory.push({ role: msg.role, content: msg.content }); });
                scrollToLatest();
        await loadConversations();
    } catch(e) {}
}

async function deleteConversation(convId) {
    if (currentRole !== 'admin') { requireAdminNotice(); return; }
    if (!confirm('Delete this conversation?')) return;
    try { await authFetch(`/api/conversations/${convId}`, { method: 'DELETE' }); } catch(e) { return; }
    showToast('Conversation deleted');
    if (currentConvId === convId) { currentConvId = null; showWelcome(); }
    await loadConversations();
}

async function deleteAllConversations() {
    if (currentRole !== 'admin') { requireAdminNotice(); return; }
    if (!confirm('Delete ALL conversations? This cannot be undone.')) return;
    try { await authFetch('/api/conversations', { method: 'DELETE' }); } catch(e) { return; }
    showToast('All conversations deleted');
    currentConvId = null;
    conversationHistory = [];
    showWelcome();
    await loadConversations();
}

function newChat() {
    currentConvId = null;
    conversationHistory = [];
    showWelcome();
    document.querySelectorAll('.conv-item').forEach(el => el.classList.remove('active'));
    showToast('New conversation');
}

function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; }
function showWelcome() {
    document.getElementById('chatContainer').innerHTML = '<div class="welcome-screen" id="welcomeScreen"><img src="/static/caic-bg.png" alt="cAIc" class="welcome-logo"></div>';
}

async function sendSearch() {
    if (privateEnabled) { showToast('Web search is disabled in private chat mode'); return; }
    resetScrollLock();
    const input = document.getElementById('userInput');
    const query = input.value.trim();
    if (!query || isStreaming) return;
    const model = currentModel;
    const welcome = document.getElementById('welcomeScreen');
    if (welcome) welcome.remove();
    const userDiv = appendMessage('user', '🔍 ' + query, true);
    conversationHistory.push({ role: 'user', content: '🔍 ' + query });
    input.value = '';
    input.style.height = 'auto';
    const assistantDiv = appendMessage('assistant', '', true, true, userDiv);
    const textEl = assistantDiv.querySelector('.text');
    textEl.innerHTML = '<div class="search-indicator"><div class="spinner"></div>Searching the web...</div>';
    setStreamingState(true);
    const ttrStart = performance.now();
    let ttr = 0;
    let tokenCount = 0;
    try {
        abortController = new AbortController();
        const resp = await authFetch('/api/search', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ conversation_id: currentConvId, query, model }), signal: abortController.signal });
        const reader = resp.body.getReader();
        const decoder = new TextDecoder();
        let fullText = '';
        let buffer = '';
        let firstToken = true;
        while (true) {
            const { done, value } = await reader.read();
            if (done) break;
            buffer += decoder.decode(value, { stream: true });
            const lines = buffer.split('\n');
            buffer = lines.pop();
            for (const line of lines) {
                if (!line.startsWith('data: ')) continue;
                try {
                    const data = JSON.parse(line.slice(6));
                    if (data.error) { textEl.textContent = 'Error: ' + data.error; setStreamingState(false); return; }
                    if (data.conversation_id && !currentConvId) { currentConvId = data.conversation_id; await loadConversations(); }
                    if (data.search_results) { textEl.innerHTML = '<div class="search-indicator">🔍 Found ' + data.search_results + ' results, summarizing...</div>'; }
                    if (data.token) { if (firstToken) { textEl.innerHTML = ''; firstToken = false; ttr = performance.now() - ttrStart; tokenCount = 0; } fullText += data.token; tokenCount++; textEl.innerHTML = renderMarkdown(fullText); scrollToLatest(); }
                    if (data.raw_results) {
                        let rawHtml = '<details class="raw-results"><summary>🔍 View raw search results (' + data.raw_results.length + ')</summary><ul>';
                        data.raw_results.forEach(r => { 
                            const safeUrl = sanitizeUrl(r.url || '');
                            rawHtml += '<li>';
                            if (safeUrl) {
                                rawHtml += `<a href="${escapeHtml(safeUrl)}" target="_blank" rel="noopener">${escapeHtml(r.title)}</a>`;
                            } else {
                                rawHtml += `<span>${escapeHtml(r.title)}</span>`;
                            }
                            if (r.content) rawHtml += `<small>${escapeHtml(r.content)}</small>`;
                            rawHtml += '</li>'; 
                        });
                        rawHtml += '</ul></details>';
                        textEl.innerHTML += rawHtml;
                    }
                    if (data.done) {
                        const roleLabel = assistantDiv.querySelector('.role-label');
                        if (roleLabel) {
                            roleLabel.textContent = 'web search';
                            if (ttr > 0) {
                                const ttrSec = ttr / 1000;
                                roleLabel.innerHTML += `<span class="ttr-badge search">ttr: ${ttrSec.toFixed(1)}s</span>`;
                            }
                            let usedPct = 0;
                            if (tokenCount > 0) {
                                const pt = data.prompt_tokens || 0;
                                const totalTokens = pt + tokenCount;
                                const ctxLen = data.context_length || 4096;
                                usedPct = Math.round((totalTokens / ctxLen) * 100);
                                roleLabel.innerHTML += `<span class="tok-badge">tok: ${totalTokens}/${ctxLen} ${usedPct}%</span>`;
                            }
                        }
                        conversationHistory.push({ role: 'assistant', content: fullText });
                        addCopyButtons(assistantDiv);
                        addMessageToolbar(assistantDiv);
                        setStreamingState(false);
                        cumulativeTokens += tokenCount; sessionTokens += tokenCount; updateTopbarTokens(usedPct);
                        await loadConversations();
                    }
                } catch(e) { console.log('Parse error:', e); }
            }
        }
    } catch (e) {
        if (e.name === 'AbortError') textEl.innerHTML = '<div class="typing-indicator stopped"><span></span><span></span><span></span></div><em style="color:var(--text-muted)">[stopped]</em>';
        else textEl.textContent = 'Error: ' + e.message;
        setStreamingState(false);
    }
}

function onFileSelected(event) {
    const file = event.target.files[0];
    if (!file) return;
    selectedFile = file;
    const preview = document.getElementById('filePreview');
    const thumb = document.getElementById('filePreviewThumb');
    const icon = document.getElementById('filePreviewIcon');
    const nameEl = document.getElementById('filePreviewName');
    const sizeEl = document.getElementById('filePreviewSize');
    const notice = document.getElementById('filePreviewNotice');
    nameEl.textContent = file.name;
    sizeEl.textContent = (file.size / 1024).toFixed(1) + ' KB';
    if (file.type.startsWith('image/')) {
        thumb.style.display = 'block';
        thumb.src = URL.createObjectURL(file);
        icon.style.display = 'none';
        notice.textContent = 'Model does not support image input — filename only will be used as context.';
    } else {
        thumb.style.display = 'none';
        icon.style.display = 'inline';
        icon.textContent = '📄';
        notice.textContent = '';
    }
    preview.classList.add('visible');
    event.target.value = '';
}
function clearFileSelection() {
    selectedFile = null;
    const preview = document.getElementById('filePreview');
    preview.classList.remove('visible');
    const thumb = document.getElementById('filePreviewThumb');
    thumb.src = '';
    showToast('File removed');
}
function showGallery(convId) {
    document.getElementById('galleryOverlay').classList.add('visible');
    loadGallery(convId);
}
function closeGallery(event) {
    if (event && event.target !== event.currentTarget) return;
    document.getElementById('galleryOverlay').classList.remove('visible');
}
async function loadGallery(convId) {
    const body = document.getElementById('galleryBody');
    body.innerHTML = '<div class="gallery-empty">Loading...</div>';
    try {
        const resp = await authFetch('/api/upload/by-conversation/' + encodeURIComponent(convId));
        const items = await resp.json();
        if (!items || items.length === 0) {
            body.innerHTML = '<div class="gallery-empty">No attachments in this conversation.</div>';
            return;
        }
        body.innerHTML = '';
        for (const item of items) {
            const div = document.createElement('div');
            div.className = 'gallery-item';
            const isImage = item.content_type && item.content_type.startsWith('image/');
            const thumbHtml = isImage
                ? '<span class="gallery-item-icon">🖼</span>'
                : '<span class="gallery-item-icon">📄</span>';
            const expires = new Date(item.expires_at);
            const expiresStr = expires.toLocaleString();
            div.innerHTML = thumbHtml + '<div class="gallery-item-info"><div class="gallery-item-name">' + escapeHtml(item.filename) + '</div><div class="gallery-item-meta">Expires: ' + expiresStr + '</div></div><button class="gallery-item-delete" onclick="deleteGalleryItem(' + item.id + ')">Delete</button>';
            body.appendChild(div);
        }
    } catch(e) {
        body.innerHTML = '<div class="gallery-empty">Error loading attachments.</div>';
    }
}
async function deleteGalleryItem(contextId) {
    if (!confirm('Delete this attachment? It will also be removed from the RAG corpus.')) return;
    try {
        const resp = await authFetch('/api/upload/' + contextId, { method: 'DELETE' });
        if (resp.ok) {
            await loadGallery(currentConvId);
            await loadConversations();
            showToast('Attachment deleted');
        }
    } catch(e) { showToast('Delete failed'); }
}
async function sendMessage() {
    resetScrollLock();
    const input = document.getElementById('userInput');
    const message = input.value.trim();
    if (!message || isStreaming) return;
    const model = currentModel;
    const welcome = document.getElementById('welcomeScreen');
    if (welcome) welcome.remove();
    const userDiv = appendMessage('user', message, true);
    conversationHistory.push({ role: 'user', content: message });
    messageHistory.push(message);
    if (messageHistory.length > 100) messageHistory.shift();
    historyIndex = -1;
    input.value = '';
    input.style.height = 'auto';
    const assistantDiv = appendMessage('assistant', '', true, false, userDiv);
    const textEl = assistantDiv.querySelector('.text');
    textEl.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
    setStreamingState(true);
    let searchTriggered = false;
    const ttrStart = performance.now();
    let ttr = 0;
    let tokenCount = 0;
    try {
        abortController = new AbortController();
        let uploadContextId = null;
        if (selectedFile) {
            const formData = new FormData();
            formData.append('file', selectedFile, selectedFile.name);
            formData.append('mode', 'both');
            if (currentConvId) formData.append('conversation_id', currentConvId);
            try {
                const uploadResp = await authFetch('/api/upload', { method: 'POST', body: formData, signal: abortController.signal });
                const uploadData = await uploadResp.json();
                if (uploadResp.ok && uploadData.context_id) {
                    uploadContextId = uploadData.context_id;
                    if (!currentConvId && uploadData.conversation_id) currentConvId = uploadData.conversation_id;
                } else {
                    throw new Error(uploadData.detail || 'Upload failed');
                }
            } catch(e) {
                if (e.name !== 'AbortError') textEl.textContent = 'Error uploading file: ' + e.message;
                setStreamingState(false);
                return;
            }
            clearFileSelection();
            await loadConversations();
        }
        const chatBody = { conversation_id: currentConvId, message, model };
        if (uploadContextId) chatBody.upload_context_id = uploadContextId;
        if (privateEnabled) chatBody.private_chat = true;
        const resp = await authFetch('/api/chat', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(chatBody), signal: abortController.signal });
        const reader = resp.body.getReader();
        const decoder = new TextDecoder();
        let fullText = '';
        let firstToken = true;
        let buffer = '';
        while (true) {
            const { done, value } = await reader.read();
            if (done) break;
            buffer += decoder.decode(value, { stream: true });
            const lines = buffer.split('\n');
            buffer = lines.pop();
            for (const line of lines) {
                if (!line.startsWith('data: ')) continue;
                try {
                    const data = JSON.parse(line.slice(6));
                    if (data.error) { textEl.textContent = 'Error: ' + data.error; setStreamingState(false); return; }
                    if (data.conversation_id && !currentConvId) {
                        currentConvId = data.conversation_id;
                        if (uploadContextId) {
                            authFetch('/api/upload/' + uploadContextId + '/link', { method: 'PATCH', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ conversation_id: currentConvId }) }).catch(()=>{});
                        }
                        await loadConversations();
                    }
                    if (data.searching) { textEl.innerHTML = fullText ? renderMarkdown(fullText) + '<div class="search-indicator"><div class="spinner"></div>Searching...</div>' : '<div class="search-indicator"><div class="spinner"></div>Searching...</div>'; searchTriggered = true; }
                    if (data.search_results) { textEl.innerHTML = '<div class="search-indicator">🔍 Found ' + data.search_results + ' results...</div>'; fullText = ''; firstToken = true; }
                    if (data.token) { if (firstToken) { textEl.innerHTML = ''; firstToken = false; ttr = performance.now() - ttrStart; tokenCount = 0; } fullText += data.token; tokenCount++; textEl.innerHTML = renderMarkdown(fullText); scrollToLatest(); }
                    if (data.done) {
                        const roleLabel = assistantDiv.querySelector('.role-label');
                        if (data.searched && roleLabel) roleLabel.textContent = 'web search';
                        if (typeof data.perplexity === 'number' && roleLabel) { const ppl = data.perplexity; const conf = Math.round(Math.max(0, Math.min(100, (1 - (ppl - 1) / 10) * 100))); const cls = conf >= 80 ? 'high' : conf >= 15 ? 'medium' : 'low'; roleLabel.innerHTML += `<span class="conf-badge ${cls}">${conf}%</span>`; }
                        if (typeof data.tokens_per_sec === 'number' && data.tokens_per_sec > 0 && roleLabel) roleLabel.innerHTML += `<span class="tps-badge">${data.tokens_per_sec.toFixed(1)} t/s</span>`;
                        if (roleLabel && ttr > 0) {
                            const ttrSec = ttr / 1000;
                            const ttrClass = data.searched ? 'search' : (ttrSec <= 2 ? 'low' : ttrSec < 11 ? 'medium' : 'high');
                            roleLabel.innerHTML += `<span class="ttr-badge ${ttrClass}">ttr: ${ttrSec.toFixed(1)}s</span>`;
                        }
                        let usedPct = 0;
                        if (tokenCount > 0 && roleLabel) {
                            const pt = data.prompt_tokens || 0;
                            const totalTokens = pt + tokenCount;
                            const ctxLen = data.context_length || 4096;
                            usedPct = Math.round((totalTokens / ctxLen) * 100);
                            roleLabel.innerHTML += `<span class="tok-badge">tok: ${totalTokens}/${ctxLen} ${usedPct}%</span>`;
                        }
                        conversationHistory.push({ role: 'assistant', content: fullText });
                        addCopyButtons(assistantDiv);
                        addMessageToolbar(assistantDiv);
                        setStreamingState(false);
                        cumulativeTokens += tokenCount; sessionTokens += tokenCount; updateTopbarTokens(usedPct);
                        if (data.rag_update_suggestion && data.rag_update_suggestion.conflicts) {
                            showFactConflictBanner(data.rag_update_suggestion.conflicts, data.conversation_id);
                        }
                        await loadConversations();
                        await loadMemoryStats();
                        checkOllamaStatus();
                    }
                } catch(e) {}
            }
        }
    } catch (e) {
        if (e.name === 'AbortError') textEl.innerHTML = '<div class="typing-indicator stopped"><span></span><span></span><span></span></div><em style="color:var(--text-muted)">[stopped]</em>';
        else textEl.textContent = 'Error: ' + e.message;
        setStreamingState(false);
    }
}

function setStreamingState(streaming) {
    isStreaming = streaming;
    const sendBtn = document.getElementById('sendBtn');
    const searchBtn = document.getElementById('searchBtn');
    if (streaming) { 
        sendBtn.textContent = 'STOP'; 
        sendBtn.className = 'stop-btn'; 
        sendBtn.onclick = () => { if (abortController) abortController.abort(); setStreamingState(false); }; 
        if (searchBtn) searchBtn.disabled = true;
    } else { 
        sendBtn.textContent = 'SEND'; 
        sendBtn.className = 'send-btn'; 
        sendBtn.onclick = sendMessage; 
        if (searchBtn) searchBtn.disabled = privateEnabled;
    }
}

function appendMessage(role, content, animate, isSearch = false, afterEl = null, perplexity = null) {
    const container = document.getElementById('chatContainer');
    const div = document.createElement('div');
    div.className = 'message ' + role + (isSearch && role === 'assistant' ? ' search-result' : '');
    if (!animate) div.style.animation = 'none';
    const now = new Date();
    const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
    const ms = now.getMilliseconds();
    const timeStr = months[now.getMonth()] + ' ' + String(now.getDate()).padStart(2,'0') + ', ' + now.getFullYear() + ' ' + String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0') + ':' + String(now.getSeconds()).padStart(2,'0') + '.' + String(Math.floor(ms / 10)).padStart(2,'0');
    const textHtml = content ? renderMarkdown(content) : '';
    const copyIcon = role === 'user' && content ? `<span class="user-copy-btn" data-content="${escapeHtml(content).replace(/"/g,'&quot;')}" onclick="var el=this;event.stopPropagation();var t=el.getAttribute('data-content');function d(){el.textContent='✓';setTimeout(function(){el.innerHTML='📋'},1500);showToast('Copied')}if(navigator.clipboard){navigator.clipboard.writeText(t).then(d).catch(function(){execCopy(t);d()})}else{execCopy(t);d()}">📋</span>` : '';
    let roleLabelHtml = role === 'assistant' ? modelLabel() : role;
    roleLabelHtml += role === 'user' ? ' <span class="msg-time">' + timeStr + '</span>' : '';
    if (role === 'assistant' && perplexity != null) {
        const ppl = parseFloat(perplexity);
        if (!isNaN(ppl)) {
            const conf = Math.round(Math.max(0, Math.min(100, (1 - (ppl - 1) / 10) * 100)));
            const cls = conf >= 80 ? 'high' : conf >= 15 ? 'medium' : 'low';
            roleLabelHtml += `<span class="conf-badge ${cls}">${conf}%</span>`;
        }
    }
    div.innerHTML = `<div class="avatar">${role === 'user' ? 'YOU' : 'cAIc'}</div><div class="content"><div class="role-label">${roleLabelHtml}</div><div class="text">${textHtml}${copyIcon}</div>${role === 'assistant' ? '<div class="msg-toolbar"></div>' : ''}</div>`;
    if (role === 'user') {
        const pair = document.createElement('div');
        pair.className = 'msg-pair' + (isSearch ? ' search-pair' : '');
        pair.appendChild(div);
        if (_waterfallDirection === 'newest') {
            container.prepend(pair);
        } else {
            container.appendChild(pair);
        }
    } else if (role === 'assistant') {
        const pair = afterEl ? afterEl.closest('.msg-pair') : container.querySelector('.msg-pair');
        if (pair) {
            pair.appendChild(div);
        } else if (_waterfallDirection === 'newest') {
            container.prepend(div);
        } else {
            container.appendChild(div);
        }
    } else if (_waterfallDirection === 'newest') {
        container.prepend(div);
    } else {
        container.appendChild(div);
    }
    if (content && role === 'assistant') { addCopyButtons(div); addMessageToolbar(div); }
    scrollToLatest();
    return div;
}

function renderMarkdown(text) {
    let blocks = [];
    text = text.replace(/```(\w*)\n([\s\S]*?)```/g, (m, lang, code) => { blocks.push(`<pre data-lang="${lang}"><code>${escapeHtml(code)}</code></pre>`); return '\x00BLOCK' + (blocks.length - 1) + '\x00'; });
    text = text.replace(/```([\s\S]*?)```/g, (m, code) => { blocks.push(`<pre><code>${escapeHtml(code)}</code></pre>`); return '\x00BLOCK' + (blocks.length - 1) + '\x00'; });
    let h = escapeHtml(text);
    h = h.replace(/`([^`]+)`/g, '<code>$1</code>');
    h = h.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
    h = h.replace(/\*(.+?)\*/g, '<em>$1</em>');
    h = h.replace(/\n/g, '<br>');
    h = h.replace(/\x00BLOCK(\d+)\x00/g, (m, idx) => blocks[parseInt(idx)]);
    return h;
}

function sanitizeUrl(url) {
    if (!url) return '';
    try {
        const parsed = new URL(url, window.location.origin);
        if (parsed.protocol === 'http:' || parsed.protocol === 'https:') return parsed.href;
    } catch (e) {
        return '';
    }
    return '';
}

function addCopyButtons(msgDiv) {
    msgDiv.querySelectorAll('pre').forEach(pre => {
        if (pre.querySelector('.copy-btn')) return;
        const btn = document.createElement('button');
        btn.className = 'copy-btn';
        btn.textContent = 'copy';
        btn.onclick = () => {
            const text = pre.querySelector('code')?.textContent || pre.textContent;
            if (navigator.clipboard) {
                navigator.clipboard.writeText(text).then(() => { btn.textContent = 'copied!'; setTimeout(() => btn.textContent = 'copy', 1500); }).catch(() => fallbackCopy(text, btn));
            } else { fallbackCopy(text, btn); }
        };
        pre.style.position = 'relative';
        pre.appendChild(btn);
    });
}

function addMessageToolbar(msgDiv) {
    const toolbar = msgDiv.querySelector('.msg-toolbar');
    if (!toolbar || toolbar.hasChildNodes()) return;
    const textDiv = msgDiv.querySelector('.text');
    const getText = () => textDiv ? textDiv.textContent || textDiv.innerText : '';

    const copyBtn = document.createElement('button');
    copyBtn.innerHTML = '📋';
    copyBtn.title = 'Copy response';
    copyBtn.onclick = () => {
        const t = getText();
        const done = () => { copyBtn.textContent = '✓'; setTimeout(() => copyBtn.innerHTML = '📋', 1500); showToast('Copied'); };
        if (navigator.clipboard) {
            navigator.clipboard.writeText(t).then(done).catch(() => { execCopy(t); done(); });
        } else { execCopy(t); done(); }
    };

    const printBtn = document.createElement('button');
    printBtn.innerHTML = '🖨️';
    printBtn.title = 'Print response';
    printBtn.onclick = () => {
        const w = window.open('', '_blank', 'width=800,height=600');
        w.document.write(`<!DOCTYPE html><html><head><title>Print</title><style>body{font-family:system-ui;max-width:700px;margin:40px auto;padding:0 20px;line-height:1.6;font-size:14px}pre{background:#f4f4f4;padding:12px;border-radius:6px;overflow-x:auto}code{background:#f4f4f4;padding:2px 5px;border-radius:3px}pre code{background:none;padding:0}img{max-width:100%}@media print{body{margin:0}}</style></head><body>${textDiv ? textDiv.innerHTML : ''}</body></html>`);
        w.document.close();
        setTimeout(() => { w.focus(); w.print(); }, 500);
    };

    const saveBtn = document.createElement('button');
    saveBtn.innerHTML = '💾';
    saveBtn.title = 'Save as .md';
    saveBtn.onclick = () => {
        try {
            const t = getText();
            const blob = new Blob([t], { type: 'text/markdown' });
            const a = document.createElement('a');
            a.href = URL.createObjectURL(blob);
            a.download = `caic-response-${Date.now()}.md`;
            a.click();
            URL.revokeObjectURL(a.href);
            showToast('Saved');
        } catch(e) { showToast('Save failed'); }
    };
    toolbar.append(copyBtn, printBtn, saveBtn);
}

function escapeHtml(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; }
let _waterfallDirection = localStorage.getItem('waterfallDirection') || 'newest';
function toggleDirection() {
    _waterfallDirection = _waterfallDirection === 'newest' ? 'oldest' : 'newest';
    localStorage.setItem('waterfallDirection', _waterfallDirection);
    const badge = document.getElementById('directionBadge');
    if (badge) badge.textContent = _waterfallDirection === 'newest' ? '↓ NEW' : '↑ OLD';
    const container = document.getElementById('chatContainer');
    const items = Array.from(container.children);
    items.reverse().forEach(el => container.appendChild(el));
    scrollToLatest();
}
let _userScrolledAway = false;
document.getElementById('chatContainer')?.addEventListener('scroll', function() {
    if (_waterfallDirection === 'newest') {
        _userScrolledAway = this.scrollTop > 100;
    } else {
        _userScrolledAway = this.scrollTop < this.scrollHeight - this.clientHeight - 60;
    }
}, { passive: true });
function scrollToLatest() {
    const c = document.getElementById('chatContainer');
    if (!c) return;
    if (_waterfallDirection === 'newest') {
        if (_userScrolledAway) return;
        c.scrollTop = 0;
    } else {
        requestAnimationFrame(() => {
            const cc = document.getElementById('chatContainer');
            if (cc) cc.scrollTop = cc.scrollHeight;
        });
    }
}
function resetScrollLock() { _userScrolledAway = false; }
function scrollToBottom() { scrollToLatest(); }
function execCopy(text) {
    const ta = document.createElement('textarea');
    ta.value = text;
    ta.style.position = 'fixed';
    ta.style.opacity = '0';
    document.body.appendChild(ta);
    ta.select();
    document.execCommand('copy');
    document.body.removeChild(ta);
}
function showToast(msg) {
    const el = document.createElement('div');
    el.className = 'toast-error';
    el.textContent = msg;
    document.body.appendChild(el);
    setTimeout(() => el.remove(), 2000);
}
function fallbackCopy(text, btn) {
    const ta = document.createElement('textarea');
    ta.value = text;
    ta.style.position = 'fixed';
    ta.style.opacity = '0';
    document.body.appendChild(ta);
    ta.select();
    document.execCommand('copy');
    document.body.removeChild(ta);
    if (btn) btn.textContent = 'copied!';
    if (btn) setTimeout(() => btn.textContent = 'copy', 1500);
}

const userInput = document.getElementById('userInput');
const messageHistory = [];
let historyIndex = -1;
let historyDraft = '';
let cumulativeTokens = 0;
let sessionTokens = 0;
let lastContextPct = 0;

function updateTopbarTokens(pct) {
    if (typeof pct === 'number') lastContextPct = pct;
    const countEl = document.getElementById('tokValue');
    const pctEl = document.getElementById('tokPct');
    if (countEl) countEl.textContent = cumulativeTokens.toLocaleString();
    if (pctEl) {
        const cls = lastContextPct >= 95 ? 'red' : lastContextPct >= 80 ? 'orange' : 'green';
        pctEl.textContent = lastContextPct + '%';
        pctEl.className = 'ss-val ' + cls;
    }
}
updateTopbarTokens(0);
userInput.addEventListener('input', function() { this.style.height = 'auto'; this.style.height = Math.min(this.scrollHeight, 200) + 'px'; });
userInput.addEventListener('keydown', e => {
    if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey) { e.preventDefault(); sendMessage(); return; }
    if (e.key === 'Enter' && e.ctrlKey) { e.preventDefault(); sendSearch(); return; }
    if (e.key === 'ArrowUp') {
        e.preventDefault();
        if (messageHistory.length === 0) return;
        if (historyIndex === -1) historyDraft = userInput.value;
        if (historyIndex < messageHistory.length - 1) {
            historyIndex++;
            userInput.value = messageHistory[messageHistory.length - 1 - historyIndex];
        }
        userInput.selectionStart = userInput.selectionEnd = userInput.value.length;
        userInput.dispatchEvent(new Event('input'));
        return;
    }
    if (e.key === 'ArrowDown') {
        e.preventDefault();
        if (historyIndex === -1) return;
        historyIndex--;
        userInput.value = historyIndex === -1 ? historyDraft : messageHistory[messageHistory.length - 1 - historyIndex];
        userInput.selectionStart = userInput.selectionEnd = userInput.value.length;
        userInput.dispatchEvent(new Event('input'));
        return;
    }
});
const toastStyle = document.createElement('style');
toastStyle.textContent = '.toast-error { position:fixed; bottom:80px; left:50%; transform:translateX(-50%); background:var(--danger); color:#fff; padding:12px 24px; border-radius:var(--radius); font-family:var(--font-body); font-size:14px; z-index:9999; animation:fadeIn 0.2s; } @keyframes fadeIn { from{opacity:0;transform:translateX(-50%) translateY(10px)} to{opacity:1;transform:translateX(-50%) translateY(0)} } .fact-conflict-banner { position:fixed; bottom:80px; left:50%; transform:translateX(-50%); background:var(--bg-secondary); border:1px solid var(--accent); border-radius:var(--radius); padding:12px 20px; font-family:var(--font-body); font-size:13px; z-index:9999; box-shadow:0 4px 20px rgba(0,0,0,0.3); max-width:600px; line-height:1.5; } .fact-conflict-banner .actions { margin-top:8px; display:flex; gap:8px; } .fact-conflict-banner button { padding:4px 14px; border-radius:var(--radius); cursor:pointer; font-size:12px; } .fact-conflict-banner .btn-confirm { background:var(--accent); color:var(--bg-primary); border:none; } .fact-conflict-banner .btn-dismiss { background:transparent; color:var(--text-muted); border:1px solid var(--border); }';
document.head.appendChild(toastStyle);
function showFactConflictBanner(conflicts, convId) {
    if (!conflicts || !conflicts.length) return;
    const existing = document.querySelector('.fact-conflict-banner');
    if (existing) existing.remove();
    let html = '<div class="fact-conflict-banner">';
    html += '<strong>📝 Knowledge conflict detected</strong>';
    html += '<div style="margin-top:6px;font-size:12px;color:var(--text-muted);">';
    conflicts.forEach((c, i) => {
        html += `<div style="margin-top:${i>0?'8':'4'}px">`;
        html += `<div><span style="color:var(--text-muted)">Stored:</span> ${escapeHtml(c.old_fact)}</div>`;
        html += `<div><span style="color:var(--accent)">Now:</span> ${escapeHtml(c.new_fact)}</div>`;
        html += '<div class="actions">';
        html += `<button class="btn-confirm" onclick="confirmFactUpdate(${c.memory_id},${Date.now()},this)">Update knowledge</button>`;
        html += '<button class="btn-dismiss" onclick="this.closest(\'.fact-conflict-banner\').remove()">Dismiss</button>';
        html += '</div></div>';
    });
    html += '</div></div>';
    document.body.insertAdjacentHTML('beforeend', html);
}

async function confirmFactUpdate(memoryId, ts, btn) {
    const banner = btn.closest('.fact-conflict-banner');
    const text = banner ? banner.textContent : '';
    const lines = text.split('\n').map(l => l.trim()).filter(l => l);
    const newFact = lines.length > 0 ? lines[lines.length-1].replace(/^Now:\s*/, '') : '';
    btn.textContent = 'Updating...';
    btn.disabled = true;
    try {
        const resp = await authFetch('/api/memories/confirm-update', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                memory_id: memoryId,
                new_fact: newFact,
                old_fact: '',
                user_message: '',
                assistant_message: '',
            }),
        });
        if (resp.ok) {
            btn.textContent = '✓ Updated';
            setTimeout(() => { if (banner) banner.remove(); }, 1500);
        } else {
            btn.textContent = 'Failed';
            btn.style.background = 'var(--danger)';
        }
    } catch(e) {
        btn.textContent = 'Error';
        btn.style.background = 'var(--danger)';
    }
}

userInput.addEventListener('paste', e => {
    const hasImage = Array.from(e.clipboardData.items).some(i => i.type.startsWith('image/'));
    if (hasImage) {
        e.preventDefault();
        const toast = document.createElement('div');
        toast.className = 'toast-error';
        toast.textContent = 'Use the 📎 button to attach images — paste is text only.';
        document.body.appendChild(toast);
        setTimeout(() => toast.remove(), 3000);
    }
});
</script>
</body>
</html>
