SSE Streaming, Tool-Loop Refactor, Hilfsfunktionen extrahiert

This commit is contained in:
Meleki committed 2026-02-26 14:22:55 +01:00
1 parent d32b637760
commit 6ccd8cdb06
2 files changed
+374 -205

No files matched your search

+158 -82
View File
@@ -16,23 +16,24 @@ select { font-family: monospace; padding: 6px 10px; border: 1px solid #333; bord
.msg.user { align-self: flex-end; background: #16213e; border: 1px solid #333; }
.msg.assistant { align-self: flex-start; background: #0f3460; border: 1px solid #1a4a7a; }
.msg.system-info { align-self: center; background: #1a1a2e; border: 1px solid #333; color: #888; font-size: 0.85em; text-align: center; max-width: 95%; }
.tool-calls { margin-top: 8px; padding-top: 8px; border-top: 1px solid #1a4a7a; }
.tool-call { margin: 4px 0; padding: 6px 8px; background: #0a1e3d; border-radius: 4px; font-size: 0.85em; }
.msg .cursor { display: inline-block; width: 2px; height: 1em; background: #7c83ff; animation: blink 0.8s infinite; vertical-align: text-bottom; margin-left: 1px; }
@keyframes blink { 0%,50% { opacity: 1; } 51%,100% { opacity: 0; } }
.tool-box { align-self: center; max-width: 95%; padding: 8px 12px; border-radius: 6px; background: #0a1e3d; border: 1px solid #1a3a5c; font-size: 0.85em; }
.tool-name { color: #7c83ff; font-weight: bold; }
.tool-input { color: #888; margin-left: 6px; font-size: 0.85em; }
.tool-result { color: #6bff8a; font-size: 0.8em; margin-top: 4px; max-height: 100px; overflow-y: auto; }
.tool-error { color: #ff6b6b; }
.tool-input { color: #888; margin-left: 8px; }
.tool-result { color: #6bff8a; font-size: 0.8em; margin-top: 2px; max-height: 100px; overflow-y: auto; }
.tool-spinner { color: #7c83ff; }
.input-area { padding: 12px 20px; border-top: 1px solid #333; display: flex; gap: 8px; flex-shrink: 0; }
.input-area textarea { flex: 1; font-family: monospace; padding: 10px; border: 1px solid #333; border-radius: 4px; background: #0f3460; color: #e0e0e0; resize: none; font-size: 14px; min-height: 60px; max-height: 150px; }
.input-area button { padding: 10px 20px; background: #7c83ff; color: #fff; border: none; border-radius: 4px; font-family: monospace; font-weight: bold; cursor: pointer; align-self: flex-end; }
.input-area button:hover { background: #5a63e0; }
.input-area button:disabled { opacity: 0.5; cursor: not-allowed; }
.spinner { color: #7c83ff; }
.clear-btn { padding: 6px 10px; background: #333; color: #aaa; border: none; border-radius: 4px; cursor: pointer; font-family: monospace; font-size: 0.85em; }
.clear-btn:hover { background: #444; }
.system-toggle { display: flex; align-items: center; gap: 6px; margin-left: auto; }
.system-toggle label { font-size: 0.85em; color: #888; cursor: pointer; }
#systemPrompt { display: none; width: 100%; font-family: monospace; padding: 8px; border: 1px solid #333; border-radius: 4px; background: #0a0a1a; color: #e0e0e0; resize: vertical; font-size: 12px; min-height: 40px; margin: 0 20px; }
#systemPrompt { display: none; width: calc(100% - 40px); font-family: monospace; padding: 8px; border: 1px solid #333; border-radius: 4px; background: #0a0a1a; color: #e0e0e0; resize: vertical; font-size: 12px; min-height: 40px; margin: 8px 20px 0; }
#systemPrompt.visible { display: block; }
</style>
</head>
@@ -56,12 +57,14 @@ select { font-family: monospace; padding: 6px 10px; border: 1px solid #333; bord
<div id="chat"></div>
<div class="input-area">
<textarea id="input" placeholder="Nachricht eingeben... (Ctrl+Enter zum Senden)" rows="2"></textarea>
<textarea id="input" placeholder="Nachricht eingeben... (Ctrl+Enter)" rows="2"></textarea>
<button id="send" onclick="sendMessage()">Senden</button>
</div>
<script>
const messages = [];
let currentAssistantDiv = null;
let currentAssistantText = '';
function toggleSystem() {
document.getElementById('systemPrompt').classList.toggle('visible', document.getElementById('showSystem').checked);
@@ -73,66 +76,8 @@ function clearChat() {
document.getElementById('meta').textContent = '';
}
function addMessage(role, text, toolCalls) {
function scrollDown() {
const chat = document.getElementById('chat');
const div = document.createElement('div');
div.className = 'msg ' + role;
div.textContent = text;
if (toolCalls && toolCalls.length > 0) {
const tc = document.createElement('div');
tc.className = 'tool-calls';
tc.innerHTML = '<div style="color:#888;font-size:0.8em;margin-bottom:4px;">Tool Calls (' + toolCalls.length + '):</div>';
for (const call of toolCalls) {
const d = document.createElement('div');
d.className = 'tool-call';
d.innerHTML = '<span class="tool-name">' + esc(call.name) + '</span>'
+ '<span class="tool-input">' + esc(call.input) + '</span>'
+ '<div class="tool-result ' + (call.isError ? 'tool-error' : '') + '">' + esc(call.result) + '</div>';
tc.appendChild(d);
}
div.appendChild(tc);
}
chat.appendChild(div);
chat.scrollTop = chat.scrollHeight;
}
function addSystemInfo(text) {
const chat = document.getElementById('chat');
const div = document.createElement('div');
div.className = 'msg system-info';
div.textContent = text;
chat.appendChild(div);
chat.scrollTop = chat.scrollHeight;
}
function renderSteps(steps) {
const chat = document.getElementById('chat');
// Gruppiere: text-steps und tool-steps zwischen texts zusammenfassen
let currentDiv = null;
let hasText = false;
for (const step of steps) {
if (step.type === 'text' && step.text) {
const div = document.createElement('div');
div.className = 'msg assistant';
div.textContent = step.text;
chat.appendChild(div);
currentDiv = div;
hasText = true;
} else if (step.type === 'tool') {
// Tool-Step als kompaktes Info-Element
const div = document.createElement('div');
div.className = 'msg system-info';
div.innerHTML = '<span class="tool-name">' + esc(step.toolName) + '</span>'
+ ' <span class="tool-input">' + esc(step.toolInput) + '</span>'
+ '<div class="tool-result ' + (step.isError ? 'tool-error' : '') + '">' + esc(step.toolResult) + '</div>';
chat.appendChild(div);
}
}
// Falls kein Text-Step am Ende war (nur tools), leeren assistant-msg anzeigen vermeiden
chat.scrollTop = chat.scrollHeight;
}
@@ -142,6 +87,64 @@ function esc(s) {
return d.innerHTML;
}
function startAssistantBubble() {
const chat = document.getElementById('chat');
currentAssistantDiv = document.createElement('div');
currentAssistantDiv.className = 'msg assistant';
currentAssistantDiv.innerHTML = '<span class="cursor"></span>';
currentAssistantText = '';
chat.appendChild(currentAssistantDiv);
scrollDown();
}
function appendToAssistant(text) {
if (!currentAssistantDiv) startAssistantBubble();
currentAssistantText += text;
currentAssistantDiv.textContent = currentAssistantText;
// Cursor wieder anfügen
const cursor = document.createElement('span');
cursor.className = 'cursor';
currentAssistantDiv.appendChild(cursor);
scrollDown();
}
function endAssistantBubble() {
if (currentAssistantDiv) {
// Cursor entfernen
const cursor = currentAssistantDiv.querySelector('.cursor');
if (cursor) cursor.remove();
// Leere Bubbles entfernen
if (!currentAssistantText.trim()) {
currentAssistantDiv.remove();
}
}
currentAssistantDiv = null;
currentAssistantText = '';
}
function addToolBox(name, status) {
const chat = document.getElementById('chat');
const div = document.createElement('div');
div.className = 'tool-box';
div.id = 'tool-' + Date.now();
div.innerHTML = '<span class="tool-name">' + esc(name) + '</span> <span class="tool-spinner">⏳</span>';
chat.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 truncate(s, max) {
return s && s.length > max ? s.substring(0, max) + '...' : (s || '');
}
async function sendMessage() {
const input = document.getElementById('input');
const btn = document.getElementById('send');
@@ -152,11 +155,21 @@ async function sendMessage() {
if (!text) return;
messages.push({ role: 'user', content: text });
addMessage('user', text);
input.value = '';
// User-Bubble
const chat = document.getElementById('chat');
const userDiv = document.createElement('div');
userDiv.className = 'msg user';
userDiv.textContent = text;
chat.appendChild(userDiv);
scrollDown();
input.value = '';
btn.disabled = true;
meta.innerHTML = '<span class="spinner">⏳ Warte auf Antwort...</span>';
meta.innerHTML = '<span style="color:#7c83ff">⏳ Streaming...</span>';
let fullText = '';
let currentToolBox = null;
try {
const resp = await fetch('/api/chat', {
@@ -164,23 +177,87 @@ async function sendMessage() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model, messages, systemPrompt })
});
const data = await resp.json();
if (!resp.ok) {
addSystemInfo('Fehler: ' + (data.detail || JSON.stringify(data)));
} else {
messages.push({ role: 'assistant', content: data.text });
renderSteps(data.steps || []);
const toolCount = (data.steps || []).filter(s => s.type === 'tool').length;
let metaText = `Model: ${data.model} | In: ${data.inputTokens} | Out: ${data.outputTokens}`;
if (data.iterations > 1) metaText += ` | Iterations: ${data.iterations}`;
if (toolCount > 0) metaText += ` | Tools: ${toolCount}`;
meta.textContent = metaText;
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
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: ')) {
var 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':
endAssistantBubble();
startAssistantBubble();
break;
case 'text_delta':
appendToAssistant(data.text);
fullText += data.text;
break;
case 'text_end':
endAssistantBubble();
break;
case 'tool_start':
endAssistantBubble();
currentToolBox = addToolBox(data.name);
break;
case 'tool_end':
finishToolBox(currentToolBox, data);
currentToolBox = null;
break;
case 'error':
endAssistantBubble();
const errDiv = document.createElement('div');
errDiv.className = 'msg system-info';
errDiv.style.color = '#ff6b6b';
errDiv.textContent = data.error;
chat.appendChild(errDiv);
scrollDown();
break;
case 'done':
endAssistantBubble();
let metaText = `Model: ${data.model || model} | In: ${data.inputTokens || 0} | Out: ${data.outputTokens || 0}`;
if (data.iterations > 1) metaText += ` | Iterations: ${data.iterations}`;
meta.textContent = metaText;
break;
}
eventType = null;
}
}
}
// Assistent-Text zur History
if (fullText) {
messages.push({ role: 'assistant', content: fullText });
}
} catch (e) {
addSystemInfo('Netzwerk-Fehler: ' + e.message);
endAssistantBubble();
const errDiv = document.createElement('div');
errDiv.className = 'msg system-info';
errDiv.textContent = 'Netzwerk-Fehler: ' + e.message;
chat.appendChild(errDiv);
} finally {
btn.disabled = false;
endAssistantBubble();
}
}
@@ -188,7 +265,6 @@ document.getElementById('input').addEventListener('keydown', e => {
if (e.ctrlKey && e.key === 'Enter') sendMessage();
});
// Auto-resize textarea
document.getElementById('input').addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 150) + 'px';