Panel agents answer the user prompt in parallel (read-only, non-streaming); their responses are merged into the worker's last user message as a <panel_responses> block, and the session agent synthesizes one final streamed answer. The RunPanel engine is built layer-ready for future MoA. - New session fields: multi_model_mode (off/fusion), panel_agent_ids (CSV) with idempotent column migration - New partial Services/AnthropicService.Panel.cs holding the RunPanel engine - Provider-agnostic members: Anthropic RunNonStreamingToolLoop / OpenAI CompleteText / CLI plan-mode; all forced read-only, persisted with Tag=panel - SSE events: panel_start, panel_member_start, panel_member_done, panel_done - Frontend: multi-agent picker, live panel bubbles, history rendering - Graceful degradation: panel errors fall back to worker-only Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
403 lines
16 KiB
JavaScript
403 lines
16 KiB
JavaScript
// ── Session Edit Page ──
|
||
|
||
let _sessionAvatarEditor = null;
|
||
let _sessionContextMde = null;
|
||
|
||
registerPage('session', {
|
||
async init(params) {
|
||
const sessionId = params.id;
|
||
const isNew = !sessionId;
|
||
let session = null;
|
||
|
||
if (!isNew) {
|
||
session = await apiGetSession(sessionId);
|
||
if (!session) return navigateTo('dashboard');
|
||
}
|
||
|
||
const titleEl = document.getElementById('sessionPageTitle');
|
||
const nameEl = document.getElementById('sessionName');
|
||
const descEl = document.getElementById('sessionDescription');
|
||
const contextEl = document.getElementById('sessionContext');
|
||
const workspaceEl = document.getElementById('sessionWorkspace');
|
||
const agentSelect = document.getElementById('agentSelect');
|
||
const agentSelectCard = document.getElementById('agentSelectCard');
|
||
const preparerAgentSelect = document.getElementById('preparerAgentSelect');
|
||
const preparerAgentSelectCard = document.getElementById('preparerAgentSelectCard');
|
||
const reviewAgentSelect = document.getElementById('reviewAgentSelect');
|
||
const reviewAgentSelectCard = document.getElementById('reviewAgentSelectCard');
|
||
const reviewIterationsRow = document.getElementById('reviewIterationsRow');
|
||
const maxReviewIterationsEl = document.getElementById('maxReviewIterations');
|
||
const finalizerAgentSelect = document.getElementById('finalizerAgentSelect');
|
||
const finalizerAgentSelectCard = document.getElementById('finalizerAgentSelectCard');
|
||
const panelModeEl = document.getElementById('multiModelMode');
|
||
const panelAgentsRow = document.getElementById('panelAgentsRow');
|
||
const panelAgentsCard = document.getElementById('panelAgentsCard');
|
||
const panelAgentIdsEl = document.getElementById('panelAgentIds');
|
||
const imagePreview = document.getElementById('sessionImagePreview');
|
||
const saveBtn = document.getElementById('sessionSaveBtn');
|
||
const cancelBtn = document.getElementById('sessionCancelBtn');
|
||
const deleteBtn = document.getElementById('sessionDeleteBtn');
|
||
const commandsList = document.getElementById('customCommandsList');
|
||
const addCommandBtn = document.getElementById('addCommandBtn');
|
||
const unityMcpUrlEl = document.getElementById('sessionUnityMcpUrl');
|
||
const unityMcpTokenEl = document.getElementById('sessionUnityMcpToken');
|
||
const ownerRow = document.getElementById('sessionOwnerRow');
|
||
const userSelectCard = document.getElementById('sessionUserSelectCard');
|
||
const userSelect = document.getElementById('sessionUserSelect');
|
||
const more = document.querySelector('.more');
|
||
|
||
// Avatar Editor initialisieren
|
||
_sessionAvatarEditor = AvatarEditor.init({
|
||
previewEl: imagePreview,
|
||
initialImage: (session && session.image) || null,
|
||
initialScale: (session && session.avatarScale) || 1,
|
||
initialOffsetX: (session && session.avatarOffsetX) || 0,
|
||
initialOffsetY: (session && session.avatarOffsetY) || 0
|
||
});
|
||
|
||
if (session) {
|
||
titleEl.textContent = session.name || 'Unbenannt';
|
||
nameEl.value = session.name || '';
|
||
descEl.value = session.description || '';
|
||
workspaceEl.value = session.workspace || '';
|
||
deleteBtn.hidden = false;
|
||
|
||
// Custom Commands laden
|
||
loadCommandsIntoUI(commandsList, session.customCommands);
|
||
|
||
// Unity MCP
|
||
unityMcpUrlEl.value = session.unityMcpUrl || '';
|
||
unityMcpTokenEl.value = session.unityMcpToken || '';
|
||
} else {
|
||
titleEl.textContent = 'Neue Session';
|
||
deleteBtn.hidden = true;
|
||
}
|
||
|
||
// EasyMDE für Kontext
|
||
_sessionContextMde = createMDE(contextEl, {
|
||
initialValue: (session && session.context) || '',
|
||
placeholder: 'Zusätzlicher Kontext für den System-Prompt (optional)'
|
||
});
|
||
|
||
// Befehl hinzufügen
|
||
addCommandBtn.addEventListener('click', () => createCommandRow(commandsList));
|
||
|
||
// Telegram-Section nur für existierende Sessions
|
||
if (!isNew) {
|
||
initSessionTelegram(sessionId, session);
|
||
}
|
||
|
||
// Agents laden + Skills parallel
|
||
const agents = await apiGetAvailableAgents();
|
||
const agentMap = {};
|
||
for (const a of agents) agentMap[a.agentId] = a;
|
||
|
||
const agentSkillsMap = {};
|
||
for (const a of agents) {
|
||
agentSkillsMap[a.agentId] = a.skills || [];
|
||
}
|
||
|
||
function renderAgentInCard(cardEl, agent, emptyIcon, emptyText) {
|
||
const inner = agent ? buildAgentCard(agent, agentSkillsMap[agent.agentId] || []) : null;
|
||
_renderCardPreview(cardEl, inner, emptyIcon, emptyText, 'agent-select-change');
|
||
}
|
||
|
||
function updateReviewVisibility() {
|
||
reviewIterationsRow.hidden = !reviewAgentSelect.value;
|
||
}
|
||
|
||
// Helper: Agent-Picker für eine Rolle einrichten (Initial-Rendering + Click-Handler)
|
||
function setupAgentPicker(cardEl, selectEl, initialId, emptyIcon, emptyText, onChange) {
|
||
if (initialId) {
|
||
selectEl.value = initialId;
|
||
renderAgentInCard(cardEl, agentMap[initialId] || null, emptyIcon, emptyText);
|
||
} else {
|
||
renderAgentInCard(cardEl, null, emptyIcon, emptyText);
|
||
}
|
||
cardEl.addEventListener('click', () => {
|
||
openAgentPickerModal(agents, agentSkillsMap, selectEl.value, (selectedId) => {
|
||
selectEl.value = selectedId || '';
|
||
renderAgentInCard(cardEl, selectedId ? agentMap[selectedId] : null, emptyIcon, emptyText);
|
||
if (onChange) onChange(selectedId);
|
||
});
|
||
});
|
||
}
|
||
|
||
setupAgentPicker(agentSelectCard, agentSelect, session && session.agentId, 'fa-user-circle-o', 'Kein Agent ausgewählt');
|
||
setupAgentPicker(preparerAgentSelectCard, preparerAgentSelect, session && session.preparerAgentId, 'fa-filter', 'Kein Vorbereiter-Agent');
|
||
setupAgentPicker(reviewAgentSelectCard, reviewAgentSelect, session && session.reviewAgentId, 'fa-check-circle-o', 'Kein Review-Agent', updateReviewVisibility);
|
||
if (session && session.reviewAgentId) maxReviewIterationsEl.value = session.maxReviewIterations || 3;
|
||
updateReviewVisibility();
|
||
setupAgentPicker(finalizerAgentSelectCard, finalizerAgentSelect, session && session.finalizerAgentId, 'fa-flag-checkered', 'Kein Finalizer-Agent');
|
||
|
||
// Multi-Modell-Panel (Fusion): Modus + Mehrfach-Agenten-Picker
|
||
function renderPanelInCard() {
|
||
var ids = (panelAgentIdsEl.value || '').split(',').map(function(s){ return s.trim(); }).filter(Boolean);
|
||
if (ids.length === 0) {
|
||
panelAgentsCard.innerHTML = '<div class="agent-select-empty"><i class="fa fa-users"></i> Keine Panel-Agenten</div>';
|
||
return;
|
||
}
|
||
var chips = ids.map(function(id) {
|
||
var a = agentMap[id];
|
||
var label = a ? a.name : id.slice(0, 8);
|
||
var model = a ? ((typeof prettyModelName === 'function') ? prettyModelName(a.model) : a.model) : '';
|
||
return '<span class="badge">' + esc(label) + (model ? ' · ' + esc(model) : '') + '</span>';
|
||
}).join(' ');
|
||
panelAgentsCard.innerHTML = '<div style="display:flex;flex-wrap:wrap;gap:4px;align-items:center">' + chips + '</div>'
|
||
+ '<span class="agent-select-change"><i class="fa fa-pencil"></i></span>';
|
||
}
|
||
function updatePanelVisibility() {
|
||
panelAgentsRow.hidden = (panelModeEl.value === 'off');
|
||
}
|
||
panelModeEl.value = (session && session.multiModelMode) || 'off';
|
||
panelAgentIdsEl.value = (session && session.panelAgentIds) || '';
|
||
renderPanelInCard();
|
||
updatePanelVisibility();
|
||
panelModeEl.addEventListener('change', updatePanelVisibility);
|
||
panelAgentsCard.addEventListener('click', function() {
|
||
var currentIds = (panelAgentIdsEl.value || '').split(',').map(function(s){ return s.trim(); }).filter(Boolean);
|
||
openMultiAgentPickerModal(agents, agentSkillsMap, currentIds, function(newIds) {
|
||
panelAgentIdsEl.value = newIds.join(',');
|
||
renderPanelInCard();
|
||
});
|
||
});
|
||
|
||
// User-Zuweisung (jeder Besitzer, nur für bestehende Sessions)
|
||
if (!isNew) {
|
||
initSessionUserPicker(ownerRow, userSelectCard, userSelect, session && session.userId);
|
||
}
|
||
|
||
initMoreButton(more);
|
||
|
||
// Workspace Browse
|
||
const workspaceBrowse = document.getElementById('sessionWorkspaceBrowse');
|
||
if (workspaceBrowse) {
|
||
workspaceBrowse.addEventListener('click', () => {
|
||
const defaultPath = (currentUser && currentUser.defaultPath) || '';
|
||
const currentVal = workspaceEl.value.trim();
|
||
// Für den Folder-Browser den absoluten Startpfad zusammenbauen
|
||
let startPath = '/';
|
||
if (defaultPath && currentVal) {
|
||
startPath = defaultPath.replace(/\/+$/, '') + '/' + currentVal;
|
||
} else if (defaultPath) {
|
||
startPath = defaultPath;
|
||
} else if (currentVal) {
|
||
startPath = currentVal;
|
||
}
|
||
openFolderBrowser({
|
||
startPath,
|
||
basePath: defaultPath || null,
|
||
onSelect(absolutePath) {
|
||
// Relativen Pfad zum DefaultPath berechnen
|
||
if (defaultPath && absolutePath.startsWith(defaultPath)) {
|
||
let rel = absolutePath.slice(defaultPath.length).replace(/^\/+/, '');
|
||
workspaceEl.value = rel || '';
|
||
} else {
|
||
// Kein DefaultPath oder Pfad außerhalb → absoluten Pfad beibehalten (Legacy)
|
||
workspaceEl.value = absolutePath;
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Speichern
|
||
saveBtn.addEventListener('click', async () => {
|
||
const commands = getCommandsFromUI(commandsList);
|
||
const data = {
|
||
name: nameEl.value.trim() || 'Unbenannt',
|
||
description: descEl.value.trim(),
|
||
image: _sessionAvatarEditor.getImage(),
|
||
agentId: agentSelect.value || null,
|
||
reviewAgentId: reviewAgentSelect.value || null,
|
||
maxReviewIterations: parseInt(maxReviewIterationsEl.value) || 3,
|
||
finalizerAgentId: finalizerAgentSelect.value || null,
|
||
preparerAgentId: preparerAgentSelect.value || null,
|
||
multiModelMode: panelModeEl.value || 'off',
|
||
panelAgentIds: panelAgentIdsEl.value || null,
|
||
context: _sessionContextMde.value().trim() || null,
|
||
workspace: workspaceEl.value.trim() || null,
|
||
customCommands: commands,
|
||
avatarScale: _sessionAvatarEditor.getScale(),
|
||
avatarOffsetX: _sessionAvatarEditor.getOffsetX(),
|
||
avatarOffsetY: _sessionAvatarEditor.getOffsetY(),
|
||
unityMcpUrl: unityMcpUrlEl.value.trim() || null,
|
||
unityMcpToken: unityMcpTokenEl.value.trim() || null
|
||
};
|
||
|
||
// Session einem anderen Benutzer zuweisen. Wechselt der Besitzer tatsächlich,
|
||
// erst rückfragen – die Session verschwindet danach aus der eigenen Übersicht.
|
||
if (!isNew && userSelect.value && userSelect.value !== (session && session.userId)) {
|
||
var targetLabel = userSelect.dataset.userLabel || 'einem anderen Benutzer';
|
||
if (!confirm('Diese Session an "' + targetLabel + '" zuweisen? Sie verschwindet danach aus deiner Übersicht.')) {
|
||
return;
|
||
}
|
||
data.userId = userSelect.value;
|
||
}
|
||
|
||
if (isNew) {
|
||
await apiCreateSession(data);
|
||
} else {
|
||
await apiUpdateSession(sessionId, data);
|
||
// Telegram-Config mitspeichern
|
||
try {
|
||
var tgBotId = document.getElementById('telegramBotSelect').value || '';
|
||
var tgChatId = document.getElementById('sessionTelegramChatId').value.trim();
|
||
await apiSessionTelegramSave(sessionId, tgBotId, tgChatId);
|
||
} catch(e) {
|
||
showToast('Session gespeichert, aber Telegram-Fehler: ' + e.message, 'error');
|
||
return;
|
||
}
|
||
}
|
||
navigateTo('dashboard');
|
||
});
|
||
|
||
// Abbrechen
|
||
cancelBtn.addEventListener('click', () => navigateTo('dashboard'));
|
||
|
||
// Löschen
|
||
deleteBtn.addEventListener('click', async () => {
|
||
if (!confirm('Session wirklich löschen?')) return;
|
||
await apiDeleteSession(sessionId);
|
||
navigateTo('dashboard');
|
||
});
|
||
},
|
||
destroy() {
|
||
if (_sessionContextMde) {
|
||
_sessionContextMde.toTextArea();
|
||
_sessionContextMde = null;
|
||
}
|
||
if (_sessionAvatarEditor) {
|
||
_sessionAvatarEditor.destroy();
|
||
_sessionAvatarEditor = null;
|
||
}
|
||
}
|
||
});
|
||
|
||
function openAgentPickerModal(agents, agentSkillsMap, currentId, onSelect) {
|
||
const items = agents.map(a => {
|
||
a._pickerId = a.agentId;
|
||
return a;
|
||
});
|
||
_openPickerModal('Agent auswählen', items, currentId, onSelect, {
|
||
noneLabel: 'Kein Agent',
|
||
buildCard: (a) => buildAgentCard(a, agentSkillsMap[a.agentId] || [])
|
||
});
|
||
}
|
||
|
||
function openMultiAgentPickerModal(agents, agentSkillsMap, selectedIds, onConfirm) {
|
||
const items = agents.map(a => {
|
||
a._pickerId = a.agentId;
|
||
return a;
|
||
});
|
||
_openMultiPickerModal('Panel-Agenten auswählen', items, selectedIds, onConfirm, {
|
||
buildCard: (a) => buildAgentCard(a, agentSkillsMap[a.agentId] || [])
|
||
});
|
||
}
|
||
|
||
function initSessionTelegram(sessionId, session) {
|
||
var botSelectCard = document.getElementById('telegramBotSelectCard');
|
||
var botSelect = document.getElementById('telegramBotSelect');
|
||
var chatIdInput = document.getElementById('sessionTelegramChatId');
|
||
var unlinkBtn = document.getElementById('sessionTelegramUnlink');
|
||
|
||
// Unlink-Button
|
||
unlinkBtn.addEventListener('click', function() {
|
||
if (!chatIdInput.value) return;
|
||
if (!confirm('Telegram-Verknüpfung wirklich lösen?')) return;
|
||
chatIdInput.value = '';
|
||
showToast('Chat-ID entfernt. Speichern nicht vergessen.', 'info');
|
||
});
|
||
|
||
// Config laden
|
||
apiSessionTelegramGet(sessionId).then(function(config) {
|
||
if (config.chatId) chatIdInput.value = config.chatId;
|
||
if (config.telegramBotId) botSelect.value = config.telegramBotId;
|
||
|
||
// Bot-Picker initialisieren
|
||
initTelegramBotPicker(botSelectCard, botSelect, config.telegramBotId);
|
||
}).catch(function() {
|
||
initTelegramBotPicker(botSelectCard, botSelect, null);
|
||
});
|
||
}
|
||
|
||
async function initTelegramBotPicker(cardEl, selectEl, currentBotId) {
|
||
var bots = [];
|
||
try {
|
||
var result = await apiGetAvailableTelegramBots();
|
||
if (Array.isArray(result)) bots = result;
|
||
} catch(e) { /* ignore */ }
|
||
var botMap = {};
|
||
for (var i = 0; i < bots.length; i++) botMap[bots[i].botId] = bots[i];
|
||
|
||
function renderBotInCard(bot) {
|
||
var inner = bot ? buildTelegramBotCard(bot) : null;
|
||
_renderCardPreview(cardEl, inner, 'fa-paper-plane', 'Kein Bot zugewiesen', 'tg-bot-select-change');
|
||
}
|
||
|
||
if (currentBotId && botMap[currentBotId]) {
|
||
selectEl.value = currentBotId;
|
||
renderBotInCard(botMap[currentBotId]);
|
||
} else {
|
||
renderBotInCard(null);
|
||
}
|
||
|
||
cardEl.addEventListener('click', function() {
|
||
openTelegramBotPickerModal(bots, selectEl.value, function(selectedBotId) {
|
||
selectEl.value = selectedBotId || '';
|
||
renderBotInCard(selectedBotId ? botMap[selectedBotId] : null);
|
||
});
|
||
});
|
||
}
|
||
|
||
// Session einem anderen Benutzer zuweisen (jeder Besitzer seiner eigenen Session).
|
||
async function initSessionUserPicker(rowEl, cardEl, selectEl, currentOwnerId) {
|
||
var users = [];
|
||
try {
|
||
var result = await apiGetUsers();
|
||
if (Array.isArray(result)) users = result;
|
||
} catch (e) {
|
||
// Fehler beim Laden → Zuweisung bleibt versteckt
|
||
return;
|
||
}
|
||
var userMap = {};
|
||
for (var i = 0; i < users.length; i++) userMap[users[i].userId] = users[i];
|
||
|
||
function userLabel(user) {
|
||
return user ? (user.displayName || user.username || '') : '';
|
||
}
|
||
|
||
function renderUserInCard(user) {
|
||
var inner = user ? buildUserCard(user) : null;
|
||
_renderCardPreview(cardEl, inner, 'fa-user', 'Kein Benutzer', 'agent-select-change');
|
||
selectEl.dataset.userLabel = userLabel(user);
|
||
}
|
||
|
||
selectEl.value = currentOwnerId || '';
|
||
renderUserInCard(userMap[currentOwnerId] || null);
|
||
rowEl.hidden = false;
|
||
|
||
cardEl.addEventListener('click', function() {
|
||
openUserPickerModal(users, selectEl.value, function(selectedUserId) {
|
||
// "Keine Änderung" (null) ignorieren – eine Session muss immer einen Besitzer haben.
|
||
if (!selectedUserId) return;
|
||
selectEl.value = selectedUserId;
|
||
renderUserInCard(userMap[selectedUserId] || null);
|
||
});
|
||
});
|
||
}
|
||
|
||
function openUserPickerModal(users, currentId, onSelect) {
|
||
var items = users.map(function(u) {
|
||
u._pickerId = u.userId;
|
||
return u;
|
||
});
|
||
_openPickerModal('Benutzer auswählen', items, currentId, onSelect, {
|
||
noneLabel: 'Keine Änderung',
|
||
noneIcon: 'fa-user',
|
||
buildCard: function(u) { return buildUserCard(u); }
|
||
});
|
||
}
|
||
|
||
|