Files
gott/wwwroot/js/ui-helpers.js
T
Meleki 7352438b11 feat: Scanned-Keys 'Alle prüfen' Button + EasyMDE Refresh nach More-Toggle
- POST /api/admin/api-keys/scanned/recheck-all: prüft alle nicht-adoptierten Scanned-Keys sequenziell
- Button mit Loading-State, Ergebnis-Toast und Auto-Reload
- EasyMDE/CodeMirror refresh() nach initMoreButton() Einblenden (behebt leeren Editor-Bug)
2026-03-13 15:58:31 +01:00

283 lines
9.3 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 || '');
}
// Auto-Scroll: wird deaktiviert wenn User nach oben scrollt, reaktiviert wenn er ganz unten ist
var _autoScroll = true;
function initAutoScroll() {
const chat = chatEl();
if (!chat) return;
_autoScroll = true;
chat.addEventListener('scroll', function() {
const threshold = 60;
const atBottom = chat.scrollHeight - chat.scrollTop - chat.clientHeight <= threshold;
_autoScroll = atBottom;
});
}
function scrollDown() {
const chat = chatEl();
if (!chat) return;
if (!_autoScroll) return;
chat.scrollTop = chat.scrollHeight;
// Nach Font-Swap kann sich die Höhe ändern → nochmal scrollen
document.fonts.ready.then(() => {
if (_autoScroll) chat.scrollTop = chat.scrollHeight;
});
}
function scrollDownForce() {
const chat = chatEl();
if (!chat) return;
_autoScroll = true;
chat.scrollTop = chat.scrollHeight;
document.fonts.ready.then(() => {
chat.scrollTop = chat.scrollHeight;
});
}
function prettyModelName(model) {
let m = (model || `?`).split(`:`);
m = m[m.length - 1].split(`/`);
return m[m.length - 1];
}
// ── 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]');
const parent = moreEl.parentNode;
while (content.firstChild) parent.insertBefore(content.firstChild, moreEl);
moreEl.remove();
// EasyMDE/CodeMirror braucht refresh() nach Sichtbarwerden
parent.querySelectorAll('.EasyMDEContainer').forEach(c => {
const cm = c.querySelector('.CodeMirror');
if (cm && cm.CodeMirror) cm.CodeMirror.refresh();
});
});
}
// ── 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'
});
}
function formatTimestamp(isoString) {
if (!isoString) return '';
var d = new Date(isoString);
if (isNaN(d.getTime())) return '';
var dd = String(d.getDate()).padStart(2, '0');
var mm = String(d.getMonth() + 1).padStart(2, '0');
var yyyy = d.getFullYear();
var hh = String(d.getHours()).padStart(2, '0');
var min = String(d.getMinutes()).padStart(2, '0');
var ss = String(d.getSeconds()).padStart(2, '0');
return dd + '.' + mm + '.' + yyyy + ' ' + hh + ':' + min + ':' + ss;
}
// ── 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());
}
// ── Review/Sub-Agent Helpers ──
function _isReviewMessage(content) {
return content && (content.startsWith('[Review:') || content.startsWith('[Review-Feedback') || content.startsWith('[Finalizer:') || content.startsWith('[Preparer:'));
}
/**
* Returns { icon, defaultName } for a sub-agent type.
* @param {string} agentType - 'preparer', 'finalizer', or 'review'
*/
function _getSubAgentMeta(agentType) {
if (agentType === 'preparer') return { icon: 'fa-filter', defaultName: 'Vorbereiter-Agent' };
if (agentType === 'finalizer') return { icon: 'fa-flag-checkered', defaultName: 'Finalizer-Agent' };
return { icon: 'fa-check-circle-o', defaultName: 'Review-Agent' };
}
/**
* Creates a review/sub-agent header element.
* @param {string} agentType - 'preparer', 'finalizer', or 'review'
* @param {string} agentName - Display name of the agent
* @param {object} [extra] - Optional: { round, maxRounds } for review
*/
function _createSubAgentHeader(agentType, agentName, extra) {
var meta = _getSubAgentMeta(agentType);
var header = document.createElement('div');
header.className = 'review-header';
var html = '<i class="fa ' + meta.icon + '"></i> <strong>' + esc(agentName || meta.defaultName) + '</strong>';
if (agentType === 'review' && extra && extra.round) {
html += ' <span class="review-round">Runde ' + extra.round + '/' + extra.maxRounds + '</span>';
}
header.innerHTML = html;
return header;
}
// ── Streamed Elements ──
function removeStreamedElements() {
chatEl().querySelectorAll('.streamed').forEach(el => el.remove());
}