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) {