Initial: Debug Console mit Tool Use Loop
- Anthropic Messages API mit OAuth Token Auth - Chat Interface mit Message History - Tools: read_file, write_file, edit_file, list_files - Multi-Turn Tool Loop (max 25 Iterationen) - Steps-basiertes Response Format (Text + Tool interleaved) - Sandboxed Workspace für Dateioperationen
This commit is contained in:
commit
4531a19b39
8 files changed
+665
No files matched your search
@@ -0,0 +1,4 @@
|
||||
bin/
|
||||
obj/
|
||||
publish/
|
||||
appsettings.Production.json
|
||||
@@ -0,0 +1,23 @@
|
||||
## Info
|
||||
|
||||
- **.NET 10** without Nullable and Implicit Usings
|
||||
|
||||
## Deployment
|
||||
|
||||
- **Workspace:** `/home/node/.openclaw/workspace/projects/gott/`
|
||||
- **Build & Deploy:** `bash deploy.sh`
|
||||
- **WICHTIG:** Bei reinen Frontend-Änderungen (CSS/JS/HTML) IMMER auch `publish/wwwroot/` aktualisieren!
|
||||
|
||||
## Git
|
||||
|
||||
- **Push:** `git push`
|
||||
|
||||
## Notizen
|
||||
|
||||
**Commits:** NICHT nach jeder Nachricht committen - erst wenn Aufgabe abgeschlossen und User zufrieden ist.
|
||||
|
||||
## REGELN (NIEMALS BRECHEN)
|
||||
|
||||
- **NUR `bash deploy.sh` für Deployment.** NIEMALS `dotnet publish` direkt ausführen. Wenn deploy.sh nicht funktioniert → NACHFRAGEN!
|
||||
- **NIEMALS Dateien in `publish/data/` manuell überschreiben oder manipulieren.** Das sind Live-Daten (Sessions, Users, History). Ein falscher Schreibvorgang = Datenverlust.
|
||||
- **Keine Experimente am Live-System.** Wenn was nicht klappt, nicht "kreativ werden" sondern FRAGEN!
|
||||
+375
@@ -0,0 +1,375 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
var app = builder.Build();
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
|
||||
// Workspace-Root für Dateioperationen
|
||||
var dataDir = Path.Combine(AppContext.BaseDirectory, "data");
|
||||
var workspaceRoot = Path.Combine(dataDir, "workspace");
|
||||
Directory.CreateDirectory(workspaceRoot);
|
||||
|
||||
var toolDefs = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
name = "read_file",
|
||||
description = "Liest den Inhalt einer Datei. Gibt den Textinhalt zurück.",
|
||||
input_schema = new
|
||||
{
|
||||
type = "object",
|
||||
properties = new Dictionary<string, object>
|
||||
{
|
||||
["path"] = new { type = "string", description = "Relativer Dateipfad" }
|
||||
},
|
||||
required = new[] { "path" }
|
||||
}
|
||||
},
|
||||
new
|
||||
{
|
||||
name = "write_file",
|
||||
description = "Erstellt oder überschreibt eine Datei mit dem angegebenen Inhalt.",
|
||||
input_schema = new
|
||||
{
|
||||
type = "object",
|
||||
properties = new Dictionary<string, object>
|
||||
{
|
||||
["path"] = new { type = "string", description = "Relativer Dateipfad" },
|
||||
["content"] = new { type = "string", description = "Dateiinhalt" }
|
||||
},
|
||||
required = new[] { "path", "content" }
|
||||
}
|
||||
},
|
||||
new
|
||||
{
|
||||
name = "edit_file",
|
||||
description = "Ersetzt einen exakten Textabschnitt in einer Datei. oldText muss exakt matchen.",
|
||||
input_schema = new
|
||||
{
|
||||
type = "object",
|
||||
properties = new Dictionary<string, object>
|
||||
{
|
||||
["path"] = new { type = "string", description = "Relativer Dateipfad" },
|
||||
["old_text"] = new { type = "string", description = "Exakter Text der ersetzt werden soll" },
|
||||
["new_text"] = new { type = "string", description = "Neuer Text" }
|
||||
},
|
||||
required = new[] { "path", "old_text", "new_text" }
|
||||
}
|
||||
},
|
||||
new
|
||||
{
|
||||
name = "list_files",
|
||||
description = "Listet Dateien und Ordner in einem Verzeichnis auf.",
|
||||
input_schema = new
|
||||
{
|
||||
type = "object",
|
||||
properties = new Dictionary<string, object>
|
||||
{
|
||||
["path"] = new { type = "string", description = "Relativer Verzeichnispfad (leer = Root)" }
|
||||
},
|
||||
required = System.Array.Empty<string>()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
string ResolvePath(string relativePath)
|
||||
{
|
||||
var full = Path.GetFullPath(Path.Combine(workspaceRoot, relativePath ?? ""));
|
||||
if (!full.StartsWith(workspaceRoot))
|
||||
throw new System.Exception("Zugriff außerhalb des Workspace nicht erlaubt");
|
||||
return full;
|
||||
}
|
||||
|
||||
string ExecuteTool(string name, JsonElement input)
|
||||
{
|
||||
switch (name)
|
||||
{
|
||||
case "read_file":
|
||||
{
|
||||
var path = ResolvePath(input.GetProperty("path").GetString() ?? "");
|
||||
if (!File.Exists(path))
|
||||
return $"Fehler: Datei nicht gefunden: {Path.GetRelativePath(workspaceRoot, path)}";
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
case "write_file":
|
||||
{
|
||||
var path = ResolvePath(input.GetProperty("path").GetString() ?? "");
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
if (dir != null) Directory.CreateDirectory(dir);
|
||||
var content = input.GetProperty("content").GetString() ?? "";
|
||||
File.WriteAllText(path, content);
|
||||
return $"Datei geschrieben: {Path.GetRelativePath(workspaceRoot, path)} ({content.Length} Zeichen)";
|
||||
}
|
||||
case "edit_file":
|
||||
{
|
||||
var path = ResolvePath(input.GetProperty("path").GetString() ?? "");
|
||||
if (!File.Exists(path))
|
||||
return $"Fehler: Datei nicht gefunden: {Path.GetRelativePath(workspaceRoot, path)}";
|
||||
var fileContent = File.ReadAllText(path);
|
||||
var oldText = input.GetProperty("old_text").GetString() ?? "";
|
||||
var newText = input.GetProperty("new_text").GetString() ?? "";
|
||||
if (!fileContent.Contains(oldText))
|
||||
return "Fehler: old_text nicht in der Datei gefunden. Exakter Match nötig.";
|
||||
fileContent = fileContent.Replace(oldText, newText);
|
||||
File.WriteAllText(path, fileContent);
|
||||
return $"Datei bearbeitet: {Path.GetRelativePath(workspaceRoot, path)}";
|
||||
}
|
||||
case "list_files":
|
||||
{
|
||||
var path = ResolvePath(input.TryGetProperty("path", out var p) ? p.GetString() ?? "" : "");
|
||||
if (!Directory.Exists(path))
|
||||
return $"Fehler: Verzeichnis nicht gefunden: {Path.GetRelativePath(workspaceRoot, path)}";
|
||||
var entries = new List<string>();
|
||||
foreach (var dir in Directory.GetDirectories(path))
|
||||
entries.Add(Path.GetRelativePath(workspaceRoot, dir) + "/");
|
||||
foreach (var file in Directory.GetFiles(path))
|
||||
entries.Add(Path.GetRelativePath(workspaceRoot, file));
|
||||
return entries.Count == 0 ? "(leer)" : string.Join("\n", entries);
|
||||
}
|
||||
default:
|
||||
return $"Fehler: Unbekanntes Tool: {name}";
|
||||
}
|
||||
}
|
||||
|
||||
app.MapPost("/api/chat", async (HttpRequest req, IHttpClientFactory httpFactory, IConfiguration config) =>
|
||||
{
|
||||
var body = await JsonSerializer.DeserializeAsync<ChatRequest>(req.Body);
|
||||
if (body == null || body.Messages == null || body.Messages.Count == 0)
|
||||
return Results.BadRequest(new { error = "messages required" });
|
||||
|
||||
var token = config["AnthropicToken"];
|
||||
if (string.IsNullOrEmpty(token))
|
||||
return Results.Problem("AnthropicToken not configured");
|
||||
|
||||
var modelId = body.Model switch
|
||||
{
|
||||
"opus" => "claude-opus-4-6",
|
||||
"sonnet" => "claude-sonnet-4-6",
|
||||
_ => "claude-sonnet-4-6"
|
||||
};
|
||||
|
||||
// Messages in API-Format konvertieren
|
||||
var messages = new List<object>();
|
||||
foreach (var msg in body.Messages)
|
||||
{
|
||||
messages.Add(new { role = msg.Role, content = msg.Content });
|
||||
}
|
||||
|
||||
var systemPrompt = new List<object>
|
||||
{
|
||||
new { type = "text", text = "You are Claude Code, Anthropic's official CLI for Claude." }
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(body.SystemPrompt))
|
||||
{
|
||||
systemPrompt.Add(new { type = "text", text = body.SystemPrompt });
|
||||
}
|
||||
|
||||
var steps = new List<StepInfo>();
|
||||
long totalInputTokens = 0;
|
||||
long totalOutputTokens = 0;
|
||||
int iterations = 0;
|
||||
int maxIterations = 25;
|
||||
|
||||
while (iterations < maxIterations)
|
||||
{
|
||||
iterations++;
|
||||
|
||||
var payload = new Dictionary<string, object>
|
||||
{
|
||||
["model"] = modelId,
|
||||
["max_tokens"] = 4096,
|
||||
["system"] = systemPrompt,
|
||||
["messages"] = messages,
|
||||
["tools"] = toolDefs
|
||||
};
|
||||
|
||||
var client = httpFactory.CreateClient();
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
client.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01");
|
||||
client.DefaultRequestHeaders.Add("anthropic-beta", "claude-code-20250219,oauth-2025-04-20");
|
||||
client.DefaultRequestHeaders.Add("User-Agent", "claude-cli/2.1.2 (external, cli)");
|
||||
client.DefaultRequestHeaders.Add("x-app", "cli");
|
||||
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
client.DefaultRequestHeaders.Add("anthropic-dangerous-direct-browser-access", "true");
|
||||
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
var httpContent = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
var resp = await client.PostAsync("https://api.anthropic.com/v1/messages", httpContent);
|
||||
var respBody = await resp.Content.ReadAsStringAsync();
|
||||
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
return Results.Problem($"Anthropic API error: {(int)resp.StatusCode} - {respBody}");
|
||||
|
||||
var result = JsonSerializer.Deserialize<JsonElement>(respBody);
|
||||
|
||||
// Usage tracken
|
||||
if (result.TryGetProperty("usage", out var usage))
|
||||
{
|
||||
if (usage.TryGetProperty("input_tokens", out var it)) totalInputTokens += it.GetInt64();
|
||||
if (usage.TryGetProperty("output_tokens", out var ot)) totalOutputTokens += ot.GetInt64();
|
||||
}
|
||||
|
||||
var stopReason = result.GetProperty("stop_reason").GetString();
|
||||
var contentBlocks = result.GetProperty("content");
|
||||
|
||||
// Assistant-Antwort zur History hinzufügen
|
||||
var assistantContent = new List<object>();
|
||||
var textParts = new List<string>();
|
||||
|
||||
foreach (var block in contentBlocks.EnumerateArray())
|
||||
{
|
||||
var blockType = block.GetProperty("type").GetString();
|
||||
if (blockType == "text")
|
||||
{
|
||||
var text = block.GetProperty("text").GetString() ?? "";
|
||||
textParts.Add(text);
|
||||
assistantContent.Add(new { type = "text", text });
|
||||
}
|
||||
else if (blockType == "tool_use")
|
||||
{
|
||||
var toolId = block.GetProperty("id").GetString() ?? "";
|
||||
var toolName = block.GetProperty("name").GetString() ?? "";
|
||||
var toolInput = block.GetProperty("input");
|
||||
|
||||
assistantContent.Add(new
|
||||
{
|
||||
type = "tool_use",
|
||||
id = toolId,
|
||||
name = toolName,
|
||||
input = toolInput
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
messages.Add(new { role = "assistant", content = assistantContent });
|
||||
|
||||
// Zwischen-Text als Step erfassen
|
||||
if (textParts.Count > 0)
|
||||
{
|
||||
steps.Add(new StepInfo
|
||||
{
|
||||
Type = "text",
|
||||
Text = string.Join("\n", textParts)
|
||||
});
|
||||
}
|
||||
|
||||
// Kein Tool Use? Fertig!
|
||||
if (stopReason != "tool_use")
|
||||
{
|
||||
return Results.Ok(new
|
||||
{
|
||||
text = string.Join("\n", textParts),
|
||||
model = modelId,
|
||||
inputTokens = totalInputTokens,
|
||||
outputTokens = totalOutputTokens,
|
||||
iterations,
|
||||
steps
|
||||
});
|
||||
}
|
||||
|
||||
// Tool Calls ausführen
|
||||
var toolResults = new List<object>();
|
||||
foreach (var block in contentBlocks.EnumerateArray())
|
||||
{
|
||||
if (block.GetProperty("type").GetString() != "tool_use") continue;
|
||||
|
||||
var toolId = block.GetProperty("id").GetString() ?? "";
|
||||
var toolName = block.GetProperty("name").GetString() ?? "";
|
||||
var toolInput = block.GetProperty("input");
|
||||
|
||||
string toolResult;
|
||||
bool isError = false;
|
||||
try
|
||||
{
|
||||
toolResult = ExecuteTool(toolName, toolInput);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
toolResult = $"Fehler: {ex.Message}";
|
||||
isError = true;
|
||||
}
|
||||
|
||||
steps.Add(new StepInfo
|
||||
{
|
||||
Type = "tool",
|
||||
ToolName = toolName,
|
||||
ToolInput = toolInput.ToString(),
|
||||
ToolResult = toolResult.Length > 500 ? toolResult[..500] + "..." : toolResult,
|
||||
IsError = isError
|
||||
});
|
||||
|
||||
toolResults.Add(new
|
||||
{
|
||||
type = "tool_result",
|
||||
tool_use_id = toolId,
|
||||
content = toolResult,
|
||||
is_error = isError
|
||||
});
|
||||
}
|
||||
|
||||
messages.Add(new { role = "user", content = toolResults });
|
||||
}
|
||||
|
||||
return Results.Problem("Max iterations erreicht");
|
||||
});
|
||||
|
||||
app.Run("http://0.0.0.0:19000");
|
||||
|
||||
class ChatMessage
|
||||
{
|
||||
[JsonPropertyName("role")]
|
||||
public string Role { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; set; } = "";
|
||||
}
|
||||
|
||||
class ChatRequest
|
||||
{
|
||||
[JsonPropertyName("model")]
|
||||
public string Model { get; set; } = "sonnet";
|
||||
|
||||
[JsonPropertyName("messages")]
|
||||
public List<ChatMessage> Messages { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("systemPrompt")]
|
||||
public string SystemPrompt { get; set; } = "";
|
||||
}
|
||||
|
||||
class StepInfo
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("text")]
|
||||
public string Text { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("toolName")]
|
||||
public string ToolName { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("toolInput")]
|
||||
public string ToolInput { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("toolResult")]
|
||||
public string ToolResult { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("isError")]
|
||||
public bool IsError { get; set; }
|
||||
}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
if [ "$(hostname)" = "stein2" ]; then
|
||||
echo "stein2 is not allowed to do this"
|
||||
exit
|
||||
fi
|
||||
|
||||
export PATH="$HOME/.dotnet:$PATH"
|
||||
cd ~/.openclaw/workspace/projects/gott
|
||||
|
||||
dotnet publish -c Release -o publish
|
||||
cp wwwroot/index.html publish/wwwroot/index.html
|
||||
|
||||
ssh -o ConnectTimeout=5 meleki@192.168.100.200 "nohup sudo systemctl restart gott > /dev/null 2>&1 &"
|
||||
echo "Service restarted"
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
hostname
|
||||
echo ""
|
||||
|
||||
GIT="git -c safe.directory=*"
|
||||
|
||||
echo "Uncommited Files:"
|
||||
$GIT status --porcelain
|
||||
echo ""
|
||||
|
||||
echo -n "Outgoing Changes: "
|
||||
$GIT rev-list --count origin/$($GIT rev-parse --abbrev-ref HEAD)..HEAD
|
||||
echo ""
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<NoWarn>ASP0015;IDE0017;IDE0028;IDE0130;IDE0305;IDE1006;SYSLIB1045</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="publish/**" />
|
||||
<Content Remove="publish/**" />
|
||||
<EmbeddedResource Remove="publish/**" />
|
||||
<None Remove="publish/**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="data/**" />
|
||||
<Content Remove="data/**" />
|
||||
<EmbeddedResource Remove="data/**" />
|
||||
<None Remove="data/**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Anthropic" Version="12.8.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,198 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Gott - Debug Console</title>
|
||||
<style>
|
||||
* { 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%; }
|
||||
.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; }
|
||||
.tool-name { color: #7c83ff; font-weight: bold; }
|
||||
.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; }
|
||||
.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.visible { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<h1>// gott</h1>
|
||||
<select id="model">
|
||||
<option value="sonnet">Sonnet 4.6</option>
|
||||
<option value="opus">Opus 4.6</option>
|
||||
</select>
|
||||
<button class="clear-btn" onclick="clearChat()">Clear</button>
|
||||
<div class="system-toggle">
|
||||
<input type="checkbox" id="showSystem" onchange="toggleSystem()">
|
||||
<label for="showSystem">System Prompt</label>
|
||||
</div>
|
||||
</header>
|
||||
<textarea id="systemPrompt" placeholder="System Prompt (SOUL.md Inhalt etc.)"></textarea>
|
||||
<div class="meta-bar" id="meta"></div>
|
||||
|
||||
<div id="chat"></div>
|
||||
|
||||
<div class="input-area">
|
||||
<textarea id="input" placeholder="Nachricht eingeben... (Ctrl+Enter zum Senden)" rows="2"></textarea>
|
||||
<button id="send" onclick="sendMessage()">Senden</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const messages = [];
|
||||
|
||||
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 addMessage(role, text, toolCalls) {
|
||||
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;
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
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 });
|
||||
addMessage('user', text);
|
||||
input.value = '';
|
||||
|
||||
btn.disabled = true;
|
||||
meta.innerHTML = '<span class="spinner">⏳ Warte auf Antwort...</span>';
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
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;
|
||||
}
|
||||
} catch (e) {
|
||||
addSystemInfo('Netzwerk-Fehler: ' + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
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';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user