From 19f926da05ad3c55614cf2c4bb2e00ff3f8287aa Mon Sep 17 00:00:00 2001 From: Meleki Date: Thu, 26 Feb 2026 15:43:10 +0100 Subject: [PATCH] Frontend Refactoring: HTML/CSS/JS getrennt, modulare Struktur --- wwwroot/css/style.css | 206 ++++++++++++++++++++++++++++ wwwroot/index.html | 307 ++---------------------------------------- wwwroot/js/api.js | 14 ++ wwwroot/js/app.js | 28 ++++ wwwroot/js/chat.js | 108 +++++++++++++++ wwwroot/js/history.js | 40 ++++++ wwwroot/js/ui.js | 125 +++++++++++++++++ 7 files changed, 534 insertions(+), 294 deletions(-) create mode 100644 wwwroot/css/style.css create mode 100644 wwwroot/js/api.js create mode 100644 wwwroot/js/app.js create mode 100644 wwwroot/js/chat.js create mode 100644 wwwroot/js/history.js create mode 100644 wwwroot/js/ui.js diff --git a/wwwroot/css/style.css b/wwwroot/css/style.css new file mode 100644 index 0000000..01a2538 --- /dev/null +++ b/wwwroot/css/style.css @@ -0,0 +1,206 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: monospace; + background: #1a1a2e; + color: #e0e0e0; + display: flex; + flex-direction: column; + height: 100vh; +} + +/* ── Header ── */ + +header { + padding: 12px 20px; + border-bottom: 1px solid #333; + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; +} + +header h1 { + color: #7c83ff; + font-size: 1.2em; +} + +select { + font-family: monospace; + padding: 6px 10px; + border: 1px solid #333; + border-radius: 4px; + background: #16213e; + color: #e0e0e0; +} + +.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: 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; } + +/* ── Meta Bar ── */ + +.meta-bar { + padding: 4px 20px; + background: #0a0a1a; + border-bottom: 1px solid #222; + font-size: 0.8em; + color: #666; + flex-shrink: 0; + min-height: 24px; +} + +/* ── Chat ── */ + +#chat { + flex: 1; + overflow-y: auto; + padding: 16px 20px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.msg { + max-width: 85%; + padding: 10px 14px; + border-radius: 8px; + white-space: pre-wrap; + word-break: break-word; + line-height: 1.5; +} + +.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%; +} + +/* ── Cursor ── */ + +.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 Boxes ── */ + +.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-spinner { color: #7c83ff; } + +/* ── Input Area ── */ + +.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; } diff --git a/wwwroot/index.html b/wwwroot/index.html index 260c7a3..12150c6 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -1,41 +1,10 @@ - - -Gott - Debug Console - + + + Gott - Debug Console + @@ -45,276 +14,26 @@ select { font-family: monospace; padding: 6px 10px; border: 1px solid #333; bord - +
- +
+
-
- +
- + + + + + diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js new file mode 100644 index 0000000..d36a71c --- /dev/null +++ b/wwwroot/js/api.js @@ -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(); +} diff --git a/wwwroot/js/app.js b/wwwroot/js/app.js new file mode 100644 index 0000000..2694ffc --- /dev/null +++ b/wwwroot/js/app.js @@ -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(); +}); diff --git a/wwwroot/js/chat.js b/wwwroot/js/chat.js new file mode 100644 index 0000000..f3fb416 --- /dev/null +++ b/wwwroot/js/chat.js @@ -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('⏳ Streaming...'); + + 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(); + } +} diff --git a/wwwroot/js/history.js b/wwwroot/js/history.js new file mode 100644 index 0000000..3f02b3b --- /dev/null +++ b/wwwroot/js/history.js @@ -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); + } +} diff --git a/wwwroot/js/ui.js b/wwwroot/js/ui.js new file mode 100644 index 0000000..2136379 --- /dev/null +++ b/wwwroot/js/ui.js @@ -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 = ''; + _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 = '' + esc(name) + ' ⏳'; + chatEl().appendChild(div); + scrollDown(); + return div; +} + +function finishToolBox(div, data) { + if (!div) return; + div.innerHTML = '' + esc(data.name) + '' + + '' + esc(truncate(data.input, 120)) + '' + + '
' + + esc(truncate(data.result, 300)) + '
'; + scrollDown(); +} + +function addToolBoxStatic(name, input, result, isError) { + const div = document.createElement('div'); + div.className = 'tool-box'; + div.innerHTML = '' + esc(name) + '' + + '' + esc(truncate(input, 120)) + ''; + if (result) { + div.innerHTML += '
' + + esc(truncate(result, 300)) + '
'; + } + chatEl().appendChild(div); + scrollDown(); +} + +// ── Meta Bar ── + +function setMeta(text) { + metaEl().textContent = text; +} + +function setMetaHtml(html) { + metaEl().innerHTML = html; +}