Avatar: Ctrl+V paste + serverseitige Skalierung auf max 256px; Tool-Boxes: Diff-View für edit/write_file, Foldout für alle Tools, stop_reason im Chat; ImageSharp 3.1.12

This commit is contained in:
Meleki committed 2026-02-27 16:46:07 +01:00
1 parent 18b2feef4a
commit e662548d79
13 files changed
+416 -42

No files matched your search

+83 -12
View File
@@ -12,11 +12,6 @@
display: none;
}
.custom-cmd-btn {
padding: var(--s1) var(--s2);
line-height: 1;
}
.custom-cmd-btn:hover {
background: var(--f3-bg);
border-color: var(--f3-border);
@@ -24,7 +19,6 @@
.custom-cmd-btn.running {
opacity: var(--semi-transparent);
cursor: wait;
}
/* Header Menu */
@@ -34,11 +28,6 @@
margin-left: auto;
}
.header-menu-trigger {
padding: var(--s1) var(--s2);
line-height: 1;
}
.header-menu-dropdown {
position: absolute;
top: 100%;
@@ -128,12 +117,94 @@
}
.tool-box {
padding: var(--s2);
align-self: flex-start;
background: var(--f1-bg);
border: var(--border-width) solid var(--f1-border);
padding: var(--s3);
}
.tool-box:has(.tool-box-header) {
padding: 0;
}
.tool-box:has([hidden]) {
opacity: var(--semi-transparent);
}
.tool-box-header {
display: flex;
align-items: center;
gap: var(--s2);
padding: var(--s3);
flex-wrap: wrap;
}
.tool-error-badge {
color: #ff6b6b;
border: var(--border-width) solid #ff6b6b;
padding: 0 var(--s1);
}
/* Diff View */
.tool-diff {
font-family: monospace;
max-height: 16.5em;
overflow: auto;
}
.diff-line {
display: flex;
align-items: flex-start;
white-space: pre;
}
.diff-removed {
color: #f33;
background: #f331
}
.diff-added {
color: #3f3;
background-color: #3f31;
}
.diff-marker {
flex-shrink: 0;
width: 1.5em;
text-align: center;
user-select: none;
border-right: var(--border-width) solid var(--f2-border);
margin-right: var(--s2);
}
.diff-content {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
padding-right: var(--s2);
}
.diff-more {
padding: var(--s1) var(--s2);
opacity: var(--semi-transparent);
font-family: monospace;
}
.tool-result {
white-space: pre-wrap;
word-break: break-word;
font-family: monospace;
padding: var(--s4);
max-height: 16em;
overflow: auto;
}
.tool-write-meta {
opacity: var(--semi-transparent);
margin-left: auto;
}
.input-area {
border-top: var(--border-width) solid var(--f1-border);
display: flex;
+4
View File
@@ -132,9 +132,13 @@ async function sendMessage() {
case 'done':
endStreamBubble();
if (data.stopReason === 'max_tokens') {
addWarningBubble('⚠️ Antwort wurde abgeschnitten (max_tokens erreicht). Bitte frage nach dem Rest.', true);
}
let meta = `Model: ${data.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`;
if (data.stopReason && data.stopReason !== 'end_turn' && data.stopReason !== 'tool_use') meta += ` | Stop: ${data.stopReason}`;
setMeta(meta);
break;
}
+15 -8
View File
@@ -47,7 +47,8 @@ function renderMessages(msgs, addToConversation) {
break;
case 'tool_result':
addToolBoxStatic(null, null, msg.content, msg.isError);
// tool_result wird dem vorherigen tool_use-Box angehängt wenn möglich
appendToolResult(msg.content, msg.isError);
break;
case 'error':
@@ -131,14 +132,20 @@ async function loadLatestArchive() {
break;
case 'tool_use':
div.className = 'tool-box archived';
div.innerHTML = '<span class="tool-name">' + esc(msg.toolName) + '</span>'
+ '<span class="tool-input">' + esc(truncate(msg.content, 120)) + '</span>';
break;
case 'tool_result':
div.className = 'tool-box archived';
div.innerHTML = '<div class="tool-result ' + (msg.isError ? 'tool-error' : '') + '">'
+ esc(truncate(msg.content, 300)) + '</div>';
renderToolBoxContent(div, msg.toolName, msg.content, null, false);
break;
case 'tool_result': {
// An vorherigen tool-box anhängen
const prevBoxes = fragment.querySelectorAll ? fragment.querySelectorAll('.tool-box') : [];
const lastToolBox = prevBoxes[prevBoxes.length - 1];
if (lastToolBox && !lastToolBox.querySelector('.tool-result')) {
const rd = document.createElement('div');
rd.className = 'tool-result' + (msg.isError ? ' tool-error' : '');
rd.textContent = truncate(msg.content, 300);
lastToolBox.appendChild(rd);
}
continue; // kein separates div
}
case 'error':
div.className = 'msg system-info archived';
div.style.color = '#ff6b6b';
+25
View File
@@ -1,6 +1,7 @@
// ── Agent Page ──
let _agentAvatarData = null;
let _agentPasteHandler = null;
registerPage('agent', {
async init(params) {
@@ -63,6 +64,26 @@ registerPage('agent', {
avatarInput.value = '';
});
// Avatar Paste (Ctrl+V)
document.addEventListener('paste', _agentPasteHandler = (e) => {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.startsWith('image/')) {
e.preventDefault();
const file = item.getAsFile();
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
_agentAvatarData = reader.result;
avatarPreview.innerHTML = `<img src="${reader.result}" alt="Avatar">`;
};
reader.readAsDataURL(file);
return;
}
}
});
// Speichern
saveBtn.addEventListener('click', async () => {
const data = {
@@ -93,6 +114,10 @@ registerPage('agent', {
});
},
destroy() {
if (_agentPasteHandler) {
document.removeEventListener('paste', _agentPasteHandler);
_agentPasteHandler = null;
}
_agentAvatarData = null;
}
});
+25
View File
@@ -1,6 +1,7 @@
// ── Session Edit Page ──
let _sessionImageData = null;
let _sessionPasteHandler = null;
function _createCommandRow(container, name = '', command = '') {
const item = document.createElement('div');
@@ -125,6 +126,26 @@ registerPage('session', {
imageInput.value = '';
});
// Bild Paste (Ctrl+V)
document.addEventListener('paste', _sessionPasteHandler = (e) => {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.startsWith('image/')) {
e.preventDefault();
const file = item.getAsFile();
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
_sessionImageData = reader.result;
imagePreview.innerHTML = `<img src="${reader.result}" alt="">`;
};
reader.readAsDataURL(file);
return;
}
}
});
// Workspace Browse
const workspaceBrowse = document.getElementById('sessionWorkspaceBrowse');
if (workspaceBrowse) {
@@ -175,6 +196,10 @@ registerPage('session', {
});
},
destroy() {
if (_sessionPasteHandler) {
document.removeEventListener('paste', _sessionPasteHandler);
_sessionPasteHandler = null;
}
_sessionImageData = null;
}
});
+193 -10
View File
@@ -58,6 +58,16 @@ function addErrorBubble(text, streamed = false) {
scrollDown();
}
function addWarningBubble(text, streamed = false) {
const div = document.createElement('div');
div.className = 'msg system-info';
if (streamed) div.classList.add('streamed');
div.style.color = '#f0a500';
div.textContent = (text || '').trim();
chatEl().appendChild(div);
scrollDown();
}
// ── Streaming Bubble ──
let _streamDiv = null;
@@ -104,28 +114,201 @@ function addToolBoxLoading(name) {
return div;
}
function renderToolBoxContent(div, name, inputJson, result, isError) {
let parsedInput = null;
try { parsedInput = JSON.parse(inputJson); } catch {}
const header = document.createElement('div');
header.className = 'tool-box-header';
const nameSpan = document.createElement('span');
nameSpan.className = 'tool-name';
nameSpan.textContent = name;
header.appendChild(nameSpan);
if (isError) {
const errSpan = document.createElement('span');
errSpan.className = 'tool-error-badge';
errSpan.textContent = 'error';
header.appendChild(errSpan);
}
div.innerHTML = '';
div.appendChild(header);
let foldTarget = null;
if (name === 'edit_file' && parsedInput && parsedInput.path) {
const pathSpan = document.createElement('span');
pathSpan.className = 'tool-input';
pathSpan.textContent = parsedInput.path;
header.appendChild(pathSpan);
const diff = buildEditDiff(parsedInput.old_text || '', parsedInput.new_text || '');
diff.hidden = true;
div.appendChild(diff);
foldTarget = diff;
} else if (name === 'write_file' && parsedInput && parsedInput.path) {
const pathSpan = document.createElement('span');
pathSpan.className = 'tool-input';
pathSpan.textContent = parsedInput.path;
header.appendChild(pathSpan);
const content = parsedInput.content || '';
const lines = content.split('\n');
const lineCount = lines.length;
const charCount = content.length;
const meta = document.createElement('span');
meta.className = 'tool-write-meta';
meta.textContent = lineCount + ' Zeilen, ' + charCount + ' Zeichen';
header.appendChild(meta);
const preview = buildWritePreview(content);
preview.hidden = true;
div.appendChild(preview);
foldTarget = preview;
} else {
// Standard: path/command als input anzeigen
let inputLabel = '';
if (parsedInput) {
inputLabel = parsedInput.path || parsedInput.command || JSON.stringify(parsedInput).substring(0, 120);
} else {
inputLabel = truncate(inputJson, 120);
}
if (inputLabel) {
const inputSpan = document.createElement('span');
inputSpan.className = 'tool-input';
inputSpan.textContent = inputLabel;
header.appendChild(inputSpan);
}
const resultDiv = document.createElement('div');
resultDiv.className = 'tool-result' + (isError ? ' tool-error' : '');
resultDiv.textContent = result ? truncate(result, 300) : '';
resultDiv.hidden = true;
div.appendChild(resultDiv);
foldTarget = resultDiv;
}
if (foldTarget) {
header.style.cursor = 'pointer';
header.addEventListener('click', () => {
foldTarget.hidden = !foldTarget.hidden;
});
}
}
function buildEditDiff(oldText, newText) {
const oldLines = oldText.split('\n');
const newLines = newText.split('\n');
const container = document.createElement('div');
container.className = 'tool-diff';
// Entfernte Zeilen (-)
for (const line of oldLines) {
const row = document.createElement('div');
row.className = 'diff-line diff-removed';
const marker = document.createElement('span');
marker.className = 'diff-marker';
marker.textContent = '-';
const content = document.createElement('span');
content.className = 'diff-content';
content.textContent = line;
row.appendChild(marker);
row.appendChild(content);
container.appendChild(row);
}
// Hinzugefügte Zeilen (+)
for (const line of newLines) {
const row = document.createElement('div');
row.className = 'diff-line diff-added';
const marker = document.createElement('span');
marker.className = 'diff-marker';
marker.textContent = '+';
const content = document.createElement('span');
content.className = 'diff-content';
content.textContent = line;
row.appendChild(marker);
row.appendChild(content);
container.appendChild(row);
}
return container;
}
function buildWritePreview(content) {
const lines = content.split('\n');
const maxLines = 20;
const shown = lines.slice(0, maxLines);
const container = document.createElement('div');
container.className = 'tool-diff';
for (const line of shown) {
const row = document.createElement('div');
row.className = 'diff-line diff-added';
const marker = document.createElement('span');
marker.className = 'diff-marker';
marker.textContent = '+';
const contentSpan = document.createElement('span');
contentSpan.className = 'diff-content';
contentSpan.textContent = line;
row.appendChild(marker);
row.appendChild(contentSpan);
container.appendChild(row);
}
if (lines.length > maxLines) {
const more = document.createElement('div');
more.className = 'diff-more';
more.textContent = '… ' + (lines.length - maxLines) + ' weitere Zeilen';
container.appendChild(more);
}
return container;
}
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>';
renderToolBoxContent(div, data.name, data.input, data.result, data.isError);
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>';
}
renderToolBoxContent(div, name, input, result, isError);
chatEl().appendChild(div);
scrollDown();
}
function appendToolResult(result, isError) {
// Letzten tool-box im Chat finden und Result in die bereits vorhandene .tool-result-Div schreiben
const boxes = chatEl().querySelectorAll('.tool-box');
const lastBox = boxes[boxes.length - 1];
if (!lastBox) return;
// Wenn Diff vorhanden (edit_file/write_file) → kein Result anhängen
if (lastBox.querySelector('.tool-diff')) return;
const existing = lastBox.querySelector('.tool-result');
if (existing) {
existing.textContent = truncate(result, 300);
if (isError) existing.classList.add('tool-error');
} else {
const resultDiv = document.createElement('div');
resultDiv.className = 'tool-result' + (isError ? ' tool-error' : '');
resultDiv.textContent = truncate(result, 300);
resultDiv.hidden = true;
lastBox.appendChild(resultDiv);
}
}
// ── Streamed Elements ──
function removeStreamedElements() {