Files
gott/wwwroot/js/pages/chat-page.js
T
Meleki 2673aecc5b feat: Emoji-Picker Button im Chat-Input (rechts neben Senden)
- #emojiBtn links neben #send platziert
- Fluent Emoji 3D Rendering via parseFluentEmojis()
- Kein Tooltip auf den Emoji-Buttons
- Picker öffnet sich rechts-ausgerichtet über dem Button
- emoji.userdata.js geladen (Kategorien + Mappings)
- Schließt beim Klick außerhalb oder beim destroy()
2026-03-03 16:18:00 +01:00

397 lines
12 KiB
JavaScript
Raw 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;
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;
// 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', () => history.back());
}
// 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();
}
});
});
}
// 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;
closeEmojiPicker();
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();
}
});