Files
gott/wwwroot/js/pages/session-page.js
T
Meleki 4bec740765 feat: Finalizer-Agent – dritter Agent der nach Worker + Review einschreitet
Backend:
- SessionRecord + DB-Migration: finalizer_agent_id auf sessions
- ChatContext.FinalizerContext (Model, SystemPrompt, Tools, Workspace)
- ChatService: FinalizerContext aufbauen (analog zu ReviewContext)
- AnthropicService: RunFinalizer() nach Review-Phase (oder direkt nach Worker)
- RunReviewAgentChat() refactored zu generischem RunAgentChat()
  mit agentType-Parameter ('review' | 'finalizer') für SSE-Events und DB-Tags
- SSE-Events: finalizer_start, finalizer_text, finalizer_tool_use, finalizer_done

Frontend:
- Session-Page: Finalizer-Agent-Picker (im 'Weitere Einstellungen' Bereich)
- Chat: SSE-Events für Finalizer-Phase verarbeiten
- UI: addReviewBubble/finalizeReviewBubble unterstützen agentType 'finalizer'
  mit eigener Farbe (success-green) und Flag-Checkered-Icon
- renderMarkdown: [Finalizer: AgentName] Tags parsen
- History: Finalizer-Messages als .finalizer-bubble rendern
- Shared-Page: Finalizer-Messages korrekt erkennen und stylen
- Dashboard: Finalizer-Badge (flag-checkered) auf Session-Cards
2026-03-06 23:35:30 +01:00

