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:
1 parent
18b2feef4a
commit
e662548d79
13 files changed
+416
-42
No files matched your search
@@ -36,14 +36,16 @@ public static class AgentEndpoint
|
||||
await ctx.Response.WriteAsJsonAsync(agent);
|
||||
}
|
||||
|
||||
private static async Task HandleCreate(HttpContext ctx, DatabaseService db)
|
||||
private static async Task HandleCreate(HttpContext ctx, DatabaseService db, ImageService imageService)
|
||||
{
|
||||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||||
|
||||
var avatarRaw = body.TryGetProperty("avatar", out var av) ? av.GetString() : null;
|
||||
|
||||
var agent = new AgentRecord
|
||||
{
|
||||
Name = body.TryGetProperty("name", out var n) ? n.GetString() ?? "Neuer Agent" : "Neuer Agent",
|
||||
Avatar = body.TryGetProperty("avatar", out var av) ? av.GetString() : null,
|
||||
Avatar = imageService.ResizeDataUrl(avatarRaw),
|
||||
Description = body.TryGetProperty("description", out var d) ? d.GetString() : null,
|
||||
Model = body.TryGetProperty("model", out var m) ? m.GetString() ?? "sonnet" : "sonnet",
|
||||
Persona = body.TryGetProperty("persona", out var p) ? p.GetString() : null,
|
||||
@@ -54,7 +56,7 @@ public static class AgentEndpoint
|
||||
await ctx.Response.WriteAsJsonAsync(agent);
|
||||
}
|
||||
|
||||
private static async Task HandleUpdate(HttpContext ctx, DatabaseService db)
|
||||
private static async Task HandleUpdate(HttpContext ctx, DatabaseService db, ImageService imageService)
|
||||
{
|
||||
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
|
||||
if (string.IsNullOrEmpty(agentId)) { ctx.Response.StatusCode = 400; return; }
|
||||
@@ -65,7 +67,7 @@ public static class AgentEndpoint
|
||||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||||
|
||||
if (body.TryGetProperty("name", out var n)) agent.Name = n.GetString();
|
||||
if (body.TryGetProperty("avatar", out var av)) agent.Avatar = av.GetString();
|
||||
if (body.TryGetProperty("avatar", out var av)) agent.Avatar = imageService.ResizeDataUrl(av.GetString());
|
||||
if (body.TryGetProperty("description", out var d)) agent.Description = d.GetString();
|
||||
if (body.TryGetProperty("model", out var m)) agent.Model = m.GetString();
|
||||
if (body.TryGetProperty("persona", out var p)) agent.Persona = p.GetString();
|
||||
|
||||
@@ -47,13 +47,14 @@ public static class SessionEndpoint
|
||||
await ctx.Response.WriteAsJsonAsync(session);
|
||||
}
|
||||
|
||||
private static async Task HandleCreate(HttpContext ctx, DatabaseService db)
|
||||
private static async Task HandleCreate(HttpContext ctx, DatabaseService db, ImageService imageService)
|
||||
{
|
||||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||||
|
||||
var name = body.TryGetProperty("name", out var n) ? n.GetString() : "Neue Session";
|
||||
var description = body.TryGetProperty("description", out var d) ? d.GetString() : null;
|
||||
var image = body.TryGetProperty("image", out var img) ? img.GetString() : null;
|
||||
var imageRaw = body.TryGetProperty("image", out var img) ? img.GetString() : null;
|
||||
var image = imageService.ResizeDataUrl(imageRaw);
|
||||
var agentId = body.TryGetProperty("agentId", out var a) ? a.GetString() : null;
|
||||
var workspace = body.TryGetProperty("workspace", out var w) ? w.GetString() : null;
|
||||
var context = body.TryGetProperty("context", out var ctxEl) ? ctxEl.GetString() : null;
|
||||
@@ -63,7 +64,7 @@ public static class SessionEndpoint
|
||||
await ctx.Response.WriteAsJsonAsync(session);
|
||||
}
|
||||
|
||||
private static async Task HandleUpdate(HttpContext ctx, DatabaseService db)
|
||||
private static async Task HandleUpdate(HttpContext ctx, DatabaseService db, ImageService imageService)
|
||||
{
|
||||
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
||||
if (string.IsNullOrEmpty(sessionId))
|
||||
@@ -73,7 +74,7 @@ public static class SessionEndpoint
|
||||
}
|
||||
|
||||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||||
await db.UpdateSession(sessionId, body);
|
||||
await db.UpdateSession(sessionId, body, imageService);
|
||||
|
||||
var session = await db.GetSession(sessionId);
|
||||
await ctx.Response.WriteAsJsonAsync(session);
|
||||
@@ -183,7 +184,7 @@ public static class SessionEndpoint
|
||||
var process = Process.Start(psi);
|
||||
if (process != null)
|
||||
{
|
||||
process.WaitForExit(5000);
|
||||
process.WaitForExit(500);
|
||||
}
|
||||
|
||||
ctx.Response.StatusCode = 202;
|
||||
|
||||
@@ -11,6 +11,7 @@ builder.Services.AddHttpClient();
|
||||
var dataDir = Path.Combine(AppContext.BaseDirectory, "data");
|
||||
|
||||
builder.Services.AddSingleton(new ToolService());
|
||||
builder.Services.AddSingleton(new ImageService());
|
||||
builder.Services.AddSingleton(new DatabaseService(Path.Combine(dataDir, "gott.db")));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
@@ -146,7 +146,8 @@ public class AnthropicService
|
||||
outputTokens = usage.OutputTokens,
|
||||
iterations,
|
||||
chainId,
|
||||
durationMs = sw.ElapsedMilliseconds
|
||||
durationMs = sw.ElapsedMilliseconds,
|
||||
stopReason
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ public class DatabaseService
|
||||
new { sessionId, now = DateTime.UtcNow.ToString("o") });
|
||||
}
|
||||
|
||||
public async Task UpdateSession(string sessionId, System.Text.Json.JsonElement body)
|
||||
public async Task UpdateSession(string sessionId, System.Text.Json.JsonElement body, ImageService imageService = null)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
var sets = new List<string> { "updated_at = @now" };
|
||||
@@ -239,7 +239,13 @@ public class DatabaseService
|
||||
|
||||
if (body.TryGetProperty("name", out var n)) { sets.Add("name = @name"); p.Add("name", n.GetString()); }
|
||||
if (body.TryGetProperty("description", out var d)) { sets.Add("description = @desc"); p.Add("desc", d.GetString()); }
|
||||
if (body.TryGetProperty("image", out var img)) { sets.Add("image = @image"); p.Add("image", img.GetString()); }
|
||||
if (body.TryGetProperty("image", out var img))
|
||||
{
|
||||
var imageData = img.GetString();
|
||||
if (imageService != null) imageData = imageService.ResizeDataUrl(imageData);
|
||||
sets.Add("image = @image");
|
||||
p.Add("image", imageData);
|
||||
}
|
||||
if (body.TryGetProperty("agentId", out var a)) { sets.Add("agent_id = @agentId2"); p.Add("agentId2", a.GetString()); }
|
||||
if (body.TryGetProperty("workspace", out var w)) { sets.Add("workspace = @workspace"); p.Add("workspace", w.GetString()); }
|
||||
if (body.TryGetProperty("context", out var ctx)) { sets.Add("context = @context"); p.Add("context", ctx.GetString()); }
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Png;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace Gott.Services;
|
||||
|
||||
public class ImageService
|
||||
{
|
||||
private const int MaxSideLength = 512;
|
||||
|
||||
/// <summary>
|
||||
/// Nimmt eine Base64-Data-URL (z.B. "data:image/png;base64,..."), skaliert das Bild
|
||||
/// auf max 512px Seitenlänge und gibt eine neue Data-URL zurück.
|
||||
/// Gibt null zurück wenn input null/leer ist.
|
||||
/// </summary>
|
||||
public string ResizeDataUrl(string dataUrl)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dataUrl)) return null;
|
||||
|
||||
// Data-URL parsen: "data:<mediaType>;base64,<data>"
|
||||
var commaIndex = dataUrl.IndexOf(',');
|
||||
if (commaIndex < 0) return dataUrl;
|
||||
|
||||
var base64 = dataUrl.Substring(commaIndex + 1);
|
||||
var bytes = Convert.FromBase64String(base64);
|
||||
|
||||
using var image = Image.Load(bytes);
|
||||
|
||||
// Nur skalieren wenn nötig
|
||||
if (image.Width <= MaxSideLength && image.Height <= MaxSideLength)
|
||||
return dataUrl;
|
||||
|
||||
image.Mutate(x => x.Resize(new ResizeOptions
|
||||
{
|
||||
Size = new Size(MaxSideLength, MaxSideLength),
|
||||
Mode = ResizeMode.Max
|
||||
}));
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
image.Save(ms, new PngEncoder());
|
||||
var resizedBase64 = Convert.ToBase64String(ms.ToArray());
|
||||
|
||||
return "data:image/png;base64," + resizedBase64;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
<PackageReference Include="Anthropic" Version="12.8.0" />
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.3" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+83
-12
@@ -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;
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user