Workflow-status: a new WorkflowProgressTracker singleton taps the
suppressed dynamic-workflow subagent events (those carrying
parent_tool_use_id) into per-session telemetry without altering the
existing guard; the snapshot is serialized into the new chains.workflow_info
column on FinalizeChain (ChainId-matched so a panel member of the same
session cannot clear it) and exposed via GET /api/sessions/{id}/workflow-status
(live -> persisted -> none). The header menu gains a 'Workflow-Status' entry
with a modal that live-polls every 2s while running and self-stops on close.
Panel on-demand: Fusion/MoA is now a transient per-request multiModelMode
flag (like plan/ultracode) instead of a persisted session column. Session
settings keep only the panel members + moa_layers; the mode <select> was
removed and the composer dropdown drives the mode per request.
Docs: ARCHITECTURE.md, ARCHITECTURE_QUICK.md and README.md updated for both
features plus the already-shipped z.ai/GLM provider, GLM 5.2+ effort
passthrough, web-search-prime MCP and the ultracode toggle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
698 lines
23 KiB
JavaScript
698 lines
23 KiB
JavaScript
// ── Chat Page ──
|
||
|
||
let _streamingPollInterval = null;
|
||
let _recoveryChainId = null;
|
||
let _emojiPickerEl = null;
|
||
let _emojiPickerCloseHandler = null;
|
||
let _chatUsageKeyId = null;
|
||
let _chatUsageScrollHandler = null;
|
||
|
||
// ── Chat Usage Badge ──
|
||
|
||
function _createChatUsageBadge(usage5h, usage7d) {
|
||
if (usage5h == null && usage7d == null) return null;
|
||
var badge = document.createElement('div');
|
||
badge.className = 'usage-badge';
|
||
|
||
function addRow(label, pct) {
|
||
if (pct == null) return;
|
||
var row = document.createElement('div');
|
||
row.className = 'usage-row';
|
||
var lbl = document.createElement('span');
|
||
lbl.className = 'usage-label';
|
||
lbl.textContent = label;
|
||
var track = document.createElement('div');
|
||
track.className = 'usage-track';
|
||
var fill = document.createElement('div');
|
||
fill.className = 'usage-fill';
|
||
if (pct >= 90) fill.className += ' danger';
|
||
else if (pct >= 70) fill.className += ' warn';
|
||
fill.style.width = Math.min(pct, 100) + '%';
|
||
track.appendChild(fill);
|
||
var val = document.createElement('span');
|
||
val.className = 'usage-pct';
|
||
val.textContent = pct + '%';
|
||
row.appendChild(lbl);
|
||
row.appendChild(track);
|
||
row.appendChild(val);
|
||
badge.appendChild(row);
|
||
}
|
||
|
||
addRow('Sitzung', usage5h);
|
||
addRow('Woche', usage7d);
|
||
return badge;
|
||
}
|
||
|
||
async function _initChatUsageBadge() {
|
||
if (!currentUser) return;
|
||
var container = document.getElementById('chatUsageBadge');
|
||
if (!container) return;
|
||
var s = loadSettings();
|
||
if (!s.chatUsageBadge) { container.hidden = true; return; }
|
||
|
||
try {
|
||
var selectedKey = await apiGetSelectedKey(getCurrentAgentId());
|
||
if (!selectedKey || !selectedKey.selected) {
|
||
container.innerHTML = '';
|
||
var noKeyBtn = document.createElement('button');
|
||
noKeyBtn.className = 'chat-usage-nokey';
|
||
noKeyBtn.innerHTML = '<i class="fa fa-key"></i> Key auswählen';
|
||
noKeyBtn.addEventListener('click', function() { _showKeySwitchModal(); });
|
||
container.appendChild(noKeyBtn);
|
||
container.hidden = false;
|
||
return;
|
||
}
|
||
|
||
_chatUsageKeyId = selectedKey.keyId;
|
||
_renderChatUsageBadge(container, selectedKey);
|
||
container.hidden = false;
|
||
|
||
// Scroll-Listener: Badge ausblenden wenn User nach oben scrollt
|
||
var chatEl = document.getElementById('chat');
|
||
if (chatEl) {
|
||
_chatUsageScrollHandler = function() {
|
||
var threshold = 60;
|
||
var atBottom = chatEl.scrollHeight - chatEl.scrollTop - chatEl.clientHeight <= threshold;
|
||
container.classList.toggle('hidden', !atBottom);
|
||
};
|
||
chatEl.addEventListener('scroll', _chatUsageScrollHandler);
|
||
}
|
||
} catch(e) {
|
||
console.error('Usage badge load failed:', e);
|
||
}
|
||
}
|
||
|
||
async function _checkChatPermissions() {
|
||
if (!currentUser || currentUser.isAdmin) return;
|
||
if (currentUser.canUseCli !== false) return; // CLI erlaubt → kein Problem
|
||
try {
|
||
var selectedKey = await apiGetSelectedKey(getCurrentAgentId());
|
||
if (selectedKey && selectedKey.selected) return; // Key vorhanden → kein Problem
|
||
addWarningBubble('Kein API-Key zugewiesen und keine CLI-Berechtigung. Nachrichten können nicht verarbeitet werden. Bitte wende dich an einen Admin.');
|
||
} catch(e) { /* ignore */ }
|
||
}
|
||
|
||
async function _refreshChatUsageBadge() {
|
||
if (!currentUser) return;
|
||
var container = document.getElementById('chatUsageBadge');
|
||
if (!container || !_chatUsageKeyId) return;
|
||
try {
|
||
var selectedKey = await apiGetSelectedKey(getCurrentAgentId());
|
||
if (selectedKey) _renderChatUsageBadge(container, selectedKey);
|
||
} catch(e) { /* ignore */ }
|
||
}
|
||
|
||
function _renderChatUsageBadge(container, key) {
|
||
container.innerHTML = '';
|
||
|
||
var badge = _createChatUsageBadge(key.usage5h, key.usage7d);
|
||
if (!badge) {
|
||
badge = document.createElement('div');
|
||
badge.className = 'usage-badge usage-badge-nodata';
|
||
badge.innerHTML = '<i class="fa fa-key"></i> ' + (key.name || 'API Key');
|
||
}
|
||
badge.addEventListener('click', function(e) {
|
||
e.stopPropagation();
|
||
_showKeySwitchModal();
|
||
});
|
||
badge.title = (key.name || 'API Key') + ' · ' + (key.plan || '?');
|
||
badge.classList.add('clickable');
|
||
container.appendChild(badge);
|
||
|
||
// Check-Button nur für Admins
|
||
if (currentUser && currentUser.isAdmin) {
|
||
var checkBtn = document.createElement('button');
|
||
checkBtn.className = 'chat-usage-check icon-btn';
|
||
checkBtn.innerHTML = '<i class="fa fa-refresh"></i>';
|
||
checkBtn.title = 'Key prüfen';
|
||
checkBtn.addEventListener('click', async function(e) {
|
||
e.stopPropagation();
|
||
checkBtn.disabled = true;
|
||
checkBtn.querySelector('i').className = 'fa fa-spinner fa-spin';
|
||
try {
|
||
await apiCheckApiKey(_chatUsageKeyId);
|
||
var selectedKey = await apiGetSelectedKey(getCurrentAgentId());
|
||
if (selectedKey) _renderChatUsageBadge(container, selectedKey);
|
||
} catch(e) {
|
||
console.error('Key check failed:', e);
|
||
}
|
||
checkBtn.disabled = false;
|
||
checkBtn.querySelector('i').className = 'fa fa-refresh';
|
||
});
|
||
container.appendChild(checkBtn);
|
||
}
|
||
}
|
||
|
||
async function _showKeySwitchModal() {
|
||
var keys = [];
|
||
try { keys = await apiGetAvailableKeys(); } catch(e) { return; }
|
||
|
||
var selectedKey = null;
|
||
try { selectedKey = await apiGetSelectedKey(getCurrentAgentId()); } catch(e) { /* ignore */ }
|
||
var selectedKeyId = selectedKey ? selectedKey.keyId : null;
|
||
|
||
var { body, close } = _createModal('API Key wechseln', { modal: 'key-switch-modal' });
|
||
|
||
if (keys.length === 0) {
|
||
var empty = document.createElement('div');
|
||
empty.className = 'empty-hint';
|
||
empty.textContent = 'Keine Keys verfügbar. Ein Admin muss Keys als öffentlich markieren.';
|
||
body.appendChild(empty);
|
||
return;
|
||
}
|
||
|
||
var list = document.createElement('div');
|
||
list.className = 'key-switch-list';
|
||
|
||
// "Kein Key"-Option als erste Zeile
|
||
var noneRow = document.createElement('div');
|
||
noneRow.className = 'key-switch-row';
|
||
if (!selectedKeyId) noneRow.classList.add('active');
|
||
var noneInfo = document.createElement('div');
|
||
noneInfo.className = 'key-switch-info';
|
||
var noneName = document.createElement('div');
|
||
noneName.className = 'key-switch-name';
|
||
noneName.textContent = 'Kein Key';
|
||
noneInfo.appendChild(noneName);
|
||
var noneMeta = document.createElement('div');
|
||
noneMeta.className = 'key-switch-meta';
|
||
noneMeta.textContent = 'Agent ohne API-Key nutzen';
|
||
noneInfo.appendChild(noneMeta);
|
||
noneRow.appendChild(noneInfo);
|
||
if (selectedKeyId) {
|
||
noneRow.classList.add('clickable');
|
||
noneRow.addEventListener('click', async function() {
|
||
noneRow.classList.add('loading-state');
|
||
try {
|
||
await apiSelectApiKey('none', getCurrentAgentId());
|
||
close();
|
||
await _initChatUsageBadge();
|
||
} catch(e) {
|
||
noneRow.classList.remove('loading-state');
|
||
}
|
||
});
|
||
}
|
||
list.appendChild(noneRow);
|
||
|
||
keys.forEach(function(key) {
|
||
var row = document.createElement('div');
|
||
row.className = 'key-switch-row';
|
||
if (key.keyId === selectedKeyId) row.classList.add('active');
|
||
|
||
var info = document.createElement('div');
|
||
info.className = 'key-switch-info';
|
||
|
||
var name = document.createElement('div');
|
||
name.className = 'key-switch-name';
|
||
name.textContent = key.name || key.keyId;
|
||
info.appendChild(name);
|
||
|
||
var meta = document.createElement('div');
|
||
meta.className = 'key-switch-meta';
|
||
var parts = [];
|
||
var providerLabels = { anthropic: '🟤 Anthropic', openai: '🟢 OpenAI', google: '🔵 Google' };
|
||
parts.push(providerLabels[key.provider] || key.provider || 'Anthropic');
|
||
if (key.plan) parts.push(key.plan);
|
||
if (key.status) parts.push(key.status === 'valid' ? '✅' : '⚠️');
|
||
meta.textContent = parts.join(' · ');
|
||
info.appendChild(meta);
|
||
|
||
row.appendChild(info);
|
||
|
||
var badge = _createChatUsageBadge(key.usage5h, key.usage7d);
|
||
if (badge) {
|
||
row.appendChild(badge);
|
||
}
|
||
|
||
if (key.keyId !== selectedKeyId) {
|
||
row.classList.add('clickable');
|
||
row.addEventListener('click', async function() {
|
||
row.classList.add('loading-state');
|
||
try {
|
||
await apiSelectApiKey(key.keyId, getCurrentAgentId());
|
||
close();
|
||
await _initChatUsageBadge();
|
||
} catch(e) {
|
||
console.error('Key switch failed:', e);
|
||
row.classList.remove('loading-state');
|
||
}
|
||
});
|
||
}
|
||
|
||
list.appendChild(row);
|
||
});
|
||
|
||
body.appendChild(list);
|
||
}
|
||
|
||
function _buildEmojiPicker() {
|
||
const picker = document.createElement('div');
|
||
picker.className = 'emoji-picker';
|
||
|
||
for (const [category, emojis] of Object.entries(EMOJI_USERDATA)) {
|
||
const section = document.createElement('div');
|
||
section.className = 'emoji-picker-section';
|
||
|
||
const label = document.createElement('div');
|
||
label.className = 'emoji-picker-label';
|
||
label.textContent = category;
|
||
section.appendChild(label);
|
||
|
||
const grid = document.createElement('div');
|
||
grid.className = 'emoji-picker-grid';
|
||
|
||
for (const emoji of emojis) {
|
||
const btn = document.createElement('button');
|
||
btn.className = 'emoji-picker-btn';
|
||
btn.textContent = emoji;
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const input = document.getElementById('input');
|
||
if (input) {
|
||
const start = input.selectionStart;
|
||
const end = input.selectionEnd;
|
||
const val = input.value;
|
||
input.value = val.slice(0, start) + emoji + val.slice(end);
|
||
const pos = start + emoji.length;
|
||
input.setSelectionRange(pos, pos);
|
||
input.dispatchEvent(new Event('input'));
|
||
input.focus();
|
||
}
|
||
closeEmojiPicker();
|
||
});
|
||
// Fluent Emoji im Button rendern
|
||
parseFluentEmojis(btn);
|
||
grid.appendChild(btn);
|
||
}
|
||
|
||
section.appendChild(grid);
|
||
picker.appendChild(section);
|
||
}
|
||
|
||
return picker;
|
||
}
|
||
|
||
function toggleEmojiPicker(anchor) {
|
||
if (_emojiPickerEl) {
|
||
closeEmojiPicker();
|
||
return;
|
||
}
|
||
|
||
const picker = _buildEmojiPicker();
|
||
document.body.appendChild(picker);
|
||
_emojiPickerEl = picker;
|
||
|
||
// Positionierung: über dem Button, rechts ausgerichtet
|
||
const rect = anchor.getBoundingClientRect();
|
||
const pickerHeight = 320;
|
||
const pickerWidth = 280;
|
||
let top = rect.top - pickerHeight - 6;
|
||
let left = rect.right - pickerWidth;
|
||
|
||
// Falls kein Platz oben → unter dem Button
|
||
if (top < 8) top = rect.bottom + 6;
|
||
// Falls links abgeschnitten → linksbündig zum Button
|
||
if (left < 8) left = 8;
|
||
|
||
picker.style.top = top + 'px';
|
||
picker.style.left = left + 'px';
|
||
|
||
_emojiPickerCloseHandler = (e) => {
|
||
if (!picker.contains(e.target)) closeEmojiPicker();
|
||
};
|
||
setTimeout(() => document.addEventListener('click', _emojiPickerCloseHandler), 0);
|
||
}
|
||
|
||
function closeEmojiPicker() {
|
||
if (_emojiPickerEl) {
|
||
_emojiPickerEl.remove();
|
||
_emojiPickerEl = null;
|
||
}
|
||
if (_emojiPickerCloseHandler) {
|
||
document.removeEventListener('click', _emojiPickerCloseHandler);
|
||
_emojiPickerCloseHandler = 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;
|
||
|
||
// Typing-Indicator stoppen
|
||
stopTypingIndicator();
|
||
|
||
// 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;
|
||
}
|
||
|
||
currentAgentId = session.agentId;
|
||
|
||
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;
|
||
});
|
||
}
|
||
|
||
// Remote-Verbindungen
|
||
const remoteBtn = document.getElementById('remoteBtn');
|
||
if (remoteBtn) remoteBtn.addEventListener('click', () => {
|
||
if (chatMenuDropdown) chatMenuDropdown.hidden = true;
|
||
showRemoteModal(sessionId);
|
||
});
|
||
|
||
// System-Prompt anzeigen
|
||
const systemPromptBtn = document.getElementById('systemPromptBtn');
|
||
if (systemPromptBtn) systemPromptBtn.addEventListener('click', () => {
|
||
if (chatMenuDropdown) chatMenuDropdown.hidden = true;
|
||
showSystemPromptModal(sessionId);
|
||
});
|
||
|
||
// Payload anzeigen
|
||
const payloadPreviewBtn = document.getElementById('payloadPreviewBtn');
|
||
if (payloadPreviewBtn) payloadPreviewBtn.addEventListener('click', () => {
|
||
if (chatMenuDropdown) chatMenuDropdown.hidden = true;
|
||
showPayloadPreviewModal(sessionId);
|
||
});
|
||
|
||
// Workflow-Status anzeigen
|
||
const workflowStatusBtn = document.getElementById('workflowStatusBtn');
|
||
if (workflowStatusBtn) workflowStatusBtn.addEventListener('click', () => {
|
||
if (chatMenuDropdown) chatMenuDropdown.hidden = true;
|
||
showWorkflowStatusModal(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) {
|
||
setCurrentAgentName(agent.name || null);
|
||
if (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', () => {
|
||
if (history.state && history.length > 2) {
|
||
history.back();
|
||
} else {
|
||
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'))
|
||
}
|
||
|
||
// Emoji Picker
|
||
const emojiBtn = document.getElementById('emojiBtn');
|
||
if (emojiBtn) {
|
||
emojiBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
toggleEmojiPicker(emojiBtn);
|
||
});
|
||
}
|
||
|
||
// Workspace Explorer
|
||
const explorerBtn = document.getElementById('explorerBtn');
|
||
if (explorerBtn) {
|
||
explorerBtn.addEventListener('click', () => {
|
||
openFileExplorer({
|
||
sessionId,
|
||
onSelect(ref) {
|
||
addPendingRef(ref);
|
||
document.getElementById('input')?.focus();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Unified Modus-Menü (Aus / Plan / Ultracode / Fusion / MoA) – aus der Session initialisieren.
|
||
// Klick öffnet ein Dropdown (wie der Emoji-Picker), aus dem der Modus direkt gewählt wird.
|
||
const compositeModeBtn = document.getElementById('planModeBtn');
|
||
if (compositeModeBtn) {
|
||
const hasPanelAgents = !!(session.panelAgentIds && String(session.panelAgentIds).trim());
|
||
initCompositeMode(hasPanelAgents);
|
||
compositeModeBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
toggleCompositeMenu(compositeModeBtn);
|
||
});
|
||
}
|
||
|
||
// 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
|
||
lastKnownMessageId = 0;
|
||
loadedArchiveCount = 0;
|
||
_oldestLoadedSortKey = 0;
|
||
_hasMoreOlderMessages = false;
|
||
isStreaming = false;
|
||
|
||
await loadHistory();
|
||
|
||
// Usage Badge laden (Admin only)
|
||
_initChatUsageBadge();
|
||
|
||
// Warnung anzeigen wenn kein Key und keine CLI-Berechtigung
|
||
_checkChatPermissions();
|
||
|
||
// 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;
|
||
startTypingIndicator();
|
||
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;
|
||
stopTypingIndicator();
|
||
setCurrentAgentName(null);
|
||
currentAgentId = null;
|
||
closeEmojiPicker();
|
||
// Usage Badge cleanup
|
||
if (_chatUsageScrollHandler) {
|
||
var chatEl = document.getElementById('chat');
|
||
if (chatEl) chatEl.removeEventListener('scroll', _chatUsageScrollHandler);
|
||
_chatUsageScrollHandler = null;
|
||
}
|
||
_chatUsageKeyId = null;
|
||
if (this._closeMenu) {
|
||
document.removeEventListener('click', this._closeMenu);
|
||
this._closeMenu = null;
|
||
}
|
||
if (_streamingPollInterval) {
|
||
clearInterval(_streamingPollInterval);
|
||
_streamingPollInterval = null;
|
||
}
|
||
stopHistoryPolling();
|
||
lastKnownMessageId = 0;
|
||
loadedArchiveCount = 0;
|
||
isStreaming = false;
|
||
clearPendingFiles();
|
||
clearPendingRefs();
|
||
clearReply();
|
||
}
|
||
});
|