444 lines
17 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.
// ── 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 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 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 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);
} else {
titleEl.textContent = 'Neue Session';
deleteBtn.hidden = true;
}
// EasyMDE für Kontext
_sessionContextMde = new EasyMDE({
element: contextEl,
initialValue: (session && session.context) || '',
toolbar: ['bold', 'italic', 'heading', '|', 'unordered-list', 'ordered-list', '|', 'link', 'code', '|', 'preview'],
status: false,
spellChecker: false,
autoDownloadFontAwesome: false,
minHeight: '120px',
placeholder: 'Zusätzlicher Kontext für den System-Prompt (optional)',
previewClass: ['editor-preview', 'markdown']
});
_sessionContextMde.codemirror.setOption('theme', 'default');
_sessionContextMde.codemirror.getWrapperElement().closest('.EasyMDEContainer').classList.add('fe-mde-wrap');
// Befehl hinzufügen
addCommandBtn.addEventListener('click', () => createCommandRow(commandsList));
// Telegram-Section nur für existierende Sessions
if (!isNew) {
initSessionTelegram(sessionId);
}
// Agents laden + Skills parallel
const agents = await apiGetAgents();
const agentMap = {};
for (const a of agents) agentMap[a.agentId] = a;
const agentSkillsMap = {};
await Promise.all(agents.map(async a => {
agentSkillsMap[a.agentId] = await apiGetAgentSkills(a.agentId);
}));
function renderAgentInCard(cardEl, agent, emptyIcon, emptyText) {
cardEl.innerHTML = '';
if (!agent) {
cardEl.innerHTML = '<div class="agent-select-empty"><i class="fa ' + emptyIcon + '"></i> ' + emptyText + '</div>';
return;
}
const skills = agentSkillsMap[agent.agentId] || [];
const inner = buildAgentCard(agent, skills);
while (inner.firstChild) cardEl.appendChild(inner.firstChild);
cardEl.insertAdjacentHTML('beforeend', '<span class="agent-select-change"><i class="fa fa-pencil"></i></span>');
}
function updateReviewVisibility() {
reviewIterationsRow.hidden = !reviewAgentSelect.value;
}
// Initiale Anzeige – Agent
if (session && session.agentId) {
agentSelect.value = session.agentId;
renderAgentInCard(agentSelectCard, agentMap[session.agentId] || null, 'fa-user-circle-o', 'Kein Agent ausgewählt');
} else {
renderAgentInCard(agentSelectCard, null, 'fa-user-circle-o', 'Kein Agent ausgewählt');
}
// Initiale Anzeige – Review-Agent
if (session && session.reviewAgentId) {
reviewAgentSelect.value = session.reviewAgentId;
renderAgentInCard(reviewAgentSelectCard, agentMap[session.reviewAgentId] || null, 'fa-check-circle-o', 'Kein Review-Agent');
maxReviewIterationsEl.value = session.maxReviewIterations || 3;
} else {
renderAgentInCard(reviewAgentSelectCard, null, 'fa-check-circle-o', 'Kein Review-Agent');
}
updateReviewVisibility();
// Initiale Anzeige – Finalizer-Agent
if (session && session.finalizerAgentId) {
finalizerAgentSelect.value = session.finalizerAgentId;
renderAgentInCard(finalizerAgentSelectCard, agentMap[session.finalizerAgentId] || null, 'fa-flag-checkered', 'Kein Finalizer-Agent');
} else {
renderAgentInCard(finalizerAgentSelectCard, null, 'fa-flag-checkered', 'Kein Finalizer-Agent');
}
// Klick auf Agent-Card → Modal öffnen
agentSelectCard.addEventListener('click', () => {
openAgentPickerModal(agents, agentSkillsMap, agentSelect.value, (selectedId) => {
agentSelect.value = selectedId || '';
renderAgentInCard(agentSelectCard, selectedId ? agentMap[selectedId] : null, 'fa-user-circle-o', 'Kein Agent ausgewählt');
});
});
// Klick auf Review-Agent-Card → Modal öffnen
reviewAgentSelectCard.addEventListener('click', () => {
openAgentPickerModal(agents, agentSkillsMap, reviewAgentSelect.value, (selectedId) => {
reviewAgentSelect.value = selectedId || '';
renderAgentInCard(reviewAgentSelectCard, selectedId ? agentMap[selectedId] : null, 'fa-check-circle-o', 'Kein Review-Agent');
updateReviewVisibility();
});
});
// Klick auf Finalizer-Agent-Card → Modal öffnen
finalizerAgentSelectCard.addEventListener('click', () => {
openAgentPickerModal(agents, agentSkillsMap, finalizerAgentSelect.value, (selectedId) => {
finalizerAgentSelect.value = selectedId || '';
renderAgentInCard(finalizerAgentSelectCard, selectedId ? agentMap[selectedId] : null, 'fa-flag-checkered', 'Kein Finalizer-Agent');
});
});
more.addEventListener('click', () => {
const content = more.querySelector('[hidden]');
while (content.firstChild)
more.parentNode.insertBefore(content.firstChild, more);
more.remove();
});
// 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,
context: _sessionContextMde.value().trim() || null,
workspace: workspaceEl.value.trim() || null,
customCommands: commands,
avatarScale: _sessionAvatarEditor.getScale(),
avatarOffsetX: _sessionAvatarEditor.getOffsetX(),
avatarOffsetY: _sessionAvatarEditor.getOffsetY()
};
if (isNew) {
await apiCreateSession(data);
} else {
await apiUpdateSession(sessionId, data);
// Telegram-Config mitspeichern (falls Section sichtbar)
var tgToken = document.getElementById('sessionTelegramToken');
var tgSecret = document.getElementById('sessionTelegramSecret');
if (tgToken) {
try {
await saveTelegramConfig(sessionId, tgToken, tgSecret);
} 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) {
// Overlay
const overlay = document.createElement('div');
overlay.className = 'agent-picker-overlay';
const modal = document.createElement('div');
modal.className = 'agent-picker-modal';
// Header
const header = document.createElement('div');
header.className = 'agent-picker-header';
header.innerHTML = '<span>Agent auswählen</span><button class="icon-btn agent-picker-close"><i class="fa fa-times"></i></button>';
modal.appendChild(header);
// Body
const body = document.createElement('div');
body.className = 'agent-picker-body';
// "Kein Agent" Option
const noneCard = document.createElement('div');
noneCard.className = 'card agent-picker-item' + (!currentId ? ' selected' : '');
noneCard.innerHTML = `
<div class="card-image" style="display:flex;align-items:center;justify-content:center"><i class="fa fa-ban" style="font-size:2em;opacity:var(--semi-transparent)"></i></div>
<div class="card-body">
<div class="card-title-row">
<span class="card-title">Kein Agent</span>
</div>
</div>
`;
noneCard.addEventListener('click', () => {
onSelect(null);
overlay.remove();
});
body.appendChild(noneCard);
// Agent Cards via Helper
for (const a of agents) {
const skills = agentSkillsMap[a.agentId] || [];
const item = buildAgentCard(a, skills);
item.classList.add('agent-picker-item');
if (a.agentId === currentId) item.classList.add('selected');
item.addEventListener('click', () => {
onSelect(a.agentId);
overlay.remove();
});
body.appendChild(item);
}
modal.appendChild(body);
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Schließen
header.querySelector('.agent-picker-close').addEventListener('click', () => overlay.remove());
overlay.addEventListener('click', (e) => {
if (e.target === overlay) overlay.remove();
});
}
function initSessionTelegram(sessionId) {
var tokenInput = document.getElementById('sessionTelegramToken');
var secretInput = document.getElementById('sessionTelegramSecret');
var allowedInput = document.getElementById('sessionTelegramAllowed');
var registerBtn = document.getElementById('sessionTelegramRegisterBtn');
var deleteBtn = document.getElementById('sessionTelegramDeleteBtn');
var statusBox = document.getElementById('sessionTelegramStatusBox');
// Vorhandene Config laden + Status abrufen
apiSessionTelegramGet(sessionId).then(async function(config) {
if (config.botToken) tokenInput.value = config.botToken;
if (config.allowedChatIds) allowedInput.value = config.allowedChatIds;
if (config.webhookSecret) {
secretInput.value = config.webhookSecret;
} else {
// Secret einmalig generieren und sofort persistent speichern
var newSecret = crypto.randomUUID().replace(/-/g, '');
secretInput.value = newSecret;
await apiSessionTelegramSave(sessionId, config.botToken || '', newSecret);
}
// Status automatisch abrufen wenn Token konfiguriert ist
if (config.botToken) {
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
} else {
showTelegramUnconfigured(statusBox, registerBtn, deleteBtn);
}
}).catch(function() {
showTelegramUnconfigured(statusBox, registerBtn, deleteBtn);
});
// Webhook registrieren
registerBtn.addEventListener('click', async function() {
var baseUrl = prompt('Base-URL der App:', window.location.origin);
if (!baseUrl) return;
registerBtn.disabled = true;
registerBtn.textContent = 'Wird registriert...';
try {
// Erst Token + Secret speichern (falls geändert)
await saveTelegramConfig(sessionId, tokenInput, secretInput);
var res = await apiSessionTelegramRegisterWebhook(sessionId, baseUrl.trim());
if (res.ok) {
showToast('Webhook registriert!', 'success');
} else {
showToast('Antwort: ' + JSON.stringify(res), 'error');
}
} catch(e) {
showToast('Fehler: ' + e.message, 'error');
} finally {
registerBtn.disabled = false;
registerBtn.textContent = 'Webhook registrieren';
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
}
});
// Webhook entfernen
deleteBtn.addEventListener('click', async function() {
if (!confirm('Webhook wirklich entfernen?')) return;
deleteBtn.disabled = true;
deleteBtn.textContent = 'Wird entfernt...';
try {
var res = await apiSessionTelegramDeleteWebhook(sessionId);
if (res.ok) {
showToast('Webhook entfernt', 'success');
} else {
showToast('Antwort: ' + JSON.stringify(res), 'error');
}
} catch(e) {
showToast('Fehler: ' + e.message, 'error');
} finally {
deleteBtn.disabled = false;
deleteBtn.textContent = 'Webhook entfernen';
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
}
});
}
function showTelegramUnconfigured(statusBox, registerBtn, deleteBtn) {
statusBox.innerHTML = '<p>⚠️ Kein Bot-Token konfiguriert. Speichere zuerst die Session mit einem Token.</p>';
statusBox.hidden = false;
registerBtn.hidden = true;
deleteBtn.hidden = true;
}
async function refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn) {
statusBox.hidden = true;
try {
var info = await apiSessionTelegramWebhookInfo(sessionId);
var result = info.result || {};
var url = result.url || '';
var pending = result.pending_update_count || 0;
var lastError = result.last_error_message;
var active = !!url;
var html = '';
if (active) {
html += '<p>✅ <strong>Webhook aktiv</strong></p>';
html += '<p><strong>URL:</strong> ' + esc(url) + '</p>';
html += '<p><strong>Ausstehend:</strong> ' + pending + '</p>';
if (lastError) html += '<p class="tg-error"><strong>Letzter Fehler:</strong> ' + esc(lastError) + '</p>';
} else {
html += '<p>⚠️ <strong>Webhook nicht registriert</strong></p>';
}
statusBox.innerHTML = html;
statusBox.hidden = false;
// Buttons ein/ausblenden je nach Status
registerBtn.hidden = active;
deleteBtn.hidden = !active;
} catch(e) {
statusBox.innerHTML = '<p class="tg-error">Status konnte nicht abgerufen werden: ' + esc(e.message) + '</p>';
statusBox.hidden = false;
registerBtn.hidden = false;
deleteBtn.hidden = true;
}
}
async function saveTelegramConfig(sessionId, tokenInput, secretInput) {
var token = tokenInput.value.trim();
var secret = secretInput.value.trim();
var allowedInput = document.getElementById('sessionTelegramAllowed');
var allowed = allowedInput ? allowedInput.value.trim() : '';
await apiSessionTelegramSave(sessionId, token, secret, allowed);
}