Phase 8: TypeScript migration, i18n rewrite, Activity Tree, Projects API, Heartbeats

This commit is contained in:
Rose
2026-04-29 11:50:00 +02:00
parent c705fad626
commit 255914c9f1
43 changed files with 17948 additions and 6899 deletions

View File

@@ -3,7 +3,17 @@ const SHOW_ALL_TOOLS=false; // Toggle to show all tools (default: only active)
let showAllToolsState=false; // Runtime state for toggle button
const INFLIGHT={}; // keyed by session_id while request in-flight
const SESSION_QUEUES={}; // keyed by session_id for queued follow-up turns
const $=id=>document.getElementById(id);
$=window.$||(id=>document.getElementById(id));
const AGENT_META = {
rose: {emoji:'🌹', name:'Rose'},
lotus: {emoji:'🪷', name:'Lotus'},
'forget-me-not':{emoji:'🌼', name:'Forget-me-not'},
sunflower: {emoji:'🌻', name:'Sunflower'},
iris: {emoji:'⚜️', name:'Iris'},
ivy: {emoji:'🌿', name:'Ivy'},
dandelion: {emoji:'🛡️', name:'Dandelion'},
root: {emoji:'🌳', name:'Root'},
};
function _getSessionQueue(sid, create=false){
if(!sid) return [];
if(!SESSION_QUEUES[sid]&&create) SESSION_QUEUES[sid]=[];
@@ -91,7 +101,6 @@ async function populateModelDropdown(){
_applyModelToDropdown(data.default_model, sel);
}
if(typeof syncModelChip==='function') syncModelChip();
if(typeof syncAgentChip==='function') syncAgentChip();
// Kick off a background live-model fetch for the active provider.
// This runs after the static list is already shown (no blocking flicker).
if(data.active_provider) _fetchLiveModels(data.active_provider, sel);
@@ -99,7 +108,6 @@ async function populateModelDropdown(){
// API unavailable -- keep the hardcoded HTML options as fallback
console.warn('Failed to load models from server:',e.message);
if(typeof syncModelChip==='function') syncModelChip();
if(typeof syncAgentChip==='function') syncAgentChip();
}
}
@@ -150,7 +158,6 @@ async function _fetchLiveModels(provider, sel){
// Restore selection
if(currentVal) _applyModelToDropdown(currentVal, sel);
if(typeof syncModelChip==='function') syncModelChip();
if(typeof syncAgentChip==='function') syncAgentChip();
console.log('[hermes] Live models loaded for',provider+':',added,'new models added');
}
}catch(e){
@@ -310,114 +317,89 @@ function closeModelDropdown(){
if(chip) chip.classList.remove('active');
}
document.addEventListener('click',e=>{
if(!e.target.closest('#composerModelChip') && !e.target.closest('#composerModelDropdown')) closeModelDropdown();
});
window.addEventListener('resize',()=>{
const dd=$('composerModelDropdown');
if(dd&&dd.classList.contains('open')) _positionModelDropdown();
});
// ── Agent selector dropdown ─────────────────────────────────────────────────
const AGENT_META = {
rose: {emoji:'🌹', name:'Rose', domain:'Orchestrator & Main Interface'},
lotus: {emoji:'🪷', name:'Lotus', domain:'Health, Fitness & Recovery'},
'forget-me-not':{emoji:'🌼', name:'Forget-me-not', domain:'Calendar, Time & Social'},
sunflower: {emoji:'🌻', name:'Sunflower', domain:'Finance, Wealth & Subscriptions'},
iris: {emoji:'⚜️', name:'Iris', domain:'Career, Learning & Focus'},
ivy: {emoji:'🌿', name:'Ivy', domain:'Smart Home & Environment'},
dandelion: {emoji:'🛡️', name:'Dandelion', domain:'Communication Triage'},
root: {emoji:'🌳', name:'Root', domain:'DevOps, Logs & System Health'},
};
function renderAgentDropdown(){
const dd=$('composerAgentDropdown');
if(!dd) return;
const sel=$('agentSelect');
if(!dd||!sel) return;
const current=sel.value;
const groups={'Tier-1':['rose'],'Tier-2':['lotus','forget-me-not','sunflower','iris','ivy','dandelion','root']};
const current=sel?sel.value:'rose';
const agents=[
{id:'rose',emoji:'🌹',name:'Rose',domain:'Orchestrator'},
{id:'lotus',emoji:'🪷',name:'Lotus',domain:'Health'},
{id:'forget-me-not',emoji:'🌼',name:'Forget-me-not',domain:'Calendar'},
{id:'sunflower',emoji:'🌻',name:'Sunflower',domain:'Finance'},
{id:'iris',emoji:'⚜️',name:'Iris',domain:'Career'},
{id:'ivy',emoji:'🌿',name:'Ivy',domain:'Smart Home'},
{id:'dandelion',emoji:'🛡️',name:'Dandelion',domain:'Security'},
{id:'root',emoji:'🌳',name:'Root',domain:'DevOps'}
];
let html='';
for(const [grp,ids] of Object.entries(groups)){
html+=`<div class="model-group">${grp}</div>`;
for(const id of ids){
const m=AGENT_META[id];
const active=id===current?' active':'';
html+=`<div class="agent-opt${active}" onclick="selectAgentFromDropdown('${id}')">
<span class="agent-opt-name">${m.emoji} ${m.name}</span>
<span class="agent-opt-domain">${m.domain}</span>
</div>`;
}
for(const m of agents){
const active=m.id===current?' active':'';
html+=`<div class="agent-opt${active}" onclick="selectAgentFromDropdown('${m.id}')">`;
html+=`<span class="agent-opt-name">${m.emoji} ${m.name}</span>`;
html+=`<span class="agent-opt-domain">${m.domain}</span></div>`;
}
dd.innerHTML=html;
}
function toggleAgentDropdown(){
const dd=$('composerAgentDropdown');
const chip=$('composerAgentChip');
if(!dd||!chip) return;
if(dd.classList.contains('open')){
dd.classList.remove('open');
chip.classList.remove('active');
return;
}
if(dd.classList.contains('open')){dd.classList.remove('open');chip.classList.remove('active');return;}
closeModelDropdown();
if(typeof closeProfileDropdown==='function') closeProfileDropdown();
if(typeof closeWsDropdown==='function') closeWsDropdown();
renderAgentDropdown();
dd.classList.add('open');
chip.classList.add('active');
// position below chip
const chipRect=chip.getBoundingClientRect();
const wrap=chip.closest('.composer-agent-wrap');
const wrapRect=wrap.getBoundingClientRect();
dd.style.left=(chipRect.left-wrapRect.left)+'px';
dd.style.left=chipRect.left-wrapRect.left+'px';
}
function closeAgentDropdown(){
const dd=$('composerAgentDropdown');
const chip=$('composerAgentChip');
if(dd) dd.classList.remove('open');
if(chip) chip.classList.remove('active');
}
function selectAgentFromDropdown(value){
const sel=$('agentSelect');
if(!sel) return;
const prevAgent=sel.value;
sel.value=value;
syncAgentChip();
closeAgentDropdown();
// Save to session / localStorage
if(typeof S!=='undefined'&&S.session) S.session.agent=value;
if(typeof S!=='undefined') S.session={...S.session,agent:value};
try{localStorage.setItem('hermes-webui-agent',value);}catch(e){}
try{localStorage.setItem('hermes.chat_agent',value);}catch(e){}
if(prevAgent!==value&&typeof S!=='undefined'&&S.session&&S.messages){
const newMeta=AGENT_META[value]||{emoji:'🌹',name:value||'Rose'};
const sysMsg={role:'system',content:`\u2190 Switched to ${newMeta.emoji} **${newMeta.name}**`,_ts:Math.floor(Date.now()/1e3)};
S.messages.push(sysMsg);
if(typeof renderMessages==='function') renderMessages();
}
}
function syncAgentChip(){
const sel=$('agentSelect');
const icon=$('composerAgentIcon');
const label=$('composerAgentLabel');
if(!sel||!icon||!label) return;
const m=AGENT_META[sel.value]||AGENT_META.rose;
const m=AGENT_META[sel.value]||{emoji:'🌹',name:sel.value||'Rose'};
icon.textContent=m.emoji;
label.textContent=m.name;
const topIcon=$('agentSelectorIcon');
const topLabel=$('agentSelectorLabel');
if(topIcon) topIcon.textContent=m.emoji;
if(topLabel) topLabel.textContent=m.name;
}
// Init agent chip from localStorage on load
window.addEventListener('DOMContentLoaded',()=>{
try{
const saved=localStorage.getItem('hermes-webui-agent');
if(saved){
const sel=$('agentSelect');
if(sel&&Array.from(sel.options).some(o=>o.value===saved)){
sel.value=saved;
}
}
}catch(e){}
syncAgentChip();
});
document.addEventListener('click',e=>{
if(!e.target.closest('#composerAgentChip') && !e.target.closest('#composerAgentDropdown')) closeAgentDropdown();
if(!e.target.closest('#composerModelChip') && !e.target.closest('#composerModelDropdown')) closeModelDropdown();
});
window.addEventListener('resize',()=>{
const dd=$('composerModelDropdown');
if(dd&&dd.classList.contains('open')) _positionModelDropdown();
});
// ── Scroll pinning ──────────────────────────────────────────────────────────
@@ -1138,7 +1120,6 @@ function syncTopbar(){
document.title=window._botName||'Hermes';
if(typeof syncWorkspaceDisplays==='function') syncWorkspaceDisplays();
if(typeof syncModelChip==='function') syncModelChip();
if(typeof syncAgentChip==='function') syncAgentChip();
if(typeof _syncHermesPanelSessionActions==='function') _syncHermesPanelSessionActions();
else {
const sidebarName=$('sidebarWsName');
@@ -1149,8 +1130,9 @@ function syncTopbar(){
return;
}
const sessionTitle=S.session.title||t('untitled');
$('topbarTitle').textContent=sessionTitle;
document.title=sessionTitle+' \u2014 '+(window._botName||'Hermes');
const agentMeta=S.session.agent?(AGENT_META[S.session.agent]||null):null;
$('topbarTitle').textContent=(agentMeta?agentMeta.emoji+' ':'')+sessionTitle;
document.title=(agentMeta?agentMeta.emoji+' ':'')+sessionTitle+' \u2014 '+(window._botName||'Hermes');
const vis=S.messages.filter(m=>m&&m.role&&m.role!=='tool');
$('topbarMeta').textContent=t('n_messages',vis.length);
// If a profile switch just happened, apply its model rather than the session's stale value.
@@ -1178,7 +1160,6 @@ function syncTopbar(){
}
}
if(typeof syncModelChip==='function') syncModelChip();
if(typeof syncAgentChip==='function') syncAgentChip();
// Show Clear button only when session has messages
const clearBtn=$('btnClearConv');
if(clearBtn) clearBtn.style.display=(S.messages&&S.messages.filter(msg=>msg.role!=='tool').length>0)?'':'none';
@@ -1390,6 +1371,10 @@ function renderMessages(){
scrollToBottom();
// Apply syntax highlighting after DOM is built
requestAnimationFrame(()=>{highlightCode();addCopyButtons();renderMermaidBlocks();renderKatexBlocks();});
// Refresh todo panel if it's currently open
if(typeof loadTodos==='function' && document.getElementById('panelTodos') && document.getElementById('panelTodos').classList.contains('active')){
loadTodos();
}
}
function toolIcon(name){