Phase 7: Agent Selector — per-agent soul.md + ChromaDB memory filtering
- Agent dropdown UI (chip button + hidden select) in composer header - Session.agent field persists agent selection across refresh - soul.md loaded per-agent via ephemeral_system_prompt injection - ChromaDB memory filtered by agent topic (lotus/, sunflower/, etc.) - Fixed streaming.py: agent→_ai_agent variable shadowing (lines 1161, 1163) - New API endpoints: /api/agents/topology, /api/agents/memory/search - Agent metadata registry with emoji, name, description per Tier-2 agent
This commit is contained in:
111
static/ui.js
111
static/ui.js
@@ -91,6 +91,7 @@ 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);
|
||||
@@ -98,6 +99,7 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +150,7 @@ 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){
|
||||
@@ -315,6 +318,108 @@ window.addEventListener('resize',()=>{
|
||||
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');
|
||||
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']};
|
||||
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>`;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
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';
|
||||
}
|
||||
|
||||
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;
|
||||
sel.value=value;
|
||||
syncAgentChip();
|
||||
closeAgentDropdown();
|
||||
// Save to session / localStorage
|
||||
if(typeof S!=='undefined'&&S.session) S.session.agent=value;
|
||||
try{localStorage.setItem('hermes-webui-agent',value);}catch(e){}
|
||||
}
|
||||
|
||||
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;
|
||||
icon.textContent=m.emoji;
|
||||
label.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();
|
||||
});
|
||||
|
||||
// ── Scroll pinning ──────────────────────────────────────────────────────────
|
||||
// When streaming, auto-scroll only if the user hasn't manually scrolled up.
|
||||
// Once the user scrolls back to within 80px of the bottom, re-pin.
|
||||
@@ -1033,6 +1138,7 @@ 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');
|
||||
@@ -1072,6 +1178,7 @@ 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';
|
||||
@@ -1283,10 +1390,6 @@ 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