Phase 8: TypeScript migration, i18n rewrite, Activity Tree, Projects API, Heartbeats
This commit is contained in:
127
static/ui.js
127
static/ui.js
@@ -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){
|
||||
|
||||
Reference in New Issue
Block a user