diff --git a/wwwroot/js/chat.js b/wwwroot/js/chat.js
index 5c7b375..6f69591 100644
--- a/wwwroot/js/chat.js
+++ b/wwwroot/js/chat.js
@@ -73,16 +73,17 @@ function handleStreamEvent(eventType, data) {
addChoiceBubble(data.toolCallId, data.question, data.choices);
break;
- case 'preparer_start':
+ // Preparer / Review / Finalizer events (shared pattern)
+ case 'preparer_start': case 'review_start': case 'finalizer_start':
endStreamBubble();
- addReviewBubble('start', data, 'preparer');
+ addReviewBubble('start', data, eventType.split('_')[0] === 'review' ? undefined : eventType.split('_')[0]);
break;
- case 'preparer_text':
+ case 'preparer_text': case 'review_text': case 'finalizer_text':
appendToReviewText(data.text);
break;
- case 'preparer_tool_use':
+ case 'preparer_tool_use': case 'review_tool_use': case 'finalizer_tool_use':
addReviewToolUse(data);
break;
@@ -90,19 +91,6 @@ function handleStreamEvent(eventType, data) {
finalizeReviewBubble(data.forwarded ? 'forwarded' : 'query', data, 'preparer');
break;
- case 'review_start':
- endStreamBubble();
- addReviewBubble('start', data);
- break;
-
- case 'review_text':
- appendToReviewText(data.text);
- break;
-
- case 'review_tool_use':
- addReviewToolUse(data);
- break;
-
case 'review_feedback':
finalizeReviewBubble('feedback', data);
break;
@@ -111,19 +99,6 @@ function handleStreamEvent(eventType, data) {
finalizeReviewBubble('approved', data);
break;
- case 'finalizer_start':
- endStreamBubble();
- addReviewBubble('start', data, 'finalizer');
- break;
-
- case 'finalizer_text':
- appendToReviewText(data.text);
- break;
-
- case 'finalizer_tool_use':
- addReviewToolUse(data);
- break;
-
case 'finalizer_done':
finalizeReviewBubble('done', data, 'finalizer');
break;
diff --git a/wwwroot/js/commands-ui.js b/wwwroot/js/commands-ui.js
index 285c893..eb85f8c 100644
--- a/wwwroot/js/commands-ui.js
+++ b/wwwroot/js/commands-ui.js
@@ -14,8 +14,8 @@ function createCommandRow(container, name, command, type) {
-
-
+
+
`;
@@ -53,6 +53,4 @@ function loadCommandsIntoUI(container, customCommands) {
} catch (e) { /* ignore parse errors */ }
}
-function _escCmdAttr(str) {
- return str.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>');
-}
+
diff --git a/wwwroot/js/file-explorer.js b/wwwroot/js/file-explorer.js
index 6cd656f..3a8612a 100644
--- a/wwwroot/js/file-explorer.js
+++ b/wwwroot/js/file-explorer.js
@@ -327,26 +327,13 @@ function feInitEasyMDE(container, data) {
const textarea = document.getElementById('feMdeTextarea');
_feState._originalContent = data.content;
- const mde = new EasyMDE({
- element: textarea,
+ const mde = createMDE(textarea, {
initialValue: data.content,
- toolbar: [
- 'bold', 'italic', 'heading', '|',
- 'quote', 'unordered-list', 'ordered-list', '|',
- 'link', 'image', 'code', '|',
- 'preview', 'side-by-side', '|',
- 'guide'
- ],
- status: false,
- autofocus: false,
- spellChecker: false,
- autoDownloadFontAwesome: false,
- previewClass: ['editor-preview', 'markdown'],
+ toolbar: ['bold', 'italic', 'heading', '|', 'quote', 'unordered-list', 'ordered-list', '|', 'link', 'image', 'code', '|', 'preview', 'side-by-side', '|', 'guide'],
minHeight: '100%',
maxHeight: '100%'
});
- mde.codemirror.setOption('theme', 'default');
mde.codemirror.setOption('extraKeys', {
'Ctrl-S': () => feDoSave(),
'Cmd-S': () => feDoSave()
@@ -357,7 +344,6 @@ function feInitEasyMDE(container, data) {
feUpdateSelectionInfo(mde.codemirror, data);
});
- // Änderungs-Tracking
mde.codemirror.on('change', () => {
const saveBtn = document.getElementById('feSaveBtn');
if (saveBtn) saveBtn.disabled = mde.value() === _feState._originalContent;
diff --git a/wwwroot/js/pages/admin-page.js b/wwwroot/js/pages/admin-page.js
index cdc11b3..3f3c8b3 100644
--- a/wwwroot/js/pages/admin-page.js
+++ b/wwwroot/js/pages/admin-page.js
@@ -205,12 +205,9 @@ registerPage('admin', {
});
// Speichern
- saveBtn.addEventListener('click', async function() {
- saveBtn.disabled = true;
-
+ saveBtn.addEventListener('click', () => withButtonLoading(saveBtn, saveBtn.textContent, async () => {
try {
if (editingUserId) {
- // Update
var data = {
username: usernameInput.value.trim(),
displayName: displayNameInput.value.trim(),
@@ -219,15 +216,11 @@ registerPage('admin', {
};
await apiAdminUpdateUser(editingUserId, data);
- // Passwort nur wenn ausgefüllt
var pw = passwordInput.value;
- if (pw) {
- await apiAdminResetPassword(editingUserId, pw);
- }
+ if (pw) await apiAdminResetPassword(editingUserId, pw);
showToast('Benutzer aktualisiert');
} else {
- // Create
var username = usernameInput.value.trim();
var password = passwordInput.value;
@@ -251,10 +244,8 @@ registerPage('admin', {
await loadUsers();
} catch (err) {
showToast(err.message, 'error');
- } finally {
- saveBtn.disabled = false;
}
- });
+ }));
// ── Init: User laden ──
await loadUsers();
diff --git a/wwwroot/js/pages/cronjob-page.js b/wwwroot/js/pages/cronjob-page.js
index ec3e76b..bbfbcc2 100644
--- a/wwwroot/js/pages/cronjob-page.js
+++ b/wwwroot/js/pages/cronjob-page.js
@@ -27,26 +27,16 @@ registerPage('cronjob', {
for (const s of sessions) sessionMap[s.sessionId] = s;
// Session-Card rendern
- function renderSessionSelectCard(cardEl, session, emptyHtml) {
- cardEl.innerHTML = '';
- if (!session) {
- cardEl.innerHTML = emptyHtml;
- return;
- }
- const agent = session.agentId ? agentMap[session.agentId] : null;
- const inner = buildSessionCard(session, agent);
- while (inner.firstChild) cardEl.appendChild(inner.firstChild);
- cardEl.insertAdjacentHTML('beforeend', '');
+ function renderSessionSelectCard(cardEl, session, emptyIcon, emptyText) {
+ const inner = session ? buildSessionCard(session, session.agentId ? agentMap[session.agentId] : null) : null;
+ _renderCardPreview(cardEl, inner, emptyIcon, emptyText, 'session-select-change');
}
- const sessionEmptyHtml = '
Keine Session ausgewählt
';
- const notifyEmptyHtml = ' Keine (Antwort bleibt in Arbeits-Session)
';
-
// Session-Card click → Picker (nur Sessions mit Agent)
sessionCard.addEventListener('click', () => {
openSessionPickerModal(sessions, agentMap, sessionSelect.value, (selectedId) => {
sessionSelect.value = selectedId || '';
- renderSessionSelectCard(sessionCard, selectedId ? sessionMap[selectedId] : null, sessionEmptyHtml);
+ renderSessionSelectCard(sessionCard, selectedId ? sessionMap[selectedId] : null, 'fa-comment-o', 'Keine Session ausgewählt');
}, { filterWithAgent: true, title: 'Session auswählen' });
});
@@ -54,7 +44,7 @@ registerPage('cronjob', {
notifyCard.addEventListener('click', () => {
openSessionPickerModal(sessions, agentMap, notifySelect.value, (selectedId) => {
notifySelect.value = selectedId || '';
- renderSessionSelectCard(notifyCard, selectedId ? sessionMap[selectedId] : null, notifyEmptyHtml);
+ renderSessionSelectCard(notifyCard, selectedId ? sessionMap[selectedId] : null, 'fa-bell-o', 'Keine (Antwort bleibt in Arbeits-Session)');
}, { filterWithAgent: false, title: 'Benachrichtigungs-Session auswählen', noneLabel: 'Keine Benachrichtigung', noneIcon: 'fa-bell-slash' });
});
@@ -75,11 +65,11 @@ registerPage('cronjob', {
nameInput.value = job.name || '';
sessionSelect.value = job.sessionId || '';
- renderSessionSelectCard(sessionCard, job.sessionId ? sessionMap[job.sessionId] : null, sessionEmptyHtml);
+ renderSessionSelectCard(sessionCard, job.sessionId ? sessionMap[job.sessionId] : null, 'fa-comment-o', 'Keine Session ausgewählt');
cronInput.value = job.cronExpression || '';
promptInput.value = job.prompt || '';
notifySelect.value = job.notifySessionId || '';
- renderSessionSelectCard(notifyCard, job.notifySessionId ? sessionMap[job.notifySessionId] : null, notifyEmptyHtml);
+ renderSessionSelectCard(notifyCard, job.notifySessionId ? sessionMap[job.notifySessionId] : null, 'fa-bell-o', 'Keine (Antwort bleibt in Arbeits-Session)');
enabledCheckbox.checked = job.isEnabled;
updateCronPreview(cronInput, cronPreview);
@@ -94,9 +84,7 @@ registerPage('cronjob', {
});
// Jetzt ausführen
- runBtn.addEventListener('click', async () => {
- runBtn.disabled = true;
- runBtn.innerHTML = ' Läuft...';
+ runBtn.addEventListener('click', () => withButtonLoading(runBtn, 'Läuft...', async () => {
try {
const result = await apiRunCronjob(params.id);
if (result.status === 'success') {
@@ -104,16 +92,12 @@ registerPage('cronjob', {
} else {
showToast('Fehler: ' + (result.error || 'Unbekannt'), 'error');
}
- // Status neu laden
const updated = await apiGetCronjob(params.id);
if (updated) showCronjobStatus(statusBox, updated);
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
- } finally {
- runBtn.disabled = false;
- runBtn.innerHTML = ' Jetzt ausführen';
}
- });
+ }));
}
// Speichern
@@ -132,23 +116,22 @@ registerPage('cronjob', {
return;
}
- saveBtn.disabled = true;
- try {
- if (isEdit) {
- await apiUpdateCronjob(params.id, data);
- showToast('Cronjob gespeichert');
- const updated = await apiGetCronjob(params.id);
- if (updated) showCronjobStatus(statusBox, updated);
- } else {
- const created = await apiCreateCronjob(data);
- showToast('Cronjob erstellt');
- navigateTo('cronjob', { id: created.cronjobId });
+ await withButtonLoading(saveBtn, 'Speichern...', async () => {
+ try {
+ if (isEdit) {
+ await apiUpdateCronjob(params.id, data);
+ showToast('Cronjob gespeichert');
+ const updated = await apiGetCronjob(params.id);
+ if (updated) showCronjobStatus(statusBox, updated);
+ } else {
+ const created = await apiCreateCronjob(data);
+ showToast('Cronjob erstellt');
+ navigateTo('cronjob', { id: created.cronjobId });
+ }
+ } catch (e) {
+ showToast('Fehler: ' + e.message, 'error');
}
- } catch (e) {
- showToast('Fehler: ' + e.message, 'error');
- } finally {
- saveBtn.disabled = false;
- }
+ });
});
}
});
diff --git a/wwwroot/js/pages/dashboard.js b/wwwroot/js/pages/dashboard.js
index 9ce3210..03a5458 100644
--- a/wwwroot/js/pages/dashboard.js
+++ b/wwwroot/js/pages/dashboard.js
@@ -340,10 +340,4 @@ function _openCardMenu(trigger, dropdown) {
if (finalRect.left < 0) dropdown.style.left = '8px';
}
-function formatDate(iso) {
- if (!iso) return '';
- return new Date(iso).toLocaleString('de-AT', {
- day: '2-digit', month: '2-digit', year: '2-digit',
- hour: '2-digit', minute: '2-digit'
- });
-}
+
diff --git a/wwwroot/js/pages/profile-page.js b/wwwroot/js/pages/profile-page.js
index 5d2689b..0181b36 100644
--- a/wwwroot/js/pages/profile-page.js
+++ b/wwwroot/js/pages/profile-page.js
@@ -33,29 +33,18 @@ registerPage('profile', {
defaultPathInput.value = profile.defaultPath || '';
// Profil speichern
- saveBtn.addEventListener('click', async function() {
- saveBtn.disabled = true;
- saveBtn.textContent = 'Speichern...';
-
+ saveBtn.addEventListener('click', () => withButtonLoading(saveBtn, 'Speichern...', async () => {
try {
var updated = await apiUpdateProfile({
displayName: displayNameInput.value.trim(),
persona: _profileMde.value().trim()
});
-
- // currentUser aktualisieren
- if (currentUser) {
- currentUser.displayName = updated.displayName;
- }
-
+ if (currentUser) currentUser.displayName = updated.displayName;
showToast('Profil gespeichert');
} catch (err) {
showToast(err.message, 'error');
- } finally {
- saveBtn.disabled = false;
- saveBtn.textContent = 'Speichern';
}
- });
+ }));
// Passwort ändern
passwordBtn.addEventListener('click', async function() {
@@ -73,23 +62,17 @@ registerPage('profile', {
return;
}
- passwordBtn.disabled = true;
- passwordBtn.textContent = 'Ändern...';
-
- try {
- await apiChangePassword(currentPw, newPw);
-
- currentPwInput.value = '';
- newPwInput.value = '';
- confirmPwInput.value = '';
-
- showToast('Passwort geändert');
- } catch (err) {
- showToast(err.message, 'error');
- } finally {
- passwordBtn.disabled = false;
- passwordBtn.textContent = 'Passwort ändern';
- }
+ await withButtonLoading(passwordBtn, 'Ändern...', async () => {
+ try {
+ await apiChangePassword(currentPw, newPw);
+ currentPwInput.value = '';
+ newPwInput.value = '';
+ confirmPwInput.value = '';
+ showToast('Passwort geändert');
+ } catch (err) {
+ showToast(err.message, 'error');
+ }
+ });
});
// Logout
diff --git a/wwwroot/js/pages/server-settings-page.js b/wwwroot/js/pages/server-settings-page.js
index 39d27cf..7fef533 100644
--- a/wwwroot/js/pages/server-settings-page.js
+++ b/wwwroot/js/pages/server-settings-page.js
@@ -99,9 +99,7 @@ registerPage('server-settings', {
}
// Speichern
- saveBtn.addEventListener('click', async function() {
- saveBtn.disabled = true;
-
+ saveBtn.addEventListener('click', () => withButtonLoading(saveBtn, 'Speichern...', async () => {
try {
var data = {};
for (var j = 0; j < settings.length; j++) {
@@ -122,10 +120,8 @@ registerPage('server-settings', {
showToast(result.hint || 'Einstellungen gespeichert');
} catch (err) {
showToast(err.message, 'error');
- } finally {
- saveBtn.disabled = false;
}
- });
+ }));
} catch (err) {
container.innerHTML = 'Fehler beim Laden: ' + esc(err.message) + '
';
diff --git a/wwwroot/js/pages/session-page.js b/wwwroot/js/pages/session-page.js
index ae263a6..315d0cc 100644
--- a/wwwroot/js/pages/session-page.js
+++ b/wwwroot/js/pages/session-page.js
@@ -85,15 +85,8 @@ registerPage('session', {
}
function renderAgentInCard(cardEl, agent, emptyIcon, emptyText) {
- cardEl.innerHTML = '';
- if (!agent) {
- cardEl.innerHTML = ' ' + emptyText + '
';
- return;
- }
- const skills = agentSkillsMap[agent.agentId] || [];
- const inner = buildAgentCard(agent, skills);
- while (inner.firstChild) cardEl.appendChild(inner.firstChild);
- cardEl.insertAdjacentHTML('beforeend', '');
+ const inner = agent ? buildAgentCard(agent, agentSkillsMap[agent.agentId] || []) : null;
+ _renderCardPreview(cardEl, inner, emptyIcon, emptyText, 'agent-select-change');
}
function updateReviewVisibility() {
@@ -264,45 +257,38 @@ function initSessionTelegram(sessionId) {
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');
+ await withButtonLoading(registerBtn, 'Wird registriert...', async () => {
+ try {
+ 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');
}
- } catch(e) {
- showToast('Fehler: ' + e.message, 'error');
- } finally {
- registerBtn.disabled = false;
- registerBtn.textContent = 'Webhook registrieren';
- refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
- }
+ });
+ 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');
+ await withButtonLoading(deleteBtn, 'Wird entfernt...', async () => {
+ 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');
}
- } catch(e) {
- showToast('Fehler: ' + e.message, 'error');
- } finally {
- deleteBtn.disabled = false;
- deleteBtn.textContent = 'Webhook entfernen';
- refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
- }
+ });
+ refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
});
}
diff --git a/wwwroot/js/pages/settings-page.js b/wwwroot/js/pages/settings-page.js
index 770e7c6..c0a459f 100644
--- a/wwwroot/js/pages/settings-page.js
+++ b/wwwroot/js/pages/settings-page.js
@@ -51,18 +51,13 @@ registerPage('settings', {
});
const backupBtn = document.getElementById('settingsBackupBtn');
- if (backupBtn) backupBtn.addEventListener('click', async () => {
- backupBtn.disabled = true;
- backupBtn.textContent = 'Wird erstellt...';
+ if (backupBtn) backupBtn.addEventListener('click', () => withButtonLoading(backupBtn, 'Wird erstellt...', async () => {
try {
await apiBackupDb();
} catch (e) {
console.error('Backup fehlgeschlagen:', e);
- } finally {
- backupBtn.textContent = 'Jetzt sichern';
- backupBtn.disabled = false;
}
- });
+ }));
resetBtn.addEventListener('click', async () => {
if (!confirm('Alle Einstellungen auf Standard zurücksetzen?')) return;
diff --git a/wwwroot/js/ui-helpers.js b/wwwroot/js/ui-helpers.js
index e2eeb76..f8c3638 100644
--- a/wwwroot/js/ui-helpers.js
+++ b/wwwroot/js/ui-helpers.js
@@ -81,6 +81,7 @@ function createMDE(element, options) {
autoDownloadFontAwesome: false,
autofocus: options.autofocus || false,
minHeight: options.minHeight || '120px',
+ maxHeight: options.maxHeight || undefined,
placeholder: options.placeholder || '',
previewClass: ['editor-preview', 'markdown']
});
@@ -89,6 +90,27 @@ function createMDE(element, options) {
return mde;
}
+// ── Card Preview (for Picker-Select-Cards) ──
+
+/**
+ * Renders a card preview into a select-card element.
+ * Used by agent picker (session-page) and session picker (cronjob-page).
+ * @param {HTMLElement} cardEl - The container element
+ * @param {HTMLElement|null} innerCard - Result of buildAgentCard/buildSessionCard, or null for empty state
+ * @param {string} emptyIcon - FA icon class for empty state
+ * @param {string} emptyText - Text for empty state
+ * @param {string} changeClass - CSS class for the change indicator (e.g. 'agent-select-change', 'session-select-change')
+ */
+function _renderCardPreview(cardEl, innerCard, emptyIcon, emptyText, changeClass) {
+ cardEl.innerHTML = '';
+ if (!innerCard) {
+ cardEl.innerHTML = ' ' + emptyText + '
';
+ return;
+ }
+ while (innerCard.firstChild) cardEl.appendChild(innerCard.firstChild);
+ cardEl.insertAdjacentHTML('beforeend', '');
+}
+
// ── More/Expand Button ──
function initMoreButton(moreEl) {
@@ -100,6 +122,38 @@ function initMoreButton(moreEl) {
});
}
+// ── Button Loading Helper ──
+
+/**
+ * Disables a button, sets loading text, runs async function, then restores.
+ * @param {HTMLElement} btn - The button element
+ * @param {string} loadingText - Text while loading (e.g. 'Speichern...')
+ * @param {Function} asyncFn - Async function to run
+ * @returns {Promise<*>} Result of asyncFn
+ */
+async function withButtonLoading(btn, loadingText, asyncFn) {
+ const originalText = btn.textContent;
+ const originalHtml = btn.innerHTML;
+ btn.disabled = true;
+ btn.textContent = loadingText;
+ try {
+ return await asyncFn();
+ } finally {
+ btn.disabled = false;
+ btn.innerHTML = originalHtml;
+ }
+}
+
+// ── Date Formatting ──
+
+function formatDate(iso) {
+ if (!iso) return '';
+ return new Date(iso).toLocaleString('de-AT', {
+ day: '2-digit', month: '2-digit', year: '2-digit',
+ hour: '2-digit', minute: '2-digit'
+ });
+}
+
// ── Meta Bar ──
function setMeta(text) {