Files
gott/wwwroot/js/modals.js
T
MelekiandClaude Opus 4.8 b6e546f618 Add workflow-status telemetry view and make panel modes on-demand
Workflow-status: a new WorkflowProgressTracker singleton taps the
suppressed dynamic-workflow subagent events (those carrying
parent_tool_use_id) into per-session telemetry without altering the
existing guard; the snapshot is serialized into the new chains.workflow_info
column on FinalizeChain (ChainId-matched so a panel member of the same
session cannot clear it) and exposed via GET /api/sessions/{id}/workflow-status
(live -> persisted -> none). The header menu gains a 'Workflow-Status' entry
with a modal that live-polls every 2s while running and self-stops on close.

Panel on-demand: Fusion/MoA is now a transient per-request multiModelMode
flag (like plan/ultracode) instead of a persisted session column. Session
settings keep only the panel members + moa_layers; the mode <select> was
removed and the composer dropdown drives the mode per request.

Docs: ARCHITECTURE.md, ARCHITECTURE_QUICK.md and README.md updated for both
features plus the already-shipped z.ai/GLM provider, GLM 5.2+ effort
passthrough, web-search-prime MCP and the ultracode toggle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:27:46 +02:00

574 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── Modal Helper ──
function _createModal(titleText, extraClasses) {
const overlay = document.createElement('div');
overlay.className = 'sp-overlay' + (extraClasses && extraClasses.overlay ? ' ' + extraClasses.overlay : '');
const modal = document.createElement('div');
modal.className = 'sp-modal' + (extraClasses && extraClasses.modal ? ' ' + extraClasses.modal : '');
const header = document.createElement('div');
header.className = 'sp-header';
const title = document.createElement('span');
title.textContent = titleText;
const closeBtn = document.createElement('button');
closeBtn.innerHTML = '<i class="fa fa-times"></i>';
closeBtn.className = 'sp-close icon-btn';
header.appendChild(title);
header.appendChild(closeBtn);
const body = document.createElement('div');
body.className = 'sp-body' + (extraClasses && extraClasses.body ? ' ' + extraClasses.body : '');
modal.appendChild(header);
modal.appendChild(body);
overlay.appendChild(modal);
document.body.appendChild(overlay);
const close = () => overlay.remove();
closeBtn.addEventListener('click', close);
overlay.addEventListener('click', (e) => {
if (e.target === overlay) close();
});
const onKey = (e) => {
if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); }
};
document.addEventListener('keydown', onKey);
return { overlay, modal, body, close };
}
// ── System-Prompt Modal ──
function _renderSystemPromptBlocks(container, blocks) {
for (var i = 0; i < blocks.length; i++) {
var block = blocks[i];
if (!block.content) continue;
var wrapper = document.createElement('div');
wrapper.className = 'sp-block';
var label = document.createElement('div');
label.className = 'sp-block-label';
label.innerHTML = '<i class="fa ' + esc(block.icon || 'fa-cog') + '"></i> ' + esc(block.label || 'Block');
wrapper.appendChild(label);
var content = document.createElement('div');
content.className = 'sp-block-content markdown';
content.innerHTML = renderMarkdown(block.content);
parseFluentEmojis(content);
wrapper.appendChild(content);
container.appendChild(wrapper);
}
}
async function showSystemPromptModal(sessionId) {
const { body } = _createModal('System-Prompt', { body: 'markdown' });
body.innerHTML = '<div class="sp-loading">Laden...</div>';
try {
const blocks = await apiGetSystemPromptBlocks(sessionId, typeof _planModeActive !== 'undefined' && _planModeActive);
body.innerHTML = '';
_renderSystemPromptBlocks(body, blocks);
} catch (e) {
body.innerHTML = '<div class="sp-loading text-danger">Fehler beim Laden des System-Prompts</div>';
}
}
// ── Payload Preview Modal ──
async function showPayloadPreviewModal(sessionId) {
const { body } = _createModal('Payload Preview', { modal: 'payload-preview-modal' });
body.innerHTML = '<div class="sp-loading">Laden...</div>';
try {
var planMode = typeof _planModeActive !== 'undefined' && _planModeActive;
var payload = await apiGetPayloadPreview(sessionId, planMode);
body.innerHTML = '';
var pre = document.createElement('pre');
pre.className = 'payload-json';
pre.textContent = JSON.stringify(payload, null, 2);
body.appendChild(pre);
} catch (e) {
body.innerHTML = '<div class="sp-loading text-danger">Fehler beim Laden des Payloads</div>';
}
}
// ── Workflow-Status Modal (Ultracode) ──
function _wfNum(n) { return Number(n || 0).toLocaleString('de-DE'); }
function _wfTime(t) { try { return new Date(t).toLocaleTimeString('de-DE'); } catch (e) { return ''; } }
function _renderWorkflowStatus(body, data) {
var status = (data && data.status) || 'none';
var wf = data && data.workflow;
var meta = {
running: { label: 'Workflow läuft', cls: 'wf-running', icon: 'fa-spinner fa-spin' },
completed: { label: 'Workflow abgeschlossen', cls: 'wf-done', icon: 'fa-check-circle' },
none: { label: 'Kein Workflow für diese Session', cls: 'wf-none', icon: 'fa-ban' }
};
var sm = meta[status] || meta.none;
body.innerHTML = '';
var head = document.createElement('div');
head.className = 'wf-status-head ' + sm.cls;
head.innerHTML = '<i class="fa ' + sm.icon + '"></i> ' + esc(sm.label);
body.appendChild(head);
if (!wf) {
var empty = document.createElement('div');
empty.className = 'wf-empty';
empty.textContent = 'Für diese Session wurde noch kein dynamischer Workflow (Ultracode) ausgeführt.';
body.appendChild(empty);
return;
}
var stats = [
{ label: 'Subagenten', value: _wfNum(wf.subagentCount) },
{ label: 'Tool-Aufrufe', value: _wfNum(wf.subagentToolCalls) },
{ label: 'Input-Tokens', value: _wfNum(wf.subagentInputTokens) },
{ label: 'Output-Tokens', value: _wfNum(wf.subagentOutputTokens) }
];
var grid = document.createElement('div');
grid.className = 'wf-stat-grid';
stats.forEach(function (s) {
var cell = document.createElement('div');
cell.className = 'wf-stat';
cell.innerHTML = '<div class="wf-stat-val">' + esc(String(s.value)) +
'</div><div class="wf-stat-lbl">' + esc(s.label) + '</div>';
grid.appendChild(cell);
});
body.appendChild(grid);
var tb = wf.toolBreakdown || {};
var keys = Object.keys(tb).sort(function (a, b) { return tb[b] - tb[a]; });
if (keys.length) {
var sec = document.createElement('div');
sec.className = 'wf-section';
sec.innerHTML = '<div class="wf-section-lbl">Tool-Aufschlüsselung</div>';
var list = document.createElement('div');
list.className = 'wf-tool-list';
keys.forEach(function (k) {
var row = document.createElement('div');
row.className = 'wf-tool-row';
row.innerHTML = '<span class="wf-tool-name">' + esc(k) +
'</span><span class="wf-tool-count">' + esc(String(tb[k])) + '</span>';
list.appendChild(row);
});
sec.appendChild(list);
body.appendChild(sec);
}
if (wf.startedAt) {
var ts = document.createElement('div');
ts.className = 'wf-meta';
ts.textContent = 'Start: ' + _wfTime(wf.startedAt) +
(wf.lastActivityAt ? ' · letzte Aktivität: ' + _wfTime(wf.lastActivityAt) : '');
body.appendChild(ts);
}
}
async function showWorkflowStatusModal(sessionId) {
const { body, overlay } = _createModal('Workflow-Status', { modal: 'workflow-status-modal' });
body.innerHTML = '<div class="sp-loading">Laden...</div>';
var poll = async function () {
try {
var data = await apiGetWorkflowStatus(sessionId);
_renderWorkflowStatus(body, data);
return data && data.status;
} catch (e) {
body.innerHTML = '<div class="sp-loading text-danger">Fehler beim Laden des Workflow-Status</div>';
return null;
}
};
var status = await poll();
// Live-Refresh, solange ein Workflow läuft – endet automatisch beim Schließen des Modals.
if (status === 'running') {
var timer = setInterval(async function () {
if (!document.body.contains(overlay)) { clearInterval(timer); return; }
var s = await poll();
if (s !== 'running') clearInterval(timer);
}, 2000);
}
}
// ── Command Output Modal ──
function showCommandOutputModal(name, command, result) {
const { body } = _createModal(name, { modal: 'cmd-output-modal' });
const exitCode = result.exitCode != null ? result.exitCode : '?';
const isError = exitCode !== 0;
const statusText = result.status === 'timeout' ? 'Timeout' : `Exit ${exitCode}`;
const meta = document.createElement('div');
meta.className = 'cmd-output-meta';
meta.innerHTML = `<code>${esc(command)}</code> <span class="cmd-exit-code${isError ? ' cmd-exit-error' : ''}">${esc(statusText)}</span>`;
body.appendChild(meta);
const output = document.createElement('pre');
output.className = 'cmd-output-content';
output.textContent = result.output || '(kein Output)';
body.appendChild(output);
}
// ── Krea Image Preview Modal ──
function showKreaImageModal(titleText, fullUrl) {
const { body } = _createModal(titleText, { overlay: 'krea-overlay', modal: 'krea-modal', body: 'krea-modal-body' });
var img = document.createElement('img');
img.src = fullUrl;
img.className = 'krea-modal-image';
img.alt = titleText;
img.addEventListener('error', function() {
body.innerHTML = '<div class="sp-loading text-danger">Bild konnte nicht geladen werden</div>';
});
body.appendChild(img);
}
// ── Picker Modal Helper ──
function _openPickerModal(title, items, currentId, onSelect, options) {
options = options || {};
const noneLabel = options.noneLabel || 'Keine Auswahl';
const noneIcon = options.noneIcon || 'fa-ban';
const overlay = document.createElement('div');
overlay.className = 'agent-picker-overlay';
const modal = document.createElement('div');
modal.className = 'agent-picker-modal';
const header = document.createElement('div');
header.className = 'agent-picker-header';
header.innerHTML = `<span>${esc(title)}</span><button class="icon-btn agent-picker-close"><i class="fa fa-times"></i></button>`;
modal.appendChild(header);
const body = document.createElement('div');
body.className = 'agent-picker-body';
// "Keine" Option
const noneCard = document.createElement('div');
noneCard.className = 'card agent-picker-item' + (!currentId ? ' selected' : '');
noneCard.innerHTML = `
<div class="card-image none-card-image"><i class="fa ${esc(noneIcon)} none-card-icon"></i></div>
<div class="card-body">
<div class="card-title-row">
<span class="card-title">${esc(noneLabel)}</span>
</div>
</div>
`;
noneCard.addEventListener('click', () => {
onSelect(null);
overlay.remove();
});
body.appendChild(noneCard);
// Items
for (const item of items) {
const cardEl = options.buildCard(item);
cardEl.classList.add('agent-picker-item');
if (item._pickerId === currentId) cardEl.classList.add('selected');
cardEl.addEventListener('click', () => {
onSelect(item._pickerId);
overlay.remove();
});
body.appendChild(cardEl);
}
modal.appendChild(body);
overlay.appendChild(modal);
document.body.appendChild(overlay);
header.querySelector('.agent-picker-close').addEventListener('click', () => overlay.remove());
overlay.addEventListener('click', (e) => {
if (e.target === overlay) overlay.remove();
});
}
// ── Multi-Select Picker Modal ──
// Wie _openPickerModal, aber Mehrfachauswahl: Karten togglen ihren selected-Zustand,
// ein "Übernehmen"-Button bestätigt. onConfirm bekommt das Array der gewählten _pickerId.
function _openMultiPickerModal(title, items, selectedIds, onConfirm, options) {
options = options || {};
var selected = new Set(selectedIds || []);
var overlay = document.createElement('div');
overlay.className = 'agent-picker-overlay';
var modal = document.createElement('div');
modal.className = 'agent-picker-modal';
var header = document.createElement('div');
header.className = 'agent-picker-header';
header.innerHTML = '<span>' + esc(title) + '</span><button class="icon-btn agent-picker-close"><i class="fa fa-times"></i></button>';
modal.appendChild(header);
var body = document.createElement('div');
body.className = 'agent-picker-body';
for (var i = 0; i < items.length; i++) {
(function(item) {
var cardEl = options.buildCard(item);
cardEl.classList.add('agent-picker-item');
if (selected.has(item._pickerId)) cardEl.classList.add('selected');
cardEl.addEventListener('click', function() {
if (selected.has(item._pickerId)) { selected.delete(item._pickerId); cardEl.classList.remove('selected'); }
else { selected.add(item._pickerId); cardEl.classList.add('selected'); }
});
body.appendChild(cardEl);
})(items[i]);
}
modal.appendChild(body);
var footer = document.createElement('div');
footer.style.cssText = 'display:flex;justify-content:flex-end;gap:8px;padding:12px;border-top:1px solid var(--f2-border)';
var confirmBtn = document.createElement('button');
confirmBtn.className = 'add-cmd-btn';
confirmBtn.textContent = 'Übernehmen';
confirmBtn.addEventListener('click', function() {
onConfirm(Array.from(selected));
overlay.remove();
});
footer.appendChild(confirmBtn);
modal.appendChild(footer);
overlay.appendChild(modal);
document.body.appendChild(overlay);
header.querySelector('.agent-picker-close').addEventListener('click', function() { overlay.remove(); });
overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.remove(); });
}
// ── Telegram Bot Picker Modal ──
function openTelegramBotPickerModal(bots, currentBotId, onSelect) {
var items = bots.map(function(b) {
b._pickerId = b.botId;
return b;
});
_openPickerModal('Telegram Bot auswählen', items, currentBotId, onSelect, {
noneLabel: 'Kein Bot',
noneIcon: 'fa-paper-plane',
buildCard: function(b) { return buildTelegramBotCard(b); }
});
}
// ── OpenClaw Import Modal ──
async function showOpenClawImportModal(sessionId) {
const { body, close } = _createModal('OpenClaw Import', { modal: 'openclaw-import-modal' });
body.innerHTML = '<div class="sp-loading">Sessions werden geladen...</div>';
try {
const sessions = await apiGetOpenclawSessions();
if (!sessions || sessions.length === 0) {
body.innerHTML = '<div class="sp-loading">Keine OpenClaw-Sessions gefunden</div>';
return;
}
body.innerHTML = '';
var hint = document.createElement('div');
hint.className = 'openclaw-hint';
hint.textContent = 'Wähle eine OpenClaw-Session zum Importieren. Vorherige Imports werden aktualisiert.';
body.appendChild(hint);
var list = document.createElement('div');
list.className = 'openclaw-session-list';
for (var i = 0; i < sessions.length; i++) {
var s = sessions[i];
var card = document.createElement('div');
card.className = 'openclaw-session-card';
// Header
var header = document.createElement('div');
header.className = 'openclaw-card-header';
var idSpan = document.createElement('span');
idSpan.className = 'openclaw-session-id';
idSpan.textContent = s.sessionId.substring(0, 8) + '…';
idSpan.title = s.sessionId;
header.appendChild(idSpan);
var meta = document.createElement('span');
meta.className = 'openclaw-card-meta';
meta.innerHTML = '<span class="openclaw-meta-item"><i class="fa fa-calendar"></i> ' + esc(formatDate(s.createdAt)) + '</span>'
+ '<span class="openclaw-meta-item"><i class="fa fa-comments"></i> ' + s.totalMessages + ' Nachrichten</span>'
+ (s.model && s.model !== 'unknown' ? '<span class="openclaw-meta-item"><i class="fa fa-microchip"></i> ' + esc(s.model) + '</span>' : '');
header.appendChild(meta);
card.appendChild(header);
// Summary
if (s.summary) {
var summary = document.createElement('div');
summary.className = 'openclaw-summary';
// Take first line of summary that's meaningful (skip empty lines and headers)
var summaryLines = s.summary.split('\n').filter(function(l) { return l.trim() && !l.startsWith('#'); });
var summaryText = summaryLines.slice(0, 2).join(' ').trim();
if (summaryText.length > 150) summaryText = summaryText.substring(0, 150) + '…';
summary.textContent = summaryText || s.summary.substring(0, 150);
card.appendChild(summary);
}
// Preview messages
if (s.previewMessages && s.previewMessages.length > 0) {
var preview = document.createElement('div');
preview.className = 'openclaw-preview';
for (var j = 0; j < s.previewMessages.length; j++) {
var pm = s.previewMessages[j];
var line = document.createElement('div');
line.className = 'openclaw-preview-msg openclaw-preview-' + pm.role;
var icon = pm.role === 'user' ? 'fa-user' : 'fa-cog';
line.innerHTML = '<i class="fa ' + icon + '"></i> <span>' + esc(pm.text) + '</span>';
preview.appendChild(line);
}
card.appendChild(preview);
}
// Click handler - capture session in closure
(function(openclawSession) {
card.addEventListener('click', async function() {
// Confirm
var confirmed = confirm('OpenClaw-Session importieren?\n\nSession: ' + openclawSession.sessionId.substring(0, 8) + '…\n' + openclawSession.totalMessages + ' Nachrichten\n\nVorhandene Imports werden ersetzt.');
if (!confirmed) return;
card.classList.add('loading-state');
try {
var result = await apiImportOpenclawSession(openclawSession.sessionId, sessionId);
close();
showToast(result.messageCount + ' Nachrichten in ' + result.chainCount + ' Chains importiert', 'success');
} catch (e) {
card.classList.remove('loading-state');
showToast('Import fehlgeschlagen: ' + e.message, 'error');
}
});
})(s);
list.appendChild(card);
}
body.appendChild(list);
} catch (e) {
body.innerHTML = '<div class="sp-loading text-danger">Fehler: ' + esc(e.message) + '</div>';
}
}
// ── Remote-Verbindungen Modal ──
async function showRemoteModal(sessionId) {
const { body, close } = _createModal('Remote-Verbindungen');
body.innerHTML = '<div class="sp-loading">Laden...</div>';
async function renderConnections() {
try {
const connections = await apiGetRemoteConnections();
body.innerHTML = '';
var empty = document.createElement('div');
empty.className = 'sp-loading';
empty.innerHTML = '<div>Windows (PowerShell)</div>' +
'<code>irm ' + esc(location.origin) + '/remote.ps1 | iex</code>' +
'<div style="margin-top:8px">Linux (Bash)</div>' +
'<code>curl -s ' + esc(location.origin) + '/remote.sh | bash</code>';
body.appendChild(empty);
if (!connections || connections.length === 0) {
return;
}
for (var i = 0; i < connections.length; i++) {
var conn = connections[i];
var card = document.createElement('div');
card.className = 'sp-block';
card.classList.add('clickable');
var isAssigned = conn.assignedSessionId === sessionId;
var isAssignedOther = conn.assignedSessionId && conn.assignedSessionId !== sessionId;
var label = document.createElement('div');
label.className = 'sp-block-label';
var statusIcon = isAssigned ? 'fa-check-circle' : (isAssignedOther ? 'fa-link' : 'fa-desktop');
var statusClass = isAssigned ? ' text-primary' : '';
label.innerHTML = '<i class="fa ' + statusIcon + statusClass + '"></i> ' +
esc(conn.username || '?') + '@' + esc(conn.hostname || '?');
card.appendChild(label);
var content = document.createElement('div');
content.className = 'sp-block-content';
content.className = 'remote-card-content';
var info = document.createElement('span');
info.className = 'remote-card-info';
var connDate = new Date(conn.connectedAt);
info.textContent = conn.os + ' • Verbunden seit ' + connDate.toLocaleTimeString('de');
content.appendChild(info);
var btn = document.createElement('button');
if (isAssigned) {
btn.textContent = 'Trennen';
btn.classList.add('text-danger');
} else if (isAssignedOther) {
btn.textContent = 'Andere Session';
btn.disabled = true;
} else {
btn.textContent = 'Verbinden';
btn.classList.add('text-primary');
}
content.appendChild(btn);
card.appendChild(content);
body.appendChild(card);
// Event-Handler
(function(c, assigned) {
btn.addEventListener('click', async function(e) {
e.stopPropagation();
btn.disabled = true;
try {
if (assigned) {
await apiUnassignRemote(c.id);
} else {
await apiAssignRemote(c.id, sessionId);
}
await renderConnections();
} catch(err) {
showToast('Fehler: ' + err.message, 'error');
btn.disabled = false;
}
});
})(conn, isAssigned);
}
} catch (e) {
body.innerHTML = '<div class="sp-loading text-danger">Fehler beim Laden: ' + esc(e.message) + '</div>';
}
}
await renderConnections();
}
// Event-Delegation für Krea-Thumbnails im Chat
document.addEventListener('click', function(e) {
var ref = e.target.closest('.krea-ref');
if (ref) {
var fullUrl = ref.dataset.fullurl;
var filename = ref.dataset.filename;
if (fullUrl) showKreaImageModal(filename || 'Bild', fullUrl);
}
});