Files
MelekiandClaude Opus 4.8 1a0f3b507e Session-Menü: Agent/Session bearbeiten + Rücksprung zum Chat
Zwei neue Menü-Einträge im Session-Header-Menü (rechts oben):
- Sparte "Agent" → "Agent bearbeiten" (Sprung zur Agent-Edit-Page)
- Sparte "Session" → "Session bearbeiten" (Sprung zur Session-Edit-Page)
Beide Sparten sind jetzt immer sichtbar (vorher nur bei vorhandenen
Custom-Commands).

Speichern/Abbrechen auf beiden Edit-Pages springt kontextabhängig zurück:
zum Chat, wenn die Page von dort geöffnet wurde (returnTo/returnId),
sonst wie bisher zum Dashboard. Löschen bleibt → Dashboard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 16:18:56 +02:00

719 lines
24 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── 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) {}
// Sektions-Kopf (Separator + Label)
const addSectionHeader = (label) => {
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);
};
// Navigations-Eintrag (z.B. "Agent bearbeiten" → Sprung zur Edit-Page)
const addNavItem = (label, icon, target, targetParams) => {
const btn = document.createElement('button');
btn.className = 'header-menu-item';
btn.innerHTML = `<i class="fa ${icon}"></i> ${label}`;
btn.addEventListener('click', () => {
chatMenuDropdown.hidden = true;
navigateTo(target, targetParams);
});
chatMenuDropdown.appendChild(btn);
};
// Custom-Command-Buttons einer Sparte anhängen
const addCmdButtons = (cmds) => {
cmds.filter(c => c.name && c.command).forEach(cmd => {
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);
});
};
// Sparte "Agent": bearbeiten + Agent-Befehle
addSectionHeader('Agent');
if (currentAgentId) addNavItem('Agent bearbeiten', 'fa-pencil', 'agent', { id: currentAgentId, returnTo: 'chat', returnId: sessionId });
addCmdButtons(agentCmds);
// Sparte "Session": bearbeiten + Session-Befehle
addSectionHeader('Session');
addNavItem('Session bearbeiten', 'fa-pencil', 'session', { id: sessionId, returnTo: 'chat', returnId: sessionId });
addCmdButtons(sessionCmds);
}
// 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();
}
});