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
444 lines
17 KiB
JavaScript
444 lines
17 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 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);
|
||
}
|
||
|
||
|