Files
gott/wwwroot/js/modals.js
T
MelekiandClaude Opus 4.8 2b95161596 Read workflow-status telemetry from CLI transcript files
Dynamic ultracode workflows run as a detached background task with their
own transcript dir, so their subagent events never flow through the main
-p stream. The previous parent_tool_use_id tap therefore captured nothing
for real workflows and the status stayed "none" the whole run.

Replace the tap with a file-based reader:
- TapWorkflowLaunch parses the "Transcript dir:" line from the Workflow
  tool_result and registers the run.
- WorkflowProgressTracker becomes a per-session registry of transcript
  paths (no longer a stream accumulator).
- WorkflowTelemetryReader reads journal.jsonl for live counts (running)
  and <runId>.json for rich per-agent telemetry (completed); FinalizeChain
  persists the rich JSON into chains.workflow_info.
- Modal renders phases + a per-agent list (label/phase/state/tokens/
  tool-calls/duration) instead of the obsolete tool breakdown.

Sync ARCHITECTURE.md, ARCHITECTURE_QUICK.md and README.md to the new
mechanism.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:14:42 +02:00

622 lines
23 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 _wfDur(ms) {
ms = Number(ms || 0);
if (ms <= 0) return '–';
var s = Math.round(ms / 1000);
if (s < 60) return s + 's';
var m = Math.floor(s / 60);
return m + 'm ' + (s % 60) + 's';
}
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;
}
// Name / Zusammenfassung des Workflows (nur nach Abschluss vorhanden).
if (wf.workflowName || wf.summary) {
var title = document.createElement('div');
title.className = 'wf-title';
title.innerHTML = (wf.workflowName ? '<span class="wf-name">' + esc(wf.workflowName) + '</span>' : '') +
(wf.summary ? '<span class="wf-summary">' + esc(wf.summary) + '</span>' : '');
body.appendChild(title);
}
// Kennzahlen.
var stats = [
{ label: 'Subagenten', value: _wfNum(wf.subagentCount) },
{ label: 'Fertig', value: _wfNum(wf.completedCount) + ' / ' + _wfNum(wf.subagentCount) },
{ label: 'Tokens', value: _wfNum(wf.totalTokens) },
{ label: 'Tool-Aufrufe', value: _wfNum(wf.totalToolCalls) },
{ label: status === 'running' ? 'Läuft seit' : 'Dauer', value: _wfDur(wf.durationMs) }
];
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);
// Phasen (nur nach Abschluss).
var phases = wf.phases || [];
if (phases.length) {
var psec = document.createElement('div');
psec.className = 'wf-section';
psec.innerHTML = '<div class="wf-section-lbl">Phasen</div>';
var plist = document.createElement('div');
plist.className = 'wf-phase-list';
phases.forEach(function (p) {
var row = document.createElement('div');
row.className = 'wf-phase-row';
row.innerHTML = '<span class="wf-phase-title">' + esc(p.title || '') + '</span>' +
(p.detail ? '<span class="wf-phase-detail">' + esc(p.detail) + '</span>' : '');
plist.appendChild(row);
});
psec.appendChild(plist);
body.appendChild(psec);
}
// Subagenten-Details (nur nach Abschluss; live liegen noch keine Labels vor).
var agents = wf.agents || [];
if (agents.length) {
var asec = document.createElement('div');
asec.className = 'wf-section';
asec.innerHTML = '<div class="wf-section-lbl">Subagenten</div>';
var alist = document.createElement('div');
alist.className = 'wf-agent-list';
agents.forEach(function (a) {
var st = a.state || '';
var stCls = st === 'done' ? 'wf-ag-done' : (st === 'running' ? 'wf-ag-running' : 'wf-ag-other');
var row = document.createElement('div');
row.className = 'wf-agent-row';
row.innerHTML =
'<div class="wf-agent-main">' +
'<span class="wf-agent-state ' + stCls + '"></span>' +
'<span class="wf-agent-label">' + esc(a.label || ('Agent ' + (a.index || ''))) + '</span>' +
(a.phaseTitle ? '<span class="wf-agent-phase">' + esc(a.phaseTitle) + '</span>' : '') +
'</div>' +
'<div class="wf-agent-metrics">' +
'<span title="Tokens"><i class="fa fa-database"></i> ' + _wfNum(a.tokens) + '</span>' +
'<span title="Tool-Aufrufe"><i class="fa fa-wrench"></i> ' + _wfNum(a.toolCalls) + '</span>' +
'<span title="Dauer"><i class="fa fa-clock-o"></i> ' + _wfDur(a.durationMs) + '</span>' +
'</div>';
alist.appendChild(row);
});
asec.appendChild(alist);
body.appendChild(asec);
}
if (wf.startedAt) {
var ts = document.createElement('div');
ts.className = 'wf-meta';
ts.textContent = 'Start: ' + _wfTime(wf.startedAt);
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);
}
});