Files
gott/wwwroot/js/ui-helpers.js
T
Meleki 9abb5fd217 fix: esc() Quote-Escaping für Attribut-Kontexte + cache_control für Sub-Agents/Telegram
- esc() um "→" Escaping erweitert (Bug: Anführungszeichen in value="..."-Attributen brachen HTML)
- cache_control: ephemeral zu RunAgentChat() hinzugefügt (Preparer/Review/Finalizer hatten kein Prompt Caching)
- cache_control: ephemeral zu ChatNonStreaming() hinzugefügt (Telegram hatte kein Prompt Caching)
- Trailing-Leerzeile in commands-ui.js bereinigt
2026-03-07 22:42:20 +01:00

213 lines
6.8 KiB
JavaScript

// ── DOM Helpers ──
const $ = (sel) => document.querySelector(sel);
const chatEl = () => $('#chat');
const metaEl = () => $('#meta');
function esc(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML.replace(/"/g, '"');
}
function truncate(s, max) {
return s && s.length > max ? s.substring(0, max) + '...' : (s || '');
}
function scrollDown() {
const chat = chatEl();
if (!chat) return;
chat.scrollTop = chat.scrollHeight;
// Nach Font-Swap kann sich die Höhe ändern → nochmal scrollen
document.fonts.ready.then(() => {
chat.scrollTop = chat.scrollHeight;
});
}
function prettyModelName(model) {
if (!model) return '?';
model = model.split('claude-').join('');
if (model == 'sonnet') return 'Sonnet';
if (model == 'sonnet-4-5') return 'Sonnet 4.5';
if (model == 'sonnet-4-6') return 'Sonnet 4.6';
if (model == 'opus') return 'Opus';
if (model == 'opus-4-5') return 'Opus 4.5';
if (model == 'opus-4-6') return 'Opus 4.6';
if (model == 'haiku') return 'Haiku';
if (model == 'haiku-4-5') return 'Haiku 4.5';
return 'Unbekannt (' + model + ')';
}
// ── Toast Notifications ──
function showToast(message, type) {
type = type || 'info';
var container = document.getElementById('toastContainer');
if (!container) {
container = document.createElement('div');
container.id = 'toastContainer';
document.body.appendChild(container);
}
var toast = document.createElement('div');
toast.className = 'toast toast-' + type;
var icon = type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-times-circle' : 'fa-info-circle';
toast.innerHTML = '<i class="fa ' + icon + '"></i> ' + (message || '');
container.appendChild(toast);
// Einblenden
requestAnimationFrame(function() { toast.classList.add('toast-visible'); });
// Automatisch ausblenden
var timeout = setTimeout(function() { removeToast(toast); }, 4000);
toast.addEventListener('click', function() {
clearTimeout(timeout);
removeToast(toast);
});
}
function removeToast(toast) {
toast.classList.remove('toast-visible');
toast.addEventListener('transitionend', function() { toast.remove(); });
}
// ── EasyMDE Helper ──
function createMDE(element, options) {
options = options || {};
const mde = new EasyMDE({
element: element,
initialValue: options.initialValue || '',
toolbar: options.toolbar || ['bold', 'italic', 'heading', '|', 'unordered-list', 'ordered-list', '|', 'link', 'code', '|', 'preview'],
status: false,
spellChecker: false,
autoDownloadFontAwesome: false,
autofocus: options.autofocus || false,
minHeight: options.minHeight || '120px',
maxHeight: options.maxHeight || undefined,
placeholder: options.placeholder || '',
previewClass: ['editor-preview', 'markdown']
});
mde.codemirror.setOption('theme', 'default');
mde.codemirror.getWrapperElement().closest('.EasyMDEContainer').classList.add('fe-mde-wrap');
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 = '<div class="' + changeClass.replace('-change', '-empty') + '"><i class="fa ' + emptyIcon + '"></i> ' + emptyText + '</div>';
return;
}
while (innerCard.firstChild) cardEl.appendChild(innerCard.firstChild);
cardEl.insertAdjacentHTML('beforeend', '<span class="' + changeClass + '"><i class="fa fa-pencil"></i></span>');
}
// ── More/Expand Button ──
function initMoreButton(moreEl) {
if (!moreEl) return;
moreEl.addEventListener('click', () => {
const content = moreEl.querySelector('[hidden]');
while (content.firstChild) moreEl.parentNode.insertBefore(content.firstChild, moreEl);
moreEl.remove();
});
}
// ── 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) {
metaEl().textContent = text;
}
function setMetaHtml(html) {
metaEl().innerHTML = html;
}
// ── Older Messages Button ──
function showOlderMessagesButton(remainingCount) {
removeOlderMessagesButton();
const btn = document.createElement('button');
btn.className = 'archive-load-btn older-messages-btn';
btn.innerHTML = '<i class="fa fa-arrow-up"></i> ' + remainingCount + ' ältere Nachrichten laden';
btn.addEventListener('click', () => loadOlderMessages());
chatEl().prepend(btn);
}
function removeOlderMessagesButton() {
const btn = chatEl().querySelector('.older-messages-btn');
if (btn) btn.remove();
}
// ── Archive Button ──
function showArchiveLoadButton() {
const existing = chatEl().querySelector('.archive-load-btn:not(.older-messages-btn)');
if (existing) existing.remove();
const btn = document.createElement('button');
btn.className = 'archive-load-btn';
btn.innerHTML = '<i class="fa fa-archive"></i> Archivierte Nachrichten laden';
btn.addEventListener('click', loadLatestArchive);
// Immer ganz oben im Chat (aber nach dem older-messages-btn falls vorhanden)
const olderBtn = chatEl().querySelector('.older-messages-btn');
if (olderBtn) {
olderBtn.after(btn);
} else {
chatEl().prepend(btn);
}
}
function removeArchiveLoadButton() {
const btns = chatEl().querySelectorAll('.archive-load-btn:not(.older-messages-btn)');
btns.forEach(b => b.remove());
}
// ── Streamed Elements ──
function removeStreamedElements() {
chatEl().querySelectorAll('.streamed').forEach(el => el.remove());
}