p64
This commit is contained in:
@@ -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, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>AI Agent</title>
|
||||
<title></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- Markdown + Sanitizer -->
|
||||
@@ -25,29 +25,29 @@
|
||||
<div class="d-flex">
|
||||
<img src="/assets/img/logo.png" style="max-height: 20px;">
|
||||
</div>
|
||||
<div class="text-info fw-bold" style="font-size: 12px">KI-Agent</div>
|
||||
<div class="text-info fw-bold" style="font-size: 12px" data-chat-message-text="ui.header_title"></div>
|
||||
</div>
|
||||
<img src="/assets/img/logo.svg" style="max-height: 20px;">
|
||||
<div class="spacer"></div>
|
||||
|
||||
<button id="clear" class="btn btn-trans">Diesen Chat löschen</button>
|
||||
<button id="clear" class="btn btn-trans" data-chat-message-text="ui.buttons.clear"></button>
|
||||
</div>
|
||||
<div id="ai-cloud" class="ai-cloud d-none"></div>
|
||||
<div id="chat" class="chat"></div>
|
||||
|
||||
<div id="retriex-chat-options" class="retriex-chat-options p-2" aria-label="Chat-Anzeigeoptionen">
|
||||
<div id="retriex-chat-options" class="retriex-chat-options p-2" data-chat-message-aria-label="ui.options.aria_label">
|
||||
<label class="retriex-option-toggle" for="toggle-retriex-cards">
|
||||
<input id="toggle-retriex-cards" type="checkbox">
|
||||
<span>Statusinfo anzeigen</span>
|
||||
<span data-chat-message-text="ui.options.status_info"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="input-area">
|
||||
<textarea id="prompt" class="form-control bg-dark" placeholder="Stelle eine Frage"></textarea>
|
||||
<button id="send" class="btn btn-trans">Send</button>
|
||||
<button id="abort" class="btn btn-trans" disabled>Abbrechen</button>
|
||||
<textarea id="prompt" class="form-control bg-dark" data-chat-message-placeholder="ui.input.prompt_placeholder"></textarea>
|
||||
<button id="send" class="btn btn-trans" data-chat-message-text="ui.buttons.send"></button>
|
||||
<button id="abort" class="btn btn-trans" disabled data-chat-message-text="ui.buttons.abort"></button>
|
||||
</div>
|
||||
<div class="small mt-2 text-center text-secondary">powered by mitho® | RetrieX kann fehlerhafte Ausgaben machen. RetrieX verwendet alle Daten zum Trainieren seiner Modelle.</div>
|
||||
<div class="small mt-2 text-center text-secondary" data-chat-message-text="ui.footer_disclaimer"></div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user