feat: Phase 3 model toggle — cycle chat-role slot models in UI
Replaces the role-cycle toggle with a slot model toggle in the Context & Memory panel. The active model label is shown on the button; clicking cycles through Primary → Backup 1 → Backup 2 slots configured for the Chat role. - app.js: remove activeRole()/availableRoles role-cycling; add activeChatModel()/chatModels slot cycling; update send/orchestrate payloads to send slot + chat_role:"chat"; fix updateSendBtnTitle and startRunTimer to use activeChatModel() - chat.py: add slot field to ChatRequest; pass slot= to complete(); resolve backend_label from slot config; add _chat_slot_models() helper; include chat_models in GET /backend response - HELP.md: update Model toggle description, tool count (62/16), Backends section, API chat payload example Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -313,8 +313,8 @@
|
||||
});
|
||||
|
||||
// ── Tools toggle ─────────────────────────────────────────────
|
||||
// When on: submit goes to POST /orchestrate (Gemini tool loop → active role responds).
|
||||
// When off: submit goes to POST /chat (direct to active role, no tools).
|
||||
// When on: submit goes to POST /orchestrate (orchestrator tool loop → active model responds).
|
||||
// When off: submit goes to POST /chat (direct to active model, no tools).
|
||||
let toolsEnabled = localStorage.getItem('tools-enabled') === 'true';
|
||||
let _runStart = 0;
|
||||
let _runTimer = null;
|
||||
@@ -335,9 +335,8 @@
|
||||
});
|
||||
|
||||
function updateSendBtnTitle() {
|
||||
const role = activeRole();
|
||||
const rmodel = role?.model_label || '(server default)';
|
||||
const rname = role?.label || 'Chat';
|
||||
const entry = activeChatModel();
|
||||
const rmodel = entry?.label || '(server default)';
|
||||
const mode = current_mode === 'otr' ? 'Off The Record'
|
||||
: current_mode === 'note' ? 'Note'
|
||||
: 'Chat';
|
||||
@@ -347,13 +346,13 @@
|
||||
if (useOrch) {
|
||||
const omodel = orchestratorModel || '(server default)';
|
||||
lines = [
|
||||
`Role: ${rname} · ${rmodel}`,
|
||||
`Model: ${rmodel}`,
|
||||
`Orchestrator: ${omodel} (tool loop)`,
|
||||
`Mode: ${mode}`,
|
||||
];
|
||||
} else {
|
||||
lines = [
|
||||
`Role: ${rname} · ${rmodel}`,
|
||||
`Model: ${rmodel}`,
|
||||
`Mode: ${mode}`,
|
||||
`Engine: Direct (no tool loop)`,
|
||||
];
|
||||
@@ -364,14 +363,13 @@
|
||||
function startRunTimer() {
|
||||
_runStart = Date.now();
|
||||
function tick() {
|
||||
const secs = Math.floor((Date.now() - _runStart) / 1000);
|
||||
const role = activeRole();
|
||||
const rname = role?.label || 'Chat';
|
||||
const secs = Math.floor((Date.now() - _runStart) / 1000);
|
||||
const entry = activeChatModel();
|
||||
const useOrch = toolsEnabled && current_mode !== 'note';
|
||||
const model = useOrch
|
||||
const model = useOrch
|
||||
? (orchestratorModel || '(server default)') + ' (tool loop)'
|
||||
: (role?.model_label || '(server default)');
|
||||
stopBtn.title = `Running: ${rname} · ${model}\nElapsed: ${secs}s — click to cancel`;
|
||||
: (entry?.label || '(server default)');
|
||||
stopBtn.title = `Running: Chat · ${model}\nElapsed: ${secs}s — click to cancel`;
|
||||
}
|
||||
tick();
|
||||
_runTimer = setInterval(tick, 1000);
|
||||
@@ -469,23 +467,24 @@
|
||||
document.addEventListener('click', () => personaDropEl.classList.remove('open'));
|
||||
}
|
||||
|
||||
// ── Role toggle ──────────────────────────────────────────────
|
||||
// Cycles through roles that have a primary model assigned (excluding orchestrator).
|
||||
// Sends chat_role ("chat"|"coder"|"research"|...) in chat requests.
|
||||
// Falls back to "chat" when no roles are configured in the registry.
|
||||
// ── Model toggle (Phase 3) ───────────────────────────────────
|
||||
// Cycles through the chat role's configured slot models (primary → backup_1 → …).
|
||||
// Shows the model label on the button; sends slot + chat_role:"chat" in requests.
|
||||
// Falls back to "chat" / no slot when no models are configured.
|
||||
|
||||
const TYPE_CLASS = { claude_cli: '', gemini_api: 'mem-on', gemini_cli: 'mem-on', local_openai: 'local-on' };
|
||||
const backendModelHint = document.getElementById('backend-model-hint');
|
||||
|
||||
let availableRoles = []; // [{role, label, model_label, type}] from /backend
|
||||
let roleIdx = 0;
|
||||
let orchestratorModel = null; // label of the orchestrator-role model
|
||||
let chatModels = []; // [{slot, label, type}] for chat-role slots
|
||||
let availableRoles = []; // [{role, label, model_label, type}] — kept for banner check
|
||||
let modelIdx = 0;
|
||||
let orchestratorModel = null;
|
||||
|
||||
function activeRole() {
|
||||
return availableRoles.length > 0 ? availableRoles[roleIdx] : null;
|
||||
function activeChatModel() {
|
||||
return chatModels.length > 0 ? chatModels[modelIdx] : null;
|
||||
}
|
||||
|
||||
function setRoleToggleUI(entry) {
|
||||
function setModelToggleUI(entry) {
|
||||
if (!entry) {
|
||||
backendToggle.textContent = 'chat';
|
||||
backendToggle.className = 'ctx-btn';
|
||||
@@ -493,19 +492,16 @@
|
||||
backendToggle.textContent = entry.label;
|
||||
backendToggle.className = 'ctx-btn ' + (TYPE_CLASS[entry.type] || '');
|
||||
}
|
||||
if (backendModelHint) {
|
||||
const hint = entry?.model_label || '';
|
||||
backendModelHint.textContent = hint;
|
||||
backendModelHint.style.display = hint ? '' : 'none';
|
||||
}
|
||||
if (backendModelHint) backendModelHint.style.display = 'none';
|
||||
updateSendBtnTitle();
|
||||
}
|
||||
|
||||
fetch('/backend').then(r => r.json()).then(d => {
|
||||
chatModels = d.chat_models || [];
|
||||
availableRoles = d.available_roles || [];
|
||||
orchestratorModel = d.orchestrator_model || null;
|
||||
roleIdx = 0;
|
||||
setRoleToggleUI(availableRoles[0] || null);
|
||||
modelIdx = 0;
|
||||
setModelToggleUI(chatModels[0] || null);
|
||||
_maybeShowNoBanner(availableRoles);
|
||||
});
|
||||
|
||||
@@ -527,17 +523,16 @@
|
||||
style="background:none;border:none;color:#78350f;cursor:pointer;font-size:1rem;line-height:1;padding:0 0.2rem;"
|
||||
title="Dismiss">✕</button>
|
||||
`;
|
||||
// Insert at the top of #chat-col (or body if not found)
|
||||
const col = document.getElementById('chat-col') || document.body.firstElementChild;
|
||||
col.insertBefore(banner, col.firstChild);
|
||||
}
|
||||
|
||||
backendToggle.addEventListener('click', () => {
|
||||
if (availableRoles.length <= 1) return;
|
||||
roleIdx = (roleIdx + 1) % availableRoles.length;
|
||||
const entry = availableRoles[roleIdx];
|
||||
setRoleToggleUI(entry);
|
||||
addMessage('system', `Role: ${entry.label} · ${entry.model_label}`);
|
||||
if (chatModels.length <= 1) return;
|
||||
modelIdx = (modelIdx + 1) % chatModels.length;
|
||||
const entry = chatModels[modelIdx];
|
||||
setModelToggleUI(entry);
|
||||
addMessage('system', `Model: ${entry.label}`);
|
||||
});
|
||||
|
||||
// ── Sessions panel ───────────────────────────────────────────
|
||||
@@ -1346,7 +1341,8 @@
|
||||
include_mid: memMid,
|
||||
include_short: memShort,
|
||||
off_record: isOtr,
|
||||
chat_role: activeRole()?.role || 'chat',
|
||||
chat_role: 'chat',
|
||||
slot: activeChatModel()?.slot || null,
|
||||
user: CORTEX_USER,
|
||||
persona: CORTEX_PERSONA,
|
||||
};
|
||||
@@ -1377,7 +1373,8 @@
|
||||
include_mid: memMid,
|
||||
include_short: memShort,
|
||||
off_record: current_mode === 'otr',
|
||||
chat_role: activeRole()?.role || 'chat',
|
||||
chat_role: 'chat',
|
||||
slot: activeChatModel()?.slot || null,
|
||||
user: CORTEX_USER,
|
||||
persona: CORTEX_PERSONA,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user