optimize ui
This commit is contained in:
@@ -5,7 +5,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const abortBtn = document.getElementById('abort');
|
const abortBtn = document.getElementById('abort');
|
||||||
const clearBtn = document.getElementById('clear');
|
const clearBtn = document.getElementById('clear');
|
||||||
const aiCloudEl = document.getElementById('ai-cloud');
|
const aiCloudEl = document.getElementById('ai-cloud');
|
||||||
|
const retriexCardsToggleEl = document.getElementById('toggle-retriex-cards');
|
||||||
const LAST_TURN_STORAGE_KEY = 'retriex:lastCompletedTurn';
|
const LAST_TURN_STORAGE_KEY = 'retriex:lastCompletedTurn';
|
||||||
|
const DETAIL_CARDS_STORAGE_KEY = 'retriex:showDetailCards';
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
abortRequested: false,
|
abortRequested: false,
|
||||||
@@ -22,6 +24,42 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
marked.setOptions({breaks: true});
|
marked.setOptions({breaks: true});
|
||||||
|
|
||||||
|
function setRetriexDetailCardsVisible(isVisible, persist = true) {
|
||||||
|
document.body.classList.toggle('retriex-show-detail-cards', isVisible);
|
||||||
|
|
||||||
|
if (retriexCardsToggleEl) {
|
||||||
|
retriexCardsToggleEl.checked = isVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!persist) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.localStorage?.setItem(DETAIL_CARDS_STORAGE_KEY, isVisible ? '1' : '0');
|
||||||
|
} catch (err) {
|
||||||
|
console.debug('Could not persist detail card visibility:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initRetriexDetailCardsToggle() {
|
||||||
|
let isVisible = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isVisible = window.localStorage?.getItem(DETAIL_CARDS_STORAGE_KEY) === '1';
|
||||||
|
} catch (err) {
|
||||||
|
console.debug('Could not read detail card visibility:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRetriexDetailCardsVisible(isVisible, false);
|
||||||
|
|
||||||
|
retriexCardsToggleEl?.addEventListener('change', () => {
|
||||||
|
setRetriexDetailCardsVisible(retriexCardsToggleEl.checked, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initRetriexDetailCardsToggle();
|
||||||
|
|
||||||
function renderMarkdown(text) {
|
function renderMarkdown(text) {
|
||||||
return DOMPurify.sanitize(marked.parse(text));
|
return DOMPurify.sanitize(marked.parse(text));
|
||||||
}
|
}
|
||||||
@@ -764,8 +802,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
state.eventSource = source;
|
state.eventSource = source;
|
||||||
|
|
||||||
let networkErrorTimer = null;
|
let networkErrorTimer = null;
|
||||||
let completionWatchdogTimer = null;
|
|
||||||
let completionWatchdogInFlight = false;
|
|
||||||
|
|
||||||
const clearNetworkErrorTimer = () => {
|
const clearNetworkErrorTimer = () => {
|
||||||
if (!networkErrorTimer) {
|
if (!networkErrorTimer) {
|
||||||
@@ -776,15 +812,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
networkErrorTimer = null;
|
networkErrorTimer = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearCompletionWatchdog = () => {
|
|
||||||
if (!completionWatchdogTimer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearInterval(completionWatchdogTimer);
|
|
||||||
completionWatchdogTimer = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const complete = () => {
|
const complete = () => {
|
||||||
if (finished) {
|
if (finished) {
|
||||||
return;
|
return;
|
||||||
@@ -792,7 +819,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
finished = true;
|
finished = true;
|
||||||
clearNetworkErrorTimer();
|
clearNetworkErrorTimer();
|
||||||
clearCompletionWatchdog();
|
|
||||||
finishEventStream();
|
finishEventStream();
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
@@ -804,73 +830,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
finished = true;
|
finished = true;
|
||||||
clearNetworkErrorTimer();
|
clearNetworkErrorTimer();
|
||||||
clearCompletionWatchdog();
|
|
||||||
finishEventStream();
|
finishEventStream();
|
||||||
reject(err);
|
reject(err);
|
||||||
};
|
};
|
||||||
|
|
||||||
const finishFromStatus = (payload) => {
|
|
||||||
if (finished || state.abortRequested || !payload || typeof payload !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = String(payload.status || '');
|
|
||||||
const serverLastEventId = Number.parseInt(String(payload.lastEventId || '0'), 10) || 0;
|
|
||||||
|
|
||||||
// Only finalize from the side-channel when all numbered content chunks are already visible.
|
|
||||||
// This prevents cutting off the final answer if the browser missed more than just the terminal done event.
|
|
||||||
if (serverLastEventId > lastSseEventId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'completed') {
|
|
||||||
finalizeStream(bubble, raw);
|
|
||||||
rememberCompletedTurn(prompt, raw);
|
|
||||||
complete();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'failed' || status === 'interrupted') {
|
|
||||||
appendError(payload.message || 'Der Antwort-Stream wurde beendet, ohne ein Abschluss-Signal an den Browser zu senden.');
|
|
||||||
complete();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkCompletionStatus = async () => {
|
|
||||||
if (finished || state.abortRequested || completionWatchdogInFlight) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
completionWatchdogInFlight = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const statusRes = await fetch(`/ask-jobs/${encodeURIComponent(jobId)}`, {
|
|
||||||
method: 'GET',
|
|
||||||
cache: 'no-store',
|
|
||||||
signal: state.abortController?.signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!statusRes.ok) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return finishFromStatus(await statusRes.json());
|
|
||||||
} catch (err) {
|
|
||||||
if (!state.abortRequested) {
|
|
||||||
console.debug('Stream completion watchdog failed:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
completionWatchdogInFlight = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
completionWatchdogTimer = setInterval(checkCompletionStatus, 2500);
|
|
||||||
|
|
||||||
state.completeStream = complete;
|
state.completeStream = complete;
|
||||||
state.failStream = fail;
|
state.failStream = fail;
|
||||||
|
|
||||||
@@ -890,13 +853,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.data === '[DONE]') {
|
|
||||||
finalizeStream(bubble, raw);
|
|
||||||
rememberCompletedTurn(prompt, raw);
|
|
||||||
complete();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const numericEventId = Number.parseInt(event.lastEventId || '', 10);
|
const numericEventId = Number.parseInt(event.lastEventId || '', 10);
|
||||||
|
|
||||||
if (Number.isFinite(numericEventId) && numericEventId > 0) {
|
if (Number.isFinite(numericEventId) && numericEventId > 0) {
|
||||||
@@ -931,20 +887,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void checkCompletionStatus();
|
|
||||||
|
|
||||||
if (source.readyState === EventSource.CLOSED) {
|
if (source.readyState === EventSource.CLOSED) {
|
||||||
window.setTimeout(async () => {
|
|
||||||
if (finished || state.abortRequested) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusCompleted = await checkCompletionStatus();
|
|
||||||
|
|
||||||
if (!statusCompleted && !finished) {
|
|
||||||
fail(new Error('EventSource connection closed'));
|
fail(new Error('EventSource connection closed'));
|
||||||
}
|
|
||||||
}, 250);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -464,6 +464,39 @@ span.think {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.retriex-chat-options {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 0.45rem 0 0.75rem;
|
||||||
|
color: rgba(248, 249, 250, 0.76);
|
||||||
|
font-size: 0.84rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retriex-option-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
border: 1px solid rgba(134, 183, 254, 0.2);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.28rem 0.65rem;
|
||||||
|
background: rgba(13, 17, 23, 0.42);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retriex-option-toggle input {
|
||||||
|
width: 0.95rem;
|
||||||
|
height: 0.95rem;
|
||||||
|
margin: 0;
|
||||||
|
accent-color: #0dcaf0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.retriex-show-detail-cards) #chat .retriex-meta-card,
|
||||||
|
body:not(.retriex-show-detail-cards) #chat .retriex-alert {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* RetrieX chat meta/status cards */
|
/* RetrieX chat meta/status cards */
|
||||||
.retriex-meta-card,
|
.retriex-meta-card,
|
||||||
.retriex-alert {
|
.retriex-alert {
|
||||||
|
|||||||
@@ -32,6 +32,13 @@
|
|||||||
<div id="ai-cloud" class="ai-cloud d-none"></div>
|
<div id="ai-cloud" class="ai-cloud d-none"></div>
|
||||||
<div id="chat" class="chat"></div>
|
<div id="chat" class="chat"></div>
|
||||||
|
|
||||||
|
<div id="retriex-chat-options" class="retriex-chat-options" aria-label="Chat-Anzeigeoptionen">
|
||||||
|
<label class="retriex-option-toggle" for="toggle-retriex-cards">
|
||||||
|
<input id="toggle-retriex-cards" type="checkbox">
|
||||||
|
<span>Detailkarten anzeigen</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="input-area">
|
<div class="input-area">
|
||||||
<textarea id="prompt" class="form-control bg-dark" placeholder="Stelle eine Frage"></textarea>
|
<textarea id="prompt" class="form-control bg-dark" placeholder="Stelle eine Frage"></textarea>
|
||||||
<button id="send" class="btn btn-trans">Send</button>
|
<button id="send" class="btn btn-trans">Send</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user