Frontend Refactoring: HTML/CSS/JS getrennt, modulare Struktur
This commit is contained in:
1 parent
9f2f3fee1f
commit
19f926da05
7 files changed
+534
-294
No files matched your search
@@ -0,0 +1,14 @@
|
||||
// ── API Calls ──
|
||||
|
||||
async function apiChat(model, messages, systemPrompt) {
|
||||
return fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model, messages, systemPrompt })
|
||||
});
|
||||
}
|
||||
|
||||
async function apiHistory(sessionId = 'null', limit = 50) {
|
||||
const resp = await fetch(`/api/history?sessionId=${sessionId}&limit=${limit}`);
|
||||
return resp.json();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// ── App Init ──
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// System Prompt Toggle
|
||||
$('#showSystem').addEventListener('change', () => {
|
||||
$('#systemPrompt').classList.toggle('visible', $('#showSystem').checked);
|
||||
});
|
||||
|
||||
// Send Button
|
||||
$('#send').addEventListener('click', sendMessage);
|
||||
|
||||
// Ctrl+Enter
|
||||
$('#input').addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && e.key === 'Enter') sendMessage();
|
||||
});
|
||||
|
||||
// Auto-Resize Textarea
|
||||
$('#input').addEventListener('input', function () {
|
||||
this.style.height = 'auto';
|
||||
this.style.height = Math.min(this.scrollHeight, 150) + 'px';
|
||||
});
|
||||
|
||||
// Clear Button
|
||||
$('#clearBtn').addEventListener('click', clearChat);
|
||||
|
||||
// History laden
|
||||
loadHistory();
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
// ── Chat State ──
|
||||
|
||||
const messages = [];
|
||||
|
||||
function clearChat() {
|
||||
messages.length = 0;
|
||||
chatEl().innerHTML = '';
|
||||
setMeta('');
|
||||
}
|
||||
|
||||
// ── Send Message ──
|
||||
|
||||
async function sendMessage() {
|
||||
const input = $('#input');
|
||||
const btn = $('#send');
|
||||
const model = $('#model').value;
|
||||
const systemPrompt = $('#systemPrompt').value.trim();
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
|
||||
messages.push({ role: 'user', content: text });
|
||||
addUserBubble(text);
|
||||
|
||||
input.value = '';
|
||||
input.style.height = 'auto';
|
||||
btn.disabled = true;
|
||||
setMetaHtml('<span style="color:#7c83ff">⏳ Streaming...</span>');
|
||||
|
||||
let fullText = '';
|
||||
let currentToolBox = null;
|
||||
|
||||
try {
|
||||
const resp = await apiChat(model, messages, systemPrompt);
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let eventType = null;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event: ')) {
|
||||
eventType = line.substring(7).trim();
|
||||
} else if (line.startsWith('data: ') && eventType) {
|
||||
let data;
|
||||
try { data = JSON.parse(line.substring(6)); } catch { continue; }
|
||||
|
||||
switch (eventType) {
|
||||
case 'text_start':
|
||||
endStreamBubble();
|
||||
startStreamBubble();
|
||||
break;
|
||||
|
||||
case 'text_delta':
|
||||
appendToStream(data.text);
|
||||
fullText += data.text;
|
||||
break;
|
||||
|
||||
case 'text_end':
|
||||
endStreamBubble();
|
||||
break;
|
||||
|
||||
case 'tool_start':
|
||||
endStreamBubble();
|
||||
currentToolBox = addToolBoxLoading(data.name);
|
||||
break;
|
||||
|
||||
case 'tool_end':
|
||||
finishToolBox(currentToolBox, data);
|
||||
currentToolBox = null;
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
endStreamBubble();
|
||||
addErrorBubble(data.error);
|
||||
break;
|
||||
|
||||
case 'done':
|
||||
endStreamBubble();
|
||||
let meta = `Model: ${data.model || model} | In: ${data.inputTokens || 0} | Out: ${data.outputTokens || 0}`;
|
||||
if (data.iterations > 1) meta += ` | Iterations: ${data.iterations}`;
|
||||
if (data.durationMs) meta += ` | ${(data.durationMs / 1000).toFixed(1)}s`;
|
||||
setMeta(meta);
|
||||
break;
|
||||
}
|
||||
eventType = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fullText) {
|
||||
messages.push({ role: 'assistant', content: fullText });
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
endStreamBubble();
|
||||
addErrorBubble('Netzwerk-Fehler: ' + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
endStreamBubble();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// ── History Loading ──
|
||||
|
||||
async function loadHistory() {
|
||||
try {
|
||||
const data = await apiHistory('null');
|
||||
data.reverse(); // DESC → chronologisch
|
||||
|
||||
for (const entry of data) {
|
||||
for (const msg of entry.messages) {
|
||||
switch (msg.type) {
|
||||
case 'text':
|
||||
if (msg.role === 'user') {
|
||||
addUserBubble(msg.content);
|
||||
messages.push({ role: 'user', content: msg.content });
|
||||
} else if (msg.role === 'assistant') {
|
||||
addAssistantBubble(msg.content);
|
||||
messages.push({ role: 'assistant', content: msg.content });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tool_use':
|
||||
addToolBoxStatic(msg.toolName, msg.content, null, false);
|
||||
break;
|
||||
|
||||
case 'tool_result':
|
||||
addToolBoxStatic(null, null, msg.content, msg.isError);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
addErrorBubble(msg.content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scrollDown();
|
||||
} catch (e) {
|
||||
console.error('History laden fehlgeschlagen:', e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// ── DOM Helpers ──
|
||||
|
||||
const $ = (sel) => document.querySelector(sel);
|
||||
const chatEl = () => $('#chat');
|
||||
const metaEl = () => $('#meta');
|
||||
|
||||
function scrollDown() {
|
||||
const chat = chatEl();
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function truncate(s, max) {
|
||||
return s && s.length > max ? s.substring(0, max) + '...' : (s || '');
|
||||
}
|
||||
|
||||
// ── Message Bubbles ──
|
||||
|
||||
function addUserBubble(text) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg user';
|
||||
div.textContent = text;
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
}
|
||||
|
||||
function addAssistantBubble(text) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg assistant';
|
||||
div.textContent = text;
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
return div;
|
||||
}
|
||||
|
||||
function addErrorBubble(text) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg system-info';
|
||||
div.style.color = '#ff6b6b';
|
||||
div.textContent = text;
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
}
|
||||
|
||||
// ── Streaming Bubble ──
|
||||
|
||||
let _streamDiv = null;
|
||||
let _streamText = '';
|
||||
|
||||
function startStreamBubble() {
|
||||
_streamDiv = document.createElement('div');
|
||||
_streamDiv.className = 'msg assistant';
|
||||
_streamDiv.innerHTML = '<span class="cursor"></span>';
|
||||
_streamText = '';
|
||||
chatEl().appendChild(_streamDiv);
|
||||
scrollDown();
|
||||
}
|
||||
|
||||
function appendToStream(text) {
|
||||
if (!_streamDiv) startStreamBubble();
|
||||
_streamText += text;
|
||||
_streamDiv.textContent = _streamText;
|
||||
const cursor = document.createElement('span');
|
||||
cursor.className = 'cursor';
|
||||
_streamDiv.appendChild(cursor);
|
||||
scrollDown();
|
||||
}
|
||||
|
||||
function endStreamBubble() {
|
||||
if (_streamDiv) {
|
||||
const cursor = _streamDiv.querySelector('.cursor');
|
||||
if (cursor) cursor.remove();
|
||||
if (!_streamText.trim()) _streamDiv.remove();
|
||||
}
|
||||
_streamDiv = null;
|
||||
_streamText = '';
|
||||
}
|
||||
|
||||
// ── Tool Boxes ──
|
||||
|
||||
function addToolBoxLoading(name) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'tool-box';
|
||||
div.innerHTML = '<span class="tool-name">' + esc(name) + '</span> <span class="tool-spinner">⏳</span>';
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
return div;
|
||||
}
|
||||
|
||||
function finishToolBox(div, data) {
|
||||
if (!div) return;
|
||||
div.innerHTML = '<span class="tool-name">' + esc(data.name) + '</span>'
|
||||
+ '<span class="tool-input">' + esc(truncate(data.input, 120)) + '</span>'
|
||||
+ '<div class="tool-result ' + (data.isError ? 'tool-error' : '') + '">'
|
||||
+ esc(truncate(data.result, 300)) + '</div>';
|
||||
scrollDown();
|
||||
}
|
||||
|
||||
function addToolBoxStatic(name, input, result, isError) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'tool-box';
|
||||
div.innerHTML = '<span class="tool-name">' + esc(name) + '</span>'
|
||||
+ '<span class="tool-input">' + esc(truncate(input, 120)) + '</span>';
|
||||
if (result) {
|
||||
div.innerHTML += '<div class="tool-result ' + (isError ? 'tool-error' : '') + '">'
|
||||
+ esc(truncate(result, 300)) + '</div>';
|
||||
}
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
}
|
||||
|
||||
// ── Meta Bar ──
|
||||
|
||||
function setMeta(text) {
|
||||
metaEl().textContent = text;
|
||||
}
|
||||
|
||||
function setMetaHtml(html) {
|
||||
metaEl().innerHTML = html;
|
||||
}
|
||||
Reference in new issue
Block a user