- Neuer Stop-Button (#stop) neben Send-Button, erscheint nur während Stream - AbortController in chat.js: abort() schließt SSE-Verbindung → Backend CancellationToken triggered - apiChat() akzeptiert optionales AbortSignal - AbortError-Handling: zeigt 'Abgebrochen' statt Fehlermeldung - Button-Toggle in sendMessage(), stopStreaming(), startStreamingPoll(), Page-Load-Recovery - CSS: .stop-btn mit --dangerous Farbe - Diverse kleine UI-Fixes (TODO.md, theme.css box-spacing, settings-section Layout)
296 lines
9.6 KiB
JavaScript
296 lines
9.6 KiB
JavaScript
// ── Chat Page ──
|
||
|
||
let _streamingPollInterval = null;
|
||
let _recoveryChainId = null;
|
||
|
||
function startStreamingPoll(sessionId, sendBtn, chainId) {
|
||
_recoveryChainId = chainId;
|
||
// History-Polling stoppen – Streaming-Poll übernimmt
|
||
stopHistoryPolling();
|
||
if (_streamingPollInterval) clearInterval(_streamingPollInterval);
|
||
_streamingPollInterval = setInterval(async () => {
|
||
try {
|
||
// Neue Nachrichten laden (inkl. der noch streamenden)
|
||
await pollHistory(true);
|
||
|
||
const status = await apiIsStreaming(sessionId);
|
||
if (!status.streaming) {
|
||
clearInterval(_streamingPollInterval);
|
||
_streamingPollInterval = null;
|
||
isStreaming = false;
|
||
_abortController = null;
|
||
if (sendBtn) { sendBtn.disabled = false; sendBtn.hidden = false; }
|
||
const stopEl = document.getElementById('stop');
|
||
if (stopEl) stopEl.hidden = true;
|
||
|
||
// Finale Nachrichten nochmal laden
|
||
await pollHistory(true);
|
||
|
||
// Chain-Daten laden für Meta-Info im Chat
|
||
const cid = _recoveryChainId || status.chainId;
|
||
if (cid) {
|
||
const chain = await apiGetChain(cid);
|
||
if (chain) addChainMetaBubble(chain, false);
|
||
}
|
||
setMeta('Bereit');
|
||
_recoveryChainId = null;
|
||
|
||
// History-Polling wieder starten
|
||
startHistoryPolling();
|
||
}
|
||
} catch (e) {
|
||
console.error('Streaming-Poll fehlgeschlagen:', e);
|
||
}
|
||
}, 1500);
|
||
}
|
||
|
||
registerPage('chat', {
|
||
async init(params) {
|
||
const sessionId = params.id;
|
||
if (!sessionId) return navigateTo('dashboard');
|
||
|
||
currentSessionId = sessionId;
|
||
|
||
const session = await apiGetSession(sessionId);
|
||
if (!session) return navigateTo('dashboard');
|
||
|
||
// Kein Agent = kein Chat
|
||
if (!session.agentId) {
|
||
navigateTo('session', { id: sessionId });
|
||
return;
|
||
}
|
||
|
||
const titleEl = document.getElementById('sessionTitle');
|
||
if (titleEl) {
|
||
titleEl.textContent = session.name || 'Unbenannt';
|
||
}
|
||
|
||
// Header-Menü (3-Dot)
|
||
const chatMenuBtn = document.getElementById('chatMenuBtn');
|
||
const chatMenuDropdown = document.getElementById('chatMenuDropdown');
|
||
if (chatMenuBtn && chatMenuDropdown) {
|
||
chatMenuBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
chatMenuDropdown.hidden = !chatMenuDropdown.hidden;
|
||
});
|
||
document.addEventListener('click', this._closeMenu = () => {
|
||
chatMenuDropdown.hidden = true;
|
||
});
|
||
}
|
||
|
||
// System-Prompt anzeigen
|
||
const systemPromptBtn = document.getElementById('systemPromptBtn');
|
||
if (systemPromptBtn) systemPromptBtn.addEventListener('click', () => {
|
||
if (chatMenuDropdown) chatMenuDropdown.hidden = true;
|
||
showSystemPromptModal(sessionId);
|
||
});
|
||
|
||
// Archivieren
|
||
const archiveBtn = document.getElementById('archiveBtn');
|
||
if (archiveBtn) archiveBtn.addEventListener('click', () => {
|
||
if (chatMenuDropdown) chatMenuDropdown.hidden = true;
|
||
clearChat();
|
||
});
|
||
|
||
// Custom Command Buttons ins Header-Menü einfügen
|
||
if (chatMenuDropdown) {
|
||
// "System"-Label ganz oben einfügen
|
||
const systemLabel = document.createElement('div');
|
||
systemLabel.className = 'header-menu-label';
|
||
systemLabel.textContent = 'System';
|
||
chatMenuDropdown.prepend(systemLabel);
|
||
|
||
let agentCmds = [];
|
||
if (session.agentId) {
|
||
try {
|
||
const agent = await apiGetAgent(session.agentId);
|
||
if (agent && agent.customCommands) {
|
||
agentCmds = JSON.parse(agent.customCommands) || [];
|
||
}
|
||
} catch(e) {}
|
||
}
|
||
let sessionCmds = [];
|
||
try { sessionCmds = JSON.parse(session.customCommands || '[]') || []; } catch(e) {}
|
||
|
||
const addCmdSection = (cmds, label) => {
|
||
const validCmds = cmds.filter(c => c.name && c.command);
|
||
if (validCmds.length === 0) return;
|
||
const sep = document.createElement('div');
|
||
sep.className = 'header-menu-separator';
|
||
chatMenuDropdown.appendChild(sep);
|
||
const lbl = document.createElement('div');
|
||
lbl.className = 'header-menu-label';
|
||
lbl.textContent = label;
|
||
chatMenuDropdown.appendChild(lbl);
|
||
validCmds.forEach(cmd => {
|
||
if (!cmd.name || !cmd.command) return;
|
||
const type = cmd.type || 'background';
|
||
const btn = document.createElement('button');
|
||
btn.className = 'header-menu-item header-menu-cmd';
|
||
btn.textContent = cmd.name;
|
||
btn.title = type === 'message' ? cmd.command : `[${type}] ${cmd.command}`;
|
||
btn.addEventListener('click', async () => {
|
||
chatMenuDropdown.hidden = true;
|
||
if (type === 'message') {
|
||
const inputEl = document.getElementById('input');
|
||
if (inputEl) {
|
||
inputEl.value = cmd.command;
|
||
inputEl.dispatchEvent(new Event('input'));
|
||
}
|
||
sendMessage();
|
||
return;
|
||
}
|
||
btn.disabled = true;
|
||
btn.classList.add('running');
|
||
try {
|
||
if (type === 'foreground') {
|
||
const result = await apiExecCommand(sessionId, cmd.command, 'foreground');
|
||
showCommandOutputModal(cmd.name, cmd.command, result);
|
||
} else {
|
||
await apiExecCommand(sessionId, cmd.command, 'background');
|
||
}
|
||
} catch(e) {
|
||
console.error('Exec failed:', e);
|
||
}
|
||
btn.disabled = false;
|
||
btn.classList.remove('running');
|
||
});
|
||
chatMenuDropdown.appendChild(btn);
|
||
});
|
||
};
|
||
|
||
addCmdSection(agentCmds, 'Agent');
|
||
addCmdSection(sessionCmds, 'Session');
|
||
}
|
||
|
||
// Zurück zum Dashboard
|
||
const backBtn = document.getElementById('chatBackBtn');
|
||
if (backBtn) {
|
||
backBtn.addEventListener('click', () => navigateTo('dashboard'));
|
||
}
|
||
|
||
// Send / Stop
|
||
const sendBtn = document.getElementById('send');
|
||
if (sendBtn) sendBtn.addEventListener('click', sendMessage);
|
||
const stopBtn = document.getElementById('stop');
|
||
if (stopBtn) stopBtn.addEventListener('click', stopStreaming);
|
||
|
||
const inputEl = document.getElementById('input');
|
||
if (inputEl) {
|
||
inputEl.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
||
e.preventDefault();
|
||
sendMessage();
|
||
}
|
||
});
|
||
inputEl.addEventListener('input', function () {
|
||
this.style.height = 'auto';
|
||
this.style.height = Math.min(this.scrollHeight, 150) + 'px';
|
||
});
|
||
inputEl.dispatchEvent(new Event('input'))
|
||
}
|
||
|
||
// Workspace Explorer
|
||
const explorerBtn = document.getElementById('explorerBtn');
|
||
if (explorerBtn) {
|
||
explorerBtn.addEventListener('click', () => {
|
||
openFileExplorer({
|
||
sessionId,
|
||
onSelect(ref) {
|
||
addPendingRef(ref);
|
||
document.getElementById('input')?.focus();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// File Upload
|
||
const fileBtn = document.getElementById('fileBtn');
|
||
const fileInput = document.getElementById('fileInput');
|
||
if (fileBtn && fileInput) {
|
||
fileBtn.addEventListener('click', () => fileInput.click());
|
||
fileInput.addEventListener('change', (e) => {
|
||
if (e.target.files.length > 0) handleMultipleFiles(e.target.files);
|
||
});
|
||
}
|
||
|
||
// Drag & Drop
|
||
const chatContainer = document.querySelector('.chat-page');
|
||
if (chatContainer) {
|
||
chatContainer.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); });
|
||
chatContainer.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
if (e.dataTransfer.files.length > 0) handleMultipleFiles(e.dataTransfer.files);
|
||
});
|
||
}
|
||
|
||
// Paste
|
||
if (inputEl) {
|
||
inputEl.addEventListener('paste', (e) => {
|
||
const items = e.clipboardData?.items;
|
||
if (!items) return;
|
||
for (const item of items) {
|
||
if (item.kind === 'file') {
|
||
e.preventDefault();
|
||
handleFileSelect(item.getAsFile());
|
||
return;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// State zurücksetzen und History laden
|
||
messages.length = 0;
|
||
lastKnownMessageId = 0;
|
||
loadedArchiveCount = 0;
|
||
_oldestLoadedMessageId = 0;
|
||
_hasMoreOlderMessages = false;
|
||
isStreaming = false;
|
||
|
||
await loadHistory();
|
||
|
||
// Nach Font-Loading nochmal scrollen (Schriftart kann Höhe verändern)
|
||
document.fonts.ready.then(() => {
|
||
requestAnimationFrame(() => scrollDown());
|
||
});
|
||
|
||
// Prüfen ob Backend gerade noch streamt (z.B. nach Page-Refresh)
|
||
try {
|
||
const status = await apiIsStreaming(sessionId);
|
||
if (status.streaming) {
|
||
isStreaming = true;
|
||
if (sendBtn) { sendBtn.disabled = true; sendBtn.hidden = true; }
|
||
const stopEl = document.getElementById('stop');
|
||
if (stopEl) stopEl.hidden = false;
|
||
setMetaHtml('<span><i class="fa fa-hourglass-half"></i> Streaming...</span>');
|
||
startStreamingPoll(sessionId, sendBtn, status.chainId);
|
||
}
|
||
} catch (e) {
|
||
console.error('Streaming-Status prüfen fehlgeschlagen:', e);
|
||
}
|
||
},
|
||
|
||
destroy() {
|
||
// Stream nicht abbrechen (Backend soll weiterlaufen),
|
||
// aber Frontend-Processing stoppen
|
||
_chatAbandoned = true;
|
||
if (this._closeMenu) {
|
||
document.removeEventListener('click', this._closeMenu);
|
||
this._closeMenu = null;
|
||
}
|
||
if (_streamingPollInterval) {
|
||
clearInterval(_streamingPollInterval);
|
||
_streamingPollInterval = null;
|
||
}
|
||
stopHistoryPolling();
|
||
messages.length = 0;
|
||
lastKnownMessageId = 0;
|
||
loadedArchiveCount = 0;
|
||
isStreaming = false;
|
||
clearPendingFiles();
|
||
clearPendingRefs();
|
||
clearReply();
|
||
}
|
||
});
|