// ── 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;
try {
var selectedKey = await apiGetSelectedKey(getCurrentAgentId());
if (!selectedKey || !selectedKey.selected) {
container.innerHTML = '';
var noKeyBtn = document.createElement('button');
noKeyBtn.className = 'chat-usage-nokey';
noKeyBtn.innerHTML = ' 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 _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 = ' ' + (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 = '';
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';
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);
});
// 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();
}
});
});
}
// Plan Mode Toggle
const planModeBtn = document.getElementById('planModeBtn');
if (planModeBtn) {
// Plan-Modus bei Session-Wechsel zurücksetzen
_planModeActive = false;
planModeBtn.classList.remove('plan-mode-active');
planModeBtn.addEventListener('click', togglePlanMode);
}
// 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();
// 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();
}
});