This commit is contained in:
team 1
2026-05-09 11:24:08 +02:00
parent 424aef2575
commit c327dc4102
7 changed files with 428 additions and 29 deletions

View File

@@ -1,4 +1,4 @@
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', async () => {
const chatEl = document.getElementById('chat');
const promptEl = document.getElementById('prompt');
const sendBtn = document.getElementById('send');
@@ -12,6 +12,106 @@ document.addEventListener('DOMContentLoaded', () => {
const JOB_COMPLETION_CATCHUP_GRACE_MS = 10000;
const JOB_CLIENT_STALE_GRACE_MS = 150000;
let chatMessages = {};
function configuredMessage(path, parameters = {}) {
const segments = String(path || '').split('.').filter(Boolean);
let current = chatMessages;
for (const segment of segments) {
if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, segment)) {
return '';
}
current = current[segment];
}
if (typeof current !== 'string') {
return '';
}
return current.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, key) => {
if (!Object.prototype.hasOwnProperty.call(parameters, key)) {
return match;
}
return String(parameters[key] ?? '').replace(/\s+/g, ' ').trim();
});
}
function configuredMessageList(path) {
const segments = String(path || '').split('.').filter(Boolean);
let current = chatMessages;
for (const segment of segments) {
if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, segment)) {
return [];
}
current = current[segment];
}
return Array.isArray(current) ? current.filter((item) => typeof item === 'string' && item.trim() !== '') : [];
}
function escapeHtml(value) {
return String(value || '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function configuredEmphasis(path) {
const text = configuredMessage(path);
return text === '' ? '' : `<em>${escapeHtml(text)}</em>`;
}
async function loadConfiguredChatMessages() {
try {
const response = await fetch('/chat-messages/frontend', {
method: 'GET',
cache: 'no-store',
headers: {'Accept': 'application/json'},
});
if (!response.ok) {
console.error('Chat messages request failed with status:', response.status);
return;
}
const payload = await response.json();
chatMessages = payload && typeof payload === 'object' ? payload : {};
} catch (err) {
console.error('Chat messages load failed:', err);
}
}
function applyConfiguredUiMessages(root = document) {
const documentTitle = configuredMessage('document.title');
if (documentTitle !== '') {
document.title = documentTitle;
}
root.querySelectorAll('[data-chat-message-text]').forEach((element) => {
element.textContent = configuredMessage(element.getAttribute('data-chat-message-text') || '');
});
root.querySelectorAll('[data-chat-message-placeholder]').forEach((element) => {
element.setAttribute('placeholder', configuredMessage(element.getAttribute('data-chat-message-placeholder') || ''));
});
root.querySelectorAll('[data-chat-message-aria-label]').forEach((element) => {
element.setAttribute('aria-label', configuredMessage(element.getAttribute('data-chat-message-aria-label') || ''));
});
}
await loadConfiguredChatMessages();
applyConfiguredUiMessages();
const state = {
abortRequested: false,
isStreaming: false,
@@ -118,9 +218,8 @@ document.addEventListener('DOMContentLoaded', () => {
function isNoConcreteShopResponse(value) {
const normalized = normalizeContextHintText(value).toLowerCase();
return normalized.includes('keine konkrete shop-suchanfrage erkannt')
|| normalized.includes('shop-suche noch nicht belastbar auflösen')
|| normalized.includes('shop-suche noch nicht belastbar aufloesen');
return configuredMessageList('guards.no_concrete_shop_response_markers')
.some((marker) => normalized.includes(marker.toLowerCase()));
}
function rememberCompletedTurn(userPrompt, assistantText) {
@@ -278,7 +377,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
function addLoader() {
return addMessage('assistant', 'Antwort wird vorbereitet…', 'loader');
return addMessage('assistant', escapeHtml(configuredMessage('assistant.loader')), 'loader');
}
function hasMeaningfulChildContent(element) {
@@ -504,7 +603,7 @@ document.addEventListener('DOMContentLoaded', () => {
let key = canonicalRetriexSourceChipKey(chip.textContent);
if (key === 'shopsystem' || key === 'liveshopdaten') {
chip.textContent = 'Live-Shopdaten';
chip.textContent = configuredMessage('source_chips.live_shop_data');
key = 'liveshopdaten';
}
@@ -643,9 +742,9 @@ document.addEventListener('DOMContentLoaded', () => {
}
const isError = options.state === 'error';
const titleText = isError ? 'Antwort wurde unterbrochen' : 'Abgeschlossen';
const statusText = isError ? 'Status: unterbrochen' : 'Status: abgeschlossen';
const emptySourceText = isError ? 'nicht vollständig geprüft' : 'keine belastbare Datenbasis';
const titleText = isError ? configuredMessage('run_meta.interrupted_title') : configuredMessage('run_meta.completed_title');
const statusText = isError ? configuredMessage('run_meta.interrupted_status') : configuredMessage('run_meta.completed_status');
const emptySourceText = isError ? configuredMessage('run_meta.interrupted_empty_source') : configuredMessage('run_meta.completed_empty_source');
container.querySelectorAll('.retriex-run-meta[data-retriex-meta-id="run-status"]').forEach((card) => {
card.setAttribute('data-retriex-meta-state', isError ? 'error' : 'completed');
@@ -664,7 +763,7 @@ document.addEventListener('DOMContentLoaded', () => {
const emptySource = card.querySelector('.retriex-source-overview__empty');
if (emptySource && emptySource.textContent.trim() === 'wird geprüft') {
if (emptySource && emptySource.textContent.trim() === configuredMessage('run_meta.pending_source_marker')) {
emptySource.textContent = emptySourceText;
}
});
@@ -919,7 +1018,7 @@ document.addEventListener('DOMContentLoaded', () => {
};
const completeWithJobError = (message) => {
appendError(message || 'Der Antwort-Stream wurde beendet, bevor die Antwort abgeschlossen werden konnte.');
appendError(message || configuredMessage('stream.incomplete'));
complete();
};
@@ -937,7 +1036,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (!res.ok) {
if (res.status === 404 && Date.now() - lastClientProgressAt > JOB_CLIENT_STALE_GRACE_MS) {
completeWithJobError('Der Antwort-Job wurde nicht mehr gefunden. Bitte sende die Anfrage erneut.');
completeWithJobError(configuredMessage('stream.job_not_found_retry'));
}
return;
}
@@ -952,17 +1051,17 @@ document.addEventListener('DOMContentLoaded', () => {
}
if (status === 'failed') {
completeWithJobError(message || 'Der Antwort-Stream ist fehlgeschlagen. Bitte sende die Anfrage erneut.');
completeWithJobError(message || configuredMessage('stream.failed_retry'));
return;
}
if (status === 'interrupted') {
completeWithJobError(message || 'Der Antwort-Stream wurde durch einen Verbindungsabbruch unterbrochen. Bitte sende die Anfrage erneut, falls die Antwort unvollständig ist.');
completeWithJobError(message || configuredMessage('stream.interrupted_retry'));
return;
}
if (status === 'missing') {
completeWithJobError(message || 'Der Antwort-Job wurde nicht gefunden. Bitte sende die Anfrage erneut.');
completeWithJobError(message || configuredMessage('stream.missing_retry'));
return;
}
@@ -972,7 +1071,7 @@ document.addEventListener('DOMContentLoaded', () => {
const staleAfterSeconds = parsePositiveInteger(statusPayload?.runningStaleAfterSeconds);
if (updatedAt > 0 && serverTime > 0 && staleAfterSeconds > 0 && serverTime - updatedAt > staleAfterSeconds + 15) {
completeWithJobError(message || 'Der Antwort-Job liefert seit längerer Zeit keine neuen Daten. Der Stream wurde beendet.');
completeWithJobError(message || configuredMessage('stream.stale_retry'));
return;
}
}
@@ -1065,7 +1164,7 @@ document.addEventListener('DOMContentLoaded', () => {
bubble.classList.remove('loader');
const userMessage = 'Die Verbindung zum Antwort-Stream wurde unterbrochen. Bitte sende die Anfrage erneut, falls die Antwort unvollständig ist.';
const userMessage = configuredMessage('stream.connection_interrupted_retry');
if (raw.trim() !== '') {
const formattedMessage = `<em>${userMessage}</em>`;
@@ -1094,7 +1193,7 @@ document.addEventListener('DOMContentLoaded', () => {
state.completeStream?.();
await releaseStreamResources();
setBusyUi(false);
addMessage('assistant', '<em>[aborted]</em>');
addMessage('assistant', configuredEmphasis('assistant.aborted'));
});
clearBtn.addEventListener('click', async () => {
@@ -1116,6 +1215,6 @@ document.addEventListener('DOMContentLoaded', () => {
console.debug('Could not clear last completed turn:', err);
}
chatEl.innerHTML = '';
addMessage('assistant', '<em>History cleared.</em>');
addMessage('assistant', configuredEmphasis('assistant.history_cleared'));
});
});