Initial commit - Aaron AI v1
This commit is contained in:
@@ -0,0 +1,518 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Aaron AI</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500&family=IBM+Plex+Mono:wght@400&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg:#faf9f6;--bg2:#f0ede6;--bg3:#e8e4dc;--sidebar-bg:#eceae4;
|
||||
--border:#dddad2;--border2:#ccc9c0;--text:#1a1a18;--text2:#555550;--text3:#999990;
|
||||
--accent:#2d5a3d;--accent-light:#edf5f0;--accent-border:#c8dece;--accent-text:#1a3a26;
|
||||
--user-bg:#e8e4dc;--font-size:15px;
|
||||
--font:'IBM Plex Sans',sans-serif;--mono:'IBM Plex Mono',monospace;
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--bg:#1a1a18;--bg2:#222220;--bg3:#2a2a28;--sidebar-bg:#111110;
|
||||
--border:#2a2a28;--border2:#383836;--text:#e8e8e0;--text2:#aaa89e;--text3:#555550;
|
||||
--accent-light:#1e2e22;--accent-border:#2a3e2e;--accent-text:#a8d5b5;--user-bg:#2a2a28;
|
||||
}
|
||||
[data-font="small"]{--font-size:13px}[data-font="medium"]{--font-size:15px}[data-font="large"]{--font-size:17px}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--font);font-size:var(--font-size)}
|
||||
.app{display:flex;height:100vh;overflow:hidden}
|
||||
.sidebar{width:260px;background:var(--sidebar-bg);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}
|
||||
.sidebar-header{padding:14px;border-bottom:1px solid var(--border)}
|
||||
.new-btn{width:100%;background:var(--accent);color:#e8f5ed;border:none;border-radius:8px;padding:9px 14px;font-size:13px;font-family:var(--font);cursor:pointer;text-align:left;display:flex;align-items:center;gap:8px}
|
||||
.new-btn:hover{opacity:0.85}
|
||||
.section-label{padding:10px 14px 4px;font-size:11px;color:var(--text3);letter-spacing:.06em;text-transform:uppercase}
|
||||
.conv-list{flex:1;overflow-y:auto;padding:4px 8px 8px}
|
||||
.conv-item{padding:8px 10px;border-radius:7px;cursor:pointer;margin-bottom:1px;display:flex;align-items:center;gap:6px}
|
||||
.conv-item:hover,.conv-item.active{background:var(--bg3)}
|
||||
.conv-text{flex:1;min-width:0}
|
||||
.conv-title{font-size:13px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.conv-date{font-size:11px;color:var(--text3);margin-top:1px}
|
||||
.conv-delete{opacity:0;background:none;border:none;color:var(--text3);cursor:pointer;padding:2px 4px;border-radius:4px;font-size:14px}
|
||||
.conv-item:hover .conv-delete{opacity:1}
|
||||
.conv-delete:hover{color:#a32d2d}
|
||||
.main{flex:1;display:flex;flex-direction:column;min-width:0;background:var(--bg)}
|
||||
.topbar{padding:12px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
|
||||
.topbar-title{font-size:13px;color:var(--text3)}
|
||||
.topbar-title strong{color:var(--text);font-weight:500}
|
||||
.topbar-gear{background:none;border:none;color:var(--text3);cursor:pointer;padding:5px;border-radius:6px;font-size:17px}
|
||||
.topbar-gear:hover{background:var(--bg3);color:var(--text)}
|
||||
.messages{flex:1;overflow-y:auto;padding:28px 20px;display:flex;flex-direction:column;gap:22px}
|
||||
.msg{display:flex;flex-direction:column;max-width:760px;width:100%}
|
||||
.msg.user{align-self:flex-end;align-items:flex-end;max-width:640px}
|
||||
.msg.assistant{align-self:flex-start;align-items:flex-start}
|
||||
.msg-label{font-size:11px;color:var(--text3);margin-bottom:4px}
|
||||
.msg-bubble{padding:13px 17px;border-radius:12px;line-height:1.75}
|
||||
.msg.user .msg-bubble{background:var(--user-bg);color:var(--text);border-radius:12px 12px 3px 12px}
|
||||
.msg.assistant .msg-bubble{background:var(--accent-light);color:var(--accent-text);border-radius:12px 12px 12px 3px;border:1px solid var(--accent-border)}
|
||||
.msg-sources{font-size:11px;color:var(--text3);margin-top:6px;font-style:italic}
|
||||
.msg-bubble p{margin-bottom:.75em}.msg-bubble p:last-child{margin-bottom:0}
|
||||
.msg-bubble strong{font-weight:500;color:var(--text)}
|
||||
.msg-bubble code{font-family:var(--mono);font-size:.88em;background:var(--bg3);padding:1px 5px;border-radius:4px}
|
||||
.msg-bubble pre{background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:12px 14px;overflow-x:auto;margin:.75em 0}
|
||||
.msg-bubble pre code{background:none;padding:0}
|
||||
.msg-bubble h1,.msg-bubble h2,.msg-bubble h3{font-weight:500;margin:.75em 0 .4em;color:var(--text)}
|
||||
.msg-bubble ul,.msg-bubble ol{padding-left:1.4em;margin:.5em 0}
|
||||
.msg-bubble li{margin-bottom:.3em}
|
||||
.msg-bubble hr{border:none;border-top:1px solid var(--border);margin:1em 0}
|
||||
.msg-bubble blockquote{border-left:3px solid var(--accent-border);padding-left:12px;color:var(--text2);margin:.5em 0}
|
||||
.thinking{color:var(--text3);font-style:italic;animation:pulse 1.5s infinite}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
||||
.empty-state{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text3);text-align:center;padding:40px}
|
||||
.empty-state h2{font-size:18px;font-weight:500;color:var(--text2);margin-bottom:8px}
|
||||
.empty-state p{font-size:14px;line-height:1.6;max-width:340px}
|
||||
.input-area{padding:14px 18px;border-top:1px solid var(--border);display:flex;gap:10px;align-items:flex-end;flex-shrink:0}
|
||||
.input-wrap{flex:1;background:var(--bg2);border:1px solid var(--border2);border-radius:10px}
|
||||
textarea#input{width:100%;background:none;border:none;outline:none;padding:11px 14px;font-size:var(--font-size);font-family:var(--font);color:var(--text);resize:none;min-height:44px;max-height:160px;line-height:1.5;display:block}
|
||||
textarea#input::placeholder{color:var(--text3)}
|
||||
.send-btn{background:var(--accent);color:#e8f5ed;border:none;border-radius:8px;padding:11px 20px;font-size:14px;font-family:var(--font);cursor:pointer;flex-shrink:0}
|
||||
.send-btn:hover{opacity:0.85}
|
||||
.send-btn:disabled{opacity:.4;cursor:not-allowed}
|
||||
.settings-overlay{position:fixed;top:0;right:0;bottom:0;width:340px;background:var(--bg);border-left:1px solid var(--border);display:flex;flex-direction:column;z-index:100;transform:translateX(100%);transition:transform .2s ease}
|
||||
.settings-overlay.open{transform:translateX(0)}
|
||||
.settings-header{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
|
||||
.settings-header h2{font-size:15px;font-weight:500;color:var(--text)}
|
||||
.close-settings{background:none;border:none;color:var(--text3);font-size:20px;cursor:pointer;padding:2px 6px;border-radius:4px;line-height:1}
|
||||
.close-settings:hover{background:var(--bg3)}
|
||||
.settings-body{flex:1;overflow-y:auto;padding:18px 20px}
|
||||
.s-section{margin-bottom:22px}
|
||||
.s-section-title{font-size:11px;color:var(--text3);letter-spacing:.06em;text-transform:uppercase;margin-bottom:10px}
|
||||
.s-row{display:flex;align-items:center;justify-content:space-between;padding:9px 0;border-bottom:1px solid var(--border);gap:12px}
|
||||
.s-row:last-child{border-bottom:none}
|
||||
.s-row-info{flex:1;min-width:0}
|
||||
.s-label{font-size:14px;color:var(--text)}
|
||||
.s-desc{font-size:12px;color:var(--text3);margin-top:2px}
|
||||
.toggle{width:36px;height:20px;background:var(--accent);border-radius:10px;position:relative;cursor:pointer;flex-shrink:0;border:none}
|
||||
.toggle-knob{width:16px;height:16px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;pointer-events:none}
|
||||
.toggle[data-off="true"]{background:var(--border2)}
|
||||
.toggle[data-off="true"] .toggle-knob{right:auto;left:2px}
|
||||
.s-select{background:var(--bg3);border:1px solid var(--border2);border-radius:6px;padding:5px 8px;font-size:13px;font-family:var(--font);color:var(--text)}
|
||||
.s-btn{background:none;border:1px solid var(--border2);border-radius:6px;padding:5px 12px;font-size:12px;font-family:var(--font);color:var(--text2);cursor:pointer;white-space:nowrap}
|
||||
.s-btn:hover{background:var(--bg3)}
|
||||
.s-btn.danger{color:#a32d2d;border-color:#f7c1c1}
|
||||
.s-btn.primary{color:var(--accent);border-color:var(--accent-border)}
|
||||
.s-btn.primary:hover{background:var(--accent-light)}
|
||||
.stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px}
|
||||
.stat-card{background:var(--accent-light);border:1px solid var(--accent-border);border-radius:8px;padding:10px 12px}
|
||||
.stat-num{font-size:20px;font-weight:500;color:var(--accent)}
|
||||
.stat-lbl{font-size:11px;color:var(--text3);margin-top:2px}
|
||||
.status-indicator{display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid var(--border)}
|
||||
.status-indicator:last-child{border-bottom:none}
|
||||
.dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
||||
.dot.green{background:#2d7a3d}.dot.yellow{background:#c8821a}.dot.red{background:#a32d2d}
|
||||
.status-name{font-size:13px;color:var(--text);flex:1}
|
||||
.status-val{font-size:12px;color:var(--text3)}
|
||||
.memory-preview{background:var(--bg3);border-radius:8px;padding:10px 12px;font-size:12px;color:var(--text2);line-height:1.6;margin-bottom:10px;font-family:var(--mono);max-height:90px;overflow:hidden;white-space:pre-wrap}
|
||||
.memory-editor{width:100%;background:var(--bg3);border:1px solid var(--border2);border-radius:8px;padding:10px 12px;font-size:12px;font-family:var(--mono);color:var(--text);line-height:1.6;resize:vertical;min-height:140px;margin-bottom:8px}
|
||||
.memory-editor:focus{outline:2px solid var(--accent)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app" id="app">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<button class="new-btn" onclick="newConversation()">
|
||||
<span style="font-size:16px;line-height:1">+</span> New conversation
|
||||
</button>
|
||||
</div>
|
||||
<div id="conv-list-container" style="flex:1;overflow:hidden;display:flex;flex-direction:column"></div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="topbar">
|
||||
<div class="topbar-title"><strong>Aaron AI</strong> — personal knowledge assistant</div>
|
||||
<button class="topbar-gear" onclick="toggleSettings()" title="Settings">⚙</button>
|
||||
</div>
|
||||
<div id="messages" class="messages">
|
||||
<div class="empty-state" id="empty-state">
|
||||
<h2>What are you working on?</h2>
|
||||
<p>Ask about your documents, projects, research, or anything else. Your entire corpus is available.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-area">
|
||||
<div class="input-wrap">
|
||||
<textarea id="input" placeholder="Ask anything... (Shift+Enter for new line)" rows="1"></textarea>
|
||||
</div>
|
||||
<button class="send-btn" id="send-btn" onclick="sendMessage()">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-overlay" id="settings-panel">
|
||||
<div class="settings-header">
|
||||
<h2>Settings</h2>
|
||||
<button class="close-settings" onclick="toggleSettings()">×</button>
|
||||
</div>
|
||||
<div class="settings-body" id="settings-body">
|
||||
<div style="color:var(--text3);font-size:13px;padding:20px 0;text-align:center">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const API = {
|
||||
async get(p){const r=await fetch(p,{credentials:'same-origin'});return r.json()},
|
||||
async post(p,d){const r=await fetch(p,{credentials:'same-origin',method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(d)});return r.json()},
|
||||
async patch(p,d){const r=await fetch(p,{credentials:'same-origin',method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(d)});return r.json()},
|
||||
async del(p){const r=await fetch(p,{credentials:'same-origin',method:'DELETE'});return r.json()},
|
||||
};
|
||||
|
||||
let state={conversations:[],currentId:null,messages:[],settings:{},status:{},settingsOpen:false};
|
||||
|
||||
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
||||
|
||||
function fmt(s){
|
||||
s=esc(s);
|
||||
s=s.replace(/`([^`]+)`/g,'<code>$1</code>');
|
||||
s=s.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>');
|
||||
s=s.replace(/\*([^*]+)\*/g,'<em>$1</em>');
|
||||
return s;
|
||||
}
|
||||
|
||||
function renderMarkdown(text){
|
||||
if(!text)return'';
|
||||
const lines=text.split('\n');
|
||||
let html='',inCode=false,codeLines=[],listItems=[];
|
||||
|
||||
function flushList(){
|
||||
if(!listItems.length)return'';
|
||||
const r='<ul>'+listItems.map(i=>'<li>'+i+'</li>').join('')+'</ul>';
|
||||
listItems=[];
|
||||
return r;
|
||||
}
|
||||
|
||||
for(let i=0;i<lines.length;i++){
|
||||
const line=lines[i];
|
||||
if(line.startsWith('```')){
|
||||
if(inCode){
|
||||
html+=flushList();
|
||||
html+='<pre><code>'+esc(codeLines.join('\n'))+'</code></pre>';
|
||||
codeLines=[];inCode=false;
|
||||
} else {inCode=true;}
|
||||
continue;
|
||||
}
|
||||
if(inCode){codeLines.push(line);continue;}
|
||||
const t=line.trim();
|
||||
if(!t){html+=flushList();continue;}
|
||||
if(t.startsWith('### ')){html+=flushList();html+='<h3>'+fmt(t.slice(4))+'</h3>';continue;}
|
||||
if(t.startsWith('## ')){html+=flushList();html+='<h2>'+fmt(t.slice(3))+'</h2>';continue;}
|
||||
if(t.startsWith('# ')){html+=flushList();html+='<h1>'+fmt(t.slice(2))+'</h1>';continue;}
|
||||
if(t==='---'){html+=flushList();html+='<hr>';continue;}
|
||||
if(t.startsWith('> ')){html+=flushList();html+='<blockquote>'+fmt(t.slice(2))+'</blockquote>';continue;}
|
||||
if(t.startsWith('- ')||t.startsWith('* ')){listItems.push(fmt(t.slice(2)));continue;}
|
||||
html+=flushList();
|
||||
html+='<p>'+fmt(line)+'</p>';
|
||||
}
|
||||
html+=flushList();
|
||||
return html;
|
||||
}
|
||||
|
||||
function formatDate(iso){
|
||||
const d=new Date(iso),now=new Date(),diff=(now-d)/1000;
|
||||
if(diff<60)return'just now';
|
||||
if(diff<3600)return Math.floor(diff/60)+'m ago';
|
||||
if(diff<86400)return Math.floor(diff/3600)+'h ago';
|
||||
if(diff<172800)return'yesterday';
|
||||
return d.toLocaleDateString('en-US',{month:'short',day:'numeric'});
|
||||
}
|
||||
|
||||
function groupConvs(convs){
|
||||
const now=new Date(),r={today:[],yesterday:[],week:[],older:[]};
|
||||
convs.forEach(c=>{
|
||||
const diff=(now-new Date(c.updated_at))/86400000;
|
||||
if(diff<1)r.today.push(c);
|
||||
else if(diff<2)r.yesterday.push(c);
|
||||
else if(diff<7)r.week.push(c);
|
||||
else r.older.push(c);
|
||||
});
|
||||
return r;
|
||||
}
|
||||
|
||||
function renderConvList(){
|
||||
const container=document.getElementById('conv-list-container');
|
||||
const g=groupConvs(state.conversations);
|
||||
const labels={today:'Today',yesterday:'Yesterday',week:'This week',older:'Older'};
|
||||
let html='<div class="conv-list">';
|
||||
for(const[key,label]of Object.entries(labels)){
|
||||
if(!g[key].length)continue;
|
||||
html+=`<div class="section-label">${label}</div>`;
|
||||
g[key].forEach(c=>{
|
||||
const active=c.id===state.currentId?' active':'';
|
||||
html+=`<div class="conv-item${active}" onclick="loadConversation('${c.id}')">
|
||||
<div class="conv-text">
|
||||
<div class="conv-title" title="${esc(c.title)}">${esc(c.title)}</div>
|
||||
<div class="conv-date">${formatDate(c.updated_at)}</div>
|
||||
</div>
|
||||
<button class="conv-delete" onclick="deleteConv(event,'${c.id}')">×</button>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
if(!state.conversations.length)html+='<div style="padding:20px 14px;font-size:13px;color:var(--text3)">No conversations yet</div>';
|
||||
html+='</div>';
|
||||
container.innerHTML=html;
|
||||
}
|
||||
|
||||
function renderMessages(){
|
||||
const el=document.getElementById('messages');
|
||||
const empty=document.getElementById('empty-state');
|
||||
if(!state.messages.length){
|
||||
if(empty)empty.style.display='flex';
|
||||
el.querySelectorAll('.msg').forEach(m=>m.remove());
|
||||
return;
|
||||
}
|
||||
if(empty)empty.style.display='none';
|
||||
let html='';
|
||||
state.messages.forEach(m=>{
|
||||
const bubble=renderMarkdown(m.content);
|
||||
const sources=m.sources&&m.sources.length?`<div class="msg-sources">Sources: ${[...new Set(m.sources)].join(', ')}</div>`:'';
|
||||
html+=`<div class="msg ${m.role}">
|
||||
<div class="msg-label">${m.role==='user'?'you':'aaron ai'}</div>
|
||||
<div class="msg-bubble">${bubble}</div>
|
||||
${sources}
|
||||
</div>`;
|
||||
});
|
||||
el.innerHTML=(empty?empty.outerHTML:'')+html;
|
||||
if(empty){
|
||||
const e=document.getElementById('empty-state');
|
||||
if(e)e.style.display='none';
|
||||
}
|
||||
el.scrollTop=el.scrollHeight;
|
||||
}
|
||||
|
||||
async function loadConversations(){
|
||||
state.conversations=await API.get('/api/conversations');
|
||||
renderConvList();
|
||||
}
|
||||
|
||||
async function loadConversation(id){
|
||||
state.currentId=id;
|
||||
state.messages=await API.get(`/api/conversations/${id}/messages`);
|
||||
renderMessages();
|
||||
renderConvList();
|
||||
document.getElementById('input').focus();
|
||||
}
|
||||
|
||||
async function newConversation(){
|
||||
const data=await API.post('/api/conversations',{title:'New conversation'});
|
||||
state.currentId=data.id;
|
||||
state.messages=[];
|
||||
await loadConversations();
|
||||
renderMessages();
|
||||
document.getElementById('input').focus();
|
||||
}
|
||||
|
||||
async function deleteConv(e,id){
|
||||
e.stopPropagation();
|
||||
if(!confirm('Delete this conversation?'))return;
|
||||
await API.del(`/api/conversations/${id}`);
|
||||
if(state.currentId===id){state.currentId=null;state.messages=[];renderMessages();}
|
||||
await loadConversations();
|
||||
}
|
||||
|
||||
async function sendMessage(){
|
||||
const input=document.getElementById('input');
|
||||
const text=input.value.trim();
|
||||
if(!text)return;
|
||||
input.value='';
|
||||
input.style.height='auto';
|
||||
|
||||
if(!state.currentId){
|
||||
const data=await API.post('/api/conversations',{title:'New conversation'});
|
||||
state.currentId=data.id;
|
||||
}
|
||||
|
||||
state.messages.push({role:'user',content:text,sources:[]});
|
||||
renderMessages();
|
||||
|
||||
const el=document.getElementById('messages');
|
||||
const thinking=document.createElement('div');
|
||||
thinking.className='msg assistant';
|
||||
thinking.innerHTML='<div class="msg-label">aaron ai</div><div class="msg-bubble thinking">Thinking...</div>';
|
||||
el.appendChild(thinking);
|
||||
el.scrollTop=el.scrollHeight;
|
||||
document.getElementById('send-btn').disabled=true;
|
||||
|
||||
try{
|
||||
const data=await API.post('/api/chat',{message:text,conversation_id:state.currentId});
|
||||
thinking.remove();
|
||||
state.currentId=data.conversation_id;
|
||||
state.messages.push({role:'assistant',content:data.response,sources:data.sources||[]});
|
||||
renderMessages();
|
||||
await loadConversations();
|
||||
}catch(e){
|
||||
thinking.innerHTML='<div class="msg-label">aaron ai</div><div class="msg-bubble" style="color:#a32d2d">Error — please try again.</div>';
|
||||
}
|
||||
document.getElementById('send-btn').disabled=false;
|
||||
document.getElementById('input').focus();
|
||||
}
|
||||
|
||||
function toggleSettings(){
|
||||
state.settingsOpen=!state.settingsOpen;
|
||||
const panel=document.getElementById('settings-panel');
|
||||
if(state.settingsOpen){panel.classList.add('open');loadSettingsPanel();}
|
||||
else panel.classList.remove('open');
|
||||
}
|
||||
|
||||
async function loadSettingsPanel(){
|
||||
const[settings,status]=await Promise.all([API.get('/api/settings'),API.get('/api/status')]);
|
||||
state.settings=settings;state.status=status;
|
||||
renderSettingsPanel();
|
||||
}
|
||||
|
||||
function renderSettingsPanel(){
|
||||
const s=state.settings,st=state.status;
|
||||
document.getElementById('settings-body').innerHTML=`
|
||||
<div class="s-section">
|
||||
<div class="s-section-title">Appearance</div>
|
||||
<div class="s-row">
|
||||
<div class="s-row-info"><div class="s-label">Dark mode</div><div class="s-desc">Switch between light and dark theme</div></div>
|
||||
<button class="toggle" data-off="${s.theme!=='dark'}" onclick="toggleSetting('theme','${s.theme==='dark'?'light':'dark'}')"><div class="toggle-knob"></div></button>
|
||||
</div>
|
||||
<div class="s-row">
|
||||
<div class="s-row-info"><div class="s-label">Font size</div></div>
|
||||
<select class="s-select" onchange="updateSetting('font_size',this.value)">
|
||||
<option value="small" ${s.font_size==='small'?'selected':''}>Small (13px)</option>
|
||||
<option value="medium" ${s.font_size==='medium'?'selected':''}>Medium (15px)</option>
|
||||
<option value="large" ${s.font_size==='large'?'selected':''}>Large (17px)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="s-section">
|
||||
<div class="s-section-title">Corpus</div>
|
||||
<div class="stat-grid">
|
||||
<div class="stat-card"><div class="stat-num">${(st.chunk_count||0).toLocaleString()}</div><div class="stat-lbl">total chunks</div></div>
|
||||
<div class="stat-card"><div class="stat-num">${(st.file_count||0).toLocaleString()}</div><div class="stat-lbl">files indexed</div></div>
|
||||
</div>
|
||||
<div class="s-row">
|
||||
<div class="s-row-info"><div class="s-label">Last indexed</div><div class="s-desc">${st.last_indexed||'Unknown'}</div></div>
|
||||
<button class="s-btn primary" onclick="triggerReindex()">Re-index</button>
|
||||
</div>
|
||||
<div class="s-row">
|
||||
<div class="s-row-info"><div class="s-label">Web search</div><div class="s-desc">Search the web for current information</div></div>
|
||||
<button class="toggle" data-off="${!s.web_search}" onclick="toggleSetting('web_search',${!s.web_search})"><div class="toggle-knob"></div></button>
|
||||
</div>
|
||||
<div class="s-row">
|
||||
<div class="s-row-info"><div class="s-label">Show sources</div><div class="s-desc">Display document sources under responses</div></div>
|
||||
<button class="toggle" data-off="${!s.show_sources}" onclick="toggleSetting('show_sources',${!s.show_sources})"><div class="toggle-knob"></div></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="s-section">
|
||||
<div class="s-section-title">Memory</div>
|
||||
<div id="memory-section">
|
||||
<div class="memory-preview" id="memory-preview">Loading...</div>
|
||||
<div style="display:flex;gap:8px"><button class="s-btn primary" onclick="openMemoryEditor()">Edit memory</button></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="s-section">
|
||||
<div class="s-section-title">Conversations</div>
|
||||
<div class="s-row">
|
||||
<div class="s-row-info"><div class="s-label">Stored conversations</div></div>
|
||||
<div style="font-size:14px;color:var(--text);font-weight:500">${st.conversation_count||0}</div>
|
||||
</div>
|
||||
<div class="s-row">
|
||||
<div class="s-row-info"><div class="s-label">Export current conversation</div><div class="s-desc">Download as markdown</div></div>
|
||||
<button class="s-btn" onclick="exportConversation()">Export</button>
|
||||
</div>
|
||||
<div class="s-row">
|
||||
<div class="s-row-info"><div class="s-label">Clear all conversations</div><div class="s-desc">Permanently delete history</div></div>
|
||||
<button class="s-btn danger" onclick="clearAllConversations()">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="s-section">
|
||||
<div class="s-section-title">System</div>
|
||||
<div class="status-indicator"><div class="dot ${st.aaron_ai==='running'?'green':'red'}"></div><div class="status-name">Aaron AI service</div><div class="status-val">${st.aaron_ai||'unknown'}</div></div>
|
||||
<div class="status-indicator"><div class="dot ${st.watcher==='running'?'green':'red'}"></div><div class="status-name">File watcher</div><div class="status-val">${st.watcher||'unknown'}</div></div>
|
||||
<div class="status-indicator"><div class="dot green"></div><div class="status-name">Nextcloud files</div><div class="status-val">${(st.file_count||0).toLocaleString()} files</div></div>
|
||||
<div class="status-indicator"><div class="dot yellow"></div><div class="status-name">Model</div><div class="status-val">${st.model||'unknown'}</div></div>
|
||||
</div>`;
|
||||
API.get('/api/memory').then(data=>{
|
||||
const el=document.getElementById('memory-preview');
|
||||
if(el)el.textContent=(data.content||'').split('\n').slice(0,6).join('\n');
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleSetting(key,value){
|
||||
state.settings[key]=value;
|
||||
await API.post('/api/settings',{[key]:value});
|
||||
applySettings();
|
||||
renderSettingsPanel();
|
||||
}
|
||||
|
||||
async function updateSetting(key,value){
|
||||
state.settings[key]=value;
|
||||
await API.post('/api/settings',{[key]:value});
|
||||
applySettings();
|
||||
}
|
||||
|
||||
function applySettings(){
|
||||
const app=document.getElementById('app');
|
||||
app.setAttribute('data-theme',state.settings.theme||'light');
|
||||
app.setAttribute('data-font',state.settings.font_size||'medium');
|
||||
}
|
||||
|
||||
async function triggerReindex(){
|
||||
const btn=event.target;
|
||||
btn.textContent='Starting...';btn.disabled=true;
|
||||
await API.post('/api/reindex',{});
|
||||
btn.textContent='Started';
|
||||
setTimeout(()=>{btn.textContent='Re-index';btn.disabled=false;},3000);
|
||||
}
|
||||
|
||||
async function openMemoryEditor(){
|
||||
const data=await API.get('/api/memory');
|
||||
document.getElementById('memory-section').innerHTML=`
|
||||
<textarea class="memory-editor" id="memory-editor">${esc(data.content)}</textarea>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="s-btn primary" onclick="saveMemory()">Save</button>
|
||||
<button class="s-btn" onclick="renderSettingsPanel()">Cancel</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function saveMemory(){
|
||||
const content=document.getElementById('memory-editor').value;
|
||||
await API.post('/api/memory',{content});
|
||||
renderSettingsPanel();
|
||||
}
|
||||
|
||||
function exportConversation(){
|
||||
if(!state.messages.length){alert('No messages to export.');return;}
|
||||
let md=`# Conversation Export\n\nExported: ${new Date().toLocaleString()}\n\n---\n\n`;
|
||||
state.messages.forEach(m=>{
|
||||
md+=`**${m.role==='user'?'You':'Aaron AI'}**\n\n${m.content}\n\n`;
|
||||
if(m.sources&&m.sources.length)md+=`*Sources: ${m.sources.join(', ')}*\n\n`;
|
||||
md+='---\n\n';
|
||||
});
|
||||
const a=document.createElement('a');
|
||||
a.href=URL.createObjectURL(new Blob([md],{type:'text/markdown'}));
|
||||
a.download=`conversation-${Date.now()}.md`;
|
||||
a.click();
|
||||
}
|
||||
|
||||
async function clearAllConversations(){
|
||||
if(!confirm('Delete all conversations permanently?'))return;
|
||||
await API.del('/api/conversations');
|
||||
state.currentId=null;state.messages=[];
|
||||
await loadConversations();renderMessages();renderSettingsPanel();
|
||||
}
|
||||
|
||||
document.getElementById('input').addEventListener('input',function(){
|
||||
this.style.height='auto';
|
||||
this.style.height=Math.min(this.scrollHeight,160)+'px';
|
||||
});
|
||||
document.getElementById('input').addEventListener('keydown',function(e){
|
||||
if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMessage();}
|
||||
});
|
||||
|
||||
async function init(){
|
||||
const settings=await API.get('/api/settings');
|
||||
state.settings=settings;
|
||||
applySettings();
|
||||
await loadConversations();
|
||||
if(state.conversations.length)await loadConversation(state.conversations[0].id);
|
||||
}
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user