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,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; }
|
||||||
+13
-294
@@ -1,41 +1,10 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Gott - Debug Console</title>
|
<title>Gott - Debug Console</title>
|
||||||
<style>
|
<link rel="stylesheet" href="css/style.css">
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
||||||
body { font-family: monospace; background: #1a1a2e; color: #e0e0e0; display: flex; flex-direction: column; height: 100vh; }
|
|
||||||
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; }
|
|
||||||
.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 { 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%; }
|
|
||||||
.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-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; }
|
|
||||||
.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; }
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -45,276 +14,26 @@ select { font-family: monospace; padding: 6px 10px; border: 1px solid #333; bord
|
|||||||
<option value="sonnet">Sonnet 4.6</option>
|
<option value="sonnet">Sonnet 4.6</option>
|
||||||
<option value="opus">Opus 4.6</option>
|
<option value="opus">Opus 4.6</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="clear-btn" onclick="clearChat()">Clear</button>
|
<button class="clear-btn" id="clearBtn">Clear</button>
|
||||||
<div class="system-toggle">
|
<div class="system-toggle">
|
||||||
<input type="checkbox" id="showSystem" onchange="toggleSystem()">
|
<input type="checkbox" id="showSystem">
|
||||||
<label for="showSystem">System Prompt</label>
|
<label for="showSystem">System Prompt</label>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<textarea id="systemPrompt" placeholder="System Prompt (SOUL.md Inhalt etc.)"></textarea>
|
<textarea id="systemPrompt" placeholder="System Prompt (SOUL.md Inhalt etc.)"></textarea>
|
||||||
<div class="meta-bar" id="meta"></div>
|
<div class="meta-bar" id="meta"></div>
|
||||||
|
|
||||||
<div id="chat"></div>
|
<div id="chat"></div>
|
||||||
|
|
||||||
<div class="input-area">
|
<div class="input-area">
|
||||||
<textarea id="input" placeholder="Nachricht eingeben... (Ctrl+Enter)" rows="2"></textarea>
|
<textarea id="input" placeholder="Nachricht eingeben... (Ctrl+Enter)" rows="2"></textarea>
|
||||||
<button id="send" onclick="sendMessage()">Senden</button>
|
<button id="send">Senden</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script src="js/ui.js"></script>
|
||||||
const messages = [];
|
<script src="js/api.js"></script>
|
||||||
let currentAssistantDiv = null;
|
<script src="js/chat.js"></script>
|
||||||
let currentAssistantText = '';
|
<script src="js/history.js"></script>
|
||||||
|
<script src="js/app.js"></script>
|
||||||
function toggleSystem() {
|
|
||||||
document.getElementById('systemPrompt').classList.toggle('visible', document.getElementById('showSystem').checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearChat() {
|
|
||||||
messages.length = 0;
|
|
||||||
document.getElementById('chat').innerHTML = '';
|
|
||||||
document.getElementById('meta').textContent = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollDown() {
|
|
||||||
const chat = document.getElementById('chat');
|
|
||||||
chat.scrollTop = chat.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function esc(s) {
|
|
||||||
const d = document.createElement('div');
|
|
||||||
d.textContent = 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');
|
|
||||||
const meta = document.getElementById('meta');
|
|
||||||
const model = document.getElementById('model').value;
|
|
||||||
const systemPrompt = document.getElementById('systemPrompt').value.trim();
|
|
||||||
const text = input.value.trim();
|
|
||||||
if (!text) return;
|
|
||||||
|
|
||||||
messages.push({ role: 'user', content: text });
|
|
||||||
|
|
||||||
// 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 style="color:#7c83ff">⏳ Streaming...</span>';
|
|
||||||
|
|
||||||
let fullText = '';
|
|
||||||
let currentToolBox = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await fetch('/api/chat', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ model, messages, systemPrompt })
|
|
||||||
});
|
|
||||||
|
|
||||||
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) {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('input').addEventListener('keydown', e => {
|
|
||||||
if (e.ctrlKey && e.key === 'Enter') sendMessage();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('input').addEventListener('input', function() {
|
|
||||||
this.style.height = 'auto';
|
|
||||||
this.style.height = Math.min(this.scrollHeight, 150) + 'px';
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadHistory() {
|
|
||||||
try {
|
|
||||||
const resp = await fetch('/api/history?sessionId=null');
|
|
||||||
const data = await resp.json();
|
|
||||||
const chat = document.getElementById('chat');
|
|
||||||
|
|
||||||
// Chains sind DESC sortiert, also umkehren für chronologische Reihenfolge
|
|
||||||
data.reverse();
|
|
||||||
|
|
||||||
for (const entry of data) {
|
|
||||||
for (const msg of entry.messages) {
|
|
||||||
if (msg.role === 'user' && msg.type === 'text') {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.className = 'msg user';
|
|
||||||
div.textContent = msg.content;
|
|
||||||
chat.appendChild(div);
|
|
||||||
messages.push({ role: 'user', content: msg.content });
|
|
||||||
} else if (msg.role === 'assistant' && msg.type === 'text') {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.className = 'msg assistant';
|
|
||||||
div.textContent = msg.content;
|
|
||||||
chat.appendChild(div);
|
|
||||||
messages.push({ role: 'assistant', content: msg.content });
|
|
||||||
} else if (msg.role === 'assistant' && msg.type === 'tool_use') {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.className = 'tool-box';
|
|
||||||
div.innerHTML = '<span class="tool-name">' + esc(msg.toolName) + '</span>'
|
|
||||||
+ '<span class="tool-input">' + esc(truncate(msg.content, 120)) + '</span>';
|
|
||||||
chat.appendChild(div);
|
|
||||||
} else if (msg.type === 'tool_result') {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.className = 'tool-box';
|
|
||||||
div.innerHTML = '<div class="tool-result ' + (msg.isError ? 'tool-error' : '') + '">'
|
|
||||||
+ esc(truncate(msg.content, 300)) + '</div>';
|
|
||||||
chat.appendChild(div);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scrollDown();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('History laden fehlgeschlagen:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadHistory();
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -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