From 5fe3f8ac87fd92dd4268ec668baba1b64026b8cf Mon Sep 17 00:00:00 2001 From: Meleki Date: Fri, 6 Mar 2026 16:04:00 +0100 Subject: [PATCH] feat: Review-Agent (Multi-Agent-Kommunikation) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Session hat optionalen ReviewAgentId + MaxReviewIterations - Nach end_turn des Worker-Agents wird der Review-Agent aufgerufen - Review-Agent bekommt Worker-Konversation als Kontext + kann Tools nutzen - [APPROVED] in Review-Antwort → fertig, sonst Feedback an Worker - Worker korrigiert und wird erneut reviewed (max N Runden) - SSE-Events: review_start, review_text, review_tool_use, review_feedback, review_approved - Chat-UI: Review-Phase als eigene Bubble mit Header, Text, Status - Session-Page: Review-Agent-Picker (gleicher Agent-Card-Helper) - Dashboard: Review-Agent-Badge auf Session-Cards - DB-Migration: review_agent_id, max_review_iterations auf sessions --- Endpoints/ChatEndpoint.cs | 2 +- Endpoints/SessionEndpoint.cs | 4 +- Models/ChatContext.cs | 16 ++ Models/SessionRecord.cs | 2 + Services/AnthropicService.cs | 352 +++++++++++++++++++++++++++++++ Services/ChatService.cs | 39 +++- Services/DatabaseService.cs | 14 +- wwwroot/css/chat.css | 71 +++++++ wwwroot/index.html | 9 + wwwroot/js/chat.js | 21 ++ wwwroot/js/pages/session-page.js | 51 +++-- wwwroot/js/ui.js | 56 +++++ 12 files changed, 619 insertions(+), 18 deletions(-) diff --git a/Endpoints/ChatEndpoint.cs b/Endpoints/ChatEndpoint.cs index 0cb54af..6ae6db9 100644 --- a/Endpoints/ChatEndpoint.cs +++ b/Endpoints/ChatEndpoint.cs @@ -85,7 +85,7 @@ public static class ChatEndpoint try { - await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: ctx.ToolChoice, cancellationToken: linkedCts.Token); + await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: ctx.ToolChoice, review: ctx.Review, cancellationToken: linkedCts.Token); } finally { diff --git a/Endpoints/SessionEndpoint.cs b/Endpoints/SessionEndpoint.cs index 46c4f8d..2823006 100644 --- a/Endpoints/SessionEndpoint.cs +++ b/Endpoints/SessionEndpoint.cs @@ -77,8 +77,10 @@ public static class SessionEndpoint var context = body.TryGetProperty("context", out var ctxEl) ? ctxEl.GetString() : null; var customCommands = body.TryGetProperty("customCommands", out var cc) && cc.ValueKind != JsonValueKind.Null ? cc.GetRawText() : null; var avatarScale = body.TryGetProperty("avatarScale", out var asc) ? asc.GetDouble() : 1.0; + var reviewAgentId = body.TryGetProperty("reviewAgentId", out var ra) ? ra.GetString() : null; + var maxReviewIterations = body.TryGetProperty("maxReviewIterations", out var mri) ? mri.GetInt32() : 3; - var session = await db.CreateSession(userId, name, description, image, agentId, workspace, context, customCommands, avatarScale); + var session = await db.CreateSession(userId, name, description, image, agentId, workspace, context, customCommands, avatarScale, reviewAgentId, maxReviewIterations); await ctx.Response.WriteAsJsonAsync(session); } diff --git a/Models/ChatContext.cs b/Models/ChatContext.cs index 880ede4..dc87865 100644 --- a/Models/ChatContext.cs +++ b/Models/ChatContext.cs @@ -17,4 +17,20 @@ public class ChatContext public int MaxIterations { get; set; } public AgentRecord Agent { get; set; } public SessionRecord Session { get; set; } + + // Review-Agent + public ReviewContext Review { get; set; } +} + +public class ReviewContext +{ + public string ModelId { get; set; } + public List SystemPrompt { get; set; } + public string AgentId { get; set; } + public string Workspace { get; set; } + public bool UseMemory { get; set; } + public string WebSearchVersion { get; set; } + public string ToolChoice { get; set; } + public int MaxReviewIterations { get; set; } + public AgentRecord Agent { get; set; } } diff --git a/Models/SessionRecord.cs b/Models/SessionRecord.cs index 85598ec..0f0f594 100644 --- a/Models/SessionRecord.cs +++ b/Models/SessionRecord.cs @@ -18,6 +18,8 @@ public class SessionRecord public string TelegramChatId { get; set; } public string TelegramAllowedChatIds { get; set; } public double AvatarScale { get; set; } = 1.0; + public string ReviewAgentId { get; set; } + public int MaxReviewIterations { get; set; } = 3; public string CreatedAt { get; set; } public string UpdatedAt { get; set; } } diff --git a/Services/AnthropicService.cs b/Services/AnthropicService.cs index cef42f0..92364df 100644 --- a/Services/AnthropicService.cs +++ b/Services/AnthropicService.cs @@ -47,11 +47,13 @@ public class AnthropicService string webSearchVersion = null, int maxIterations = 100, string toolChoice = "any", + ReviewContext review = null, CancellationToken cancellationToken = default) { var sw = System.Diagnostics.Stopwatch.StartNew(); var usage = new TokenUsage(); int iterations = 0; + int reviewRound = 0; string lastStopReason = ""; var chain = new ChainRecord @@ -175,6 +177,31 @@ public class AnthropicService if (stopReason != "tool_use") { + // ── Review-Agent Phase ── + if (review != null && stopReason == "end_turn" && reviewRound < review.MaxReviewIterations) + { + var reviewResult = await RunReview(response, review, messages, chainId, workspace, sessionId, usage, iterations, reviewRound, cancellationToken); + iterations += reviewResult.ReviewIterationCount; + reviewRound++; + + if (reviewResult.HasFeedback) + { + // Feedback als User-Message an Worker zurückgeben → nächste Iteration + messages.Add(new Dictionary + { + ["role"] = "user", + ["content"] = new List { new { type = "text", text = reviewResult.Feedback } } + }); + await _db.SaveMessage(new MessageRecord + { + ChainId = chainId, Iteration = iterations, + Role = "user", Type = "text", + Content = reviewResult.Feedback + }); + continue; // → nächste Worker-Iteration + } + } + var finalStopReason = string.IsNullOrEmpty(stopReason) ? "unexpected_end" : stopReason; await FinalizeChain(chain, usage, iterations, finalStopReason, sw, streamErrorInfo); await TryWriteSse(response, "done", new @@ -242,6 +269,331 @@ public class AnthropicService } } + // ── Review-Agent Logik ── + + private class ReviewResult + { + public bool HasFeedback { get; set; } + public string Feedback { get; set; } + public int ReviewIterationCount { get; set; } + } + + /// + /// Führt den Review-Agent aus. Der Review-Agent bekommt die gesamte Konversation + /// und kann Tools nutzen (z.B. read_file um die Arbeit zu prüfen). + /// Antwortet er mit [APPROVED] → fertig. Sonst → Feedback wird an den Worker zurückgegeben. + /// reviewRound = aktuelle Runde (0-basiert), wird vom Caller hochgezählt. + /// + private async Task RunReview( + HttpResponse response, + ReviewContext review, + List workerMessages, + string chainId, + string workspace, + string sessionId, + TokenUsage usage, + int baseIterations, + int reviewRound, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + // SSE: Review-Phase Start + await TryWriteSse(response, "review_start", new + { + round = reviewRound + 1, + maxRounds = review.MaxReviewIterations, + agentId = review.AgentId, + agentName = review.Agent?.Name ?? "Review-Agent" + }, cancellationToken); + + // Review-Agent bekommt eigene Message-History + var reviewMessages = BuildReviewMessages(workerMessages); + + // Review-Agent iteriert mit eigenen Tools + var (reviewText, reviewIterations) = await RunReviewAgentChat( + response, review, reviewMessages, chainId, workspace, sessionId, + usage, baseIterations, cancellationToken); + + var result = new ReviewResult { ReviewIterationCount = reviewIterations }; + + // Prüfe ob [APPROVED] + if (!string.IsNullOrEmpty(reviewText) && reviewText.Contains("[APPROVED]")) + { + await TryWriteSse(response, "review_approved", new + { + round = reviewRound + 1, + agentName = review.Agent?.Name ?? "Review-Agent" + }, cancellationToken); + result.HasFeedback = false; + return result; + } + + // Feedback – an Worker zurückspielen + result.HasFeedback = true; + result.Feedback = $"[Review-Feedback von {review.Agent?.Name ?? "Review-Agent"} (Runde {reviewRound + 1}/{review.MaxReviewIterations})]:\n{reviewText}"; + + await TryWriteSse(response, "review_feedback", new + { + round = reviewRound + 1, + maxRounds = review.MaxReviewIterations, + feedback = reviewText, + agentName = review.Agent?.Name ?? "Review-Agent" + }, cancellationToken); + + return result; + } + + /// + /// Baut die Messages für den Review-Agent auf. + /// Der Review-Agent bekommt die komplette Worker-Konversation als eine User-Message. + /// + private List BuildReviewMessages(List workerMessages) + { + // Worker-Konversation zusammenfassen + var summary = new StringBuilder(); + summary.AppendLine("Hier ist die bisherige Konversation zwischen User und Worker-Agent. Bitte überprüfe ob die Aufgabe korrekt erledigt wurde:\n"); + summary.AppendLine("---"); + + foreach (var msg in workerMessages) + { + var json = JsonSerializer.Serialize(msg); + var el = JsonSerializer.Deserialize(json); + + if (!el.TryGetProperty("role", out var role)) continue; + var roleStr = role.GetString() ?? ""; + + if (el.TryGetProperty("content", out var content)) + { + if (content.ValueKind == JsonValueKind.String) + { + summary.AppendLine($"\n**{roleStr}:** {content.GetString()}"); + } + else if (content.ValueKind == JsonValueKind.Array) + { + foreach (var block in content.EnumerateArray()) + { + var blockType = block.TryGetProperty("type", out var bt) ? bt.GetString() ?? "" : ""; + if (blockType == "text") + { + var text = block.TryGetProperty("text", out var t) ? t.GetString() ?? "" : ""; + summary.AppendLine($"\n**{roleStr}:** {text}"); + } + else if (blockType == "tool_use") + { + var toolName = block.TryGetProperty("name", out var tn) ? tn.GetString() ?? "" : ""; + var toolInput = block.TryGetProperty("input", out var ti) ? ti.ToString() : "{}"; + summary.AppendLine($"\n**{roleStr} [Tool: {toolName}]:** {toolInput}"); + } + else if (blockType == "tool_result") + { + var resultContent = block.TryGetProperty("content", out var rc) ? rc.ToString() : ""; + // Tool-Results kürzen für Review-Kontext + if (resultContent.Length > 2000) + resultContent = resultContent[..2000] + "\n[... gekürzt]"; + summary.AppendLine($"\n**{roleStr} [Tool-Result]:** {resultContent}"); + } + } + } + } + } + summary.AppendLine("\n---"); + summary.AppendLine("\nBitte überprüfe ob die Aufgabe korrekt erledigt wurde. Du kannst dazu auch Tools verwenden (z.B. Dateien lesen). Antworte mit exakt [APPROVED] wenn alles korrekt ist, oder beschreibe was falsch ist und korrigiert werden muss."); + + return new List + { + new Dictionary + { + ["role"] = "user", + ["content"] = new List { new { type = "text", text = summary.ToString() } } + } + }; + } + + /// + /// Führt den Review-Agent Chat aus (non-streaming für den Agent, aber SSE-Events für den Client). + /// Gibt den finalen Text und die Anzahl der Iterationen zurück. + /// + private async Task<(string text, int iterations)> RunReviewAgentChat( + HttpResponse response, + ReviewContext review, + List messages, + string chainId, + string workspace, + string sessionId, + TokenUsage usage, + int baseIterations, + CancellationToken cancellationToken) + { + int iterations = 0; + var finalText = new StringBuilder(); + int maxToolIterations = 20; // Review-Agent bekommt max 20 Tool-Iterationen + + while (iterations < maxToolIterations) + { + cancellationToken.ThrowIfCancellationRequested(); + iterations++; + + var payload = new Dictionary + { + ["model"] = review.ModelId, + ["max_tokens"] = 8192, + ["messages"] = messages, + ["stream"] = false + }; + + if (review.SystemPrompt != null && review.SystemPrompt.Count > 0) + payload["system"] = review.SystemPrompt; + + var hasWorkspace = !string.IsNullOrEmpty(workspace); + var includeMemory = review.UseMemory && _memoryService != null && !string.IsNullOrEmpty(review.AgentId); + var hasTools = hasWorkspace || includeMemory; + var hasWebSearch = !string.IsNullOrEmpty(review.WebSearchVersion); + + if (hasTools || hasWebSearch) + { + var tools = new List(); + if (hasTools) + tools.AddRange(_toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory)); + if (hasWebSearch) + tools.Add(new { type = review.WebSearchVersion, name = "web_search", max_uses = 5 }); + payload["tools"] = tools; + var effectiveChoice = (review.ToolChoice == "any" && iterations == 1) ? "any" : "auto"; + payload["tool_choice"] = new { type = effectiveChoice }; + } + + var client = CreateClient(); + var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true }); + LogPayload(json, sessionId, chainId, baseIterations + iterations); + var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); + + var httpResp = await client.SendAsync( + new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent }, + cancellationToken: cancellationToken); + + var respBody = await httpResp.Content.ReadAsStringAsync(cancellationToken); + + if (!httpResp.IsSuccessStatusCode) + { + return ($"Review-Agent API Fehler: {(int)httpResp.StatusCode}: {respBody}", iterations); + } + + var resp = JsonSerializer.Deserialize(respBody); + + // Usage tracken (auf den gemeinsamen Zähler) + if (resp.TryGetProperty("usage", out var u)) + { + if (u.TryGetProperty("input_tokens", out var it)) usage.InputTokens += it.GetInt64(); + if (u.TryGetProperty("output_tokens", out var ot)) usage.OutputTokens += ot.GetInt64(); + if (u.TryGetProperty("cache_creation_input_tokens", out var cc)) usage.CacheCreationTokens += cc.GetInt64(); + if (u.TryGetProperty("cache_read_input_tokens", out var cr)) usage.CacheReadTokens += cr.GetInt64(); + } + + var stopReason = resp.TryGetProperty("stop_reason", out var sr) ? sr.GetString() ?? "" : ""; + var content = resp.GetProperty("content"); + + var assistantContent = new List(); + var toolResults = new List(); + + foreach (var block in content.EnumerateArray()) + { + var blockType = block.GetProperty("type").GetString() ?? ""; + + if (blockType == "text") + { + var text = block.GetProperty("text").GetString() ?? ""; + assistantContent.Add(new { type = "text", text }); + finalText.Append(text); + + // Review-Text per SSE an den Client streamen + await TryWriteSse(response, "review_text", new + { + text, + agentName = review.Agent?.Name ?? "Review-Agent", + agentId = review.AgentId + }, cancellationToken); + + // In DB speichern (als Review-Message markiert) + await _db.SaveMessage(new MessageRecord + { + ChainId = chainId, + Iteration = baseIterations + iterations, + Role = "assistant", + Type = "text", + Content = $"[Review: {review.Agent?.Name ?? "Review-Agent"}] {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 }); + + // Tool-Use per SSE + await TryWriteSse(response, "review_tool_use", new + { + toolName, + toolId, + input = toolInput.ToString(), + agentName = review.Agent?.Name ?? "Review-Agent" + }, cancellationToken); + + await _db.SaveMessage(new MessageRecord + { + ChainId = chainId, + Iteration = baseIterations + iterations, + Role = "assistant", + Type = "tool_use", + Content = toolInput.GetRawText(), + ToolName = toolName, + ToolCallId = toolId + }); + + string toolResult; + bool isError = false; + try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, workspace, _memoryService, review.AgentId, cancellationToken); } + catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; } + + const int MaxToolResultChars = 20_000; + var truncatedResult = toolResult.Length > MaxToolResultChars + ? toolResult[..MaxToolResultChars] + $"\n\n[... gekürzt, {toolResult.Length - MaxToolResultChars} weitere Zeichen ...]" + : toolResult; + + toolResults.Add(new + { + type = "tool_result", + tool_use_id = toolId, + content = truncatedResult, + is_error = isError + }); + + await _db.SaveMessage(new MessageRecord + { + ChainId = chainId, + Iteration = baseIterations + iterations, + Role = "user", + Type = "tool_result", + Content = toolResult, + ToolName = toolName, + ToolCallId = toolId, + IsError = isError + }); + } + } + + messages.Add(new Dictionary { ["role"] = "assistant", ["content"] = assistantContent }); + + if (stopReason == "end_turn" || stopReason == "stop_sequence" || toolResults.Count == 0) + break; + + messages.Add(new Dictionary { ["role"] = "user", ["content"] = toolResults }); + } + + return (finalText.ToString(), iterations); + } + /// /// Non-Streaming Chat für Telegram. Gibt den finalen Antworttext zurück. /// onIteration wird nach jeder Iteration aufgerufen (z.B. für Typing-Indicator). diff --git a/Services/ChatService.cs b/Services/ChatService.cs index d2259d6..3c1428d 100644 --- a/Services/ChatService.cs +++ b/Services/ChatService.cs @@ -106,6 +106,42 @@ public class ChatService if (!string.IsNullOrWhiteSpace(systemPromptText)) systemPrompt.Add(new { type = "text", text = systemPromptText }); + // Review-Agent vorbereiten (falls konfiguriert) + ReviewContext reviewContext = null; + if (!string.IsNullOrEmpty(session.ReviewAgentId)) + { + var reviewAgent = await _db.GetAgent(session.ReviewAgentId); + if (reviewAgent != null) + { + var reviewModelId = ResolveModelId(reviewAgent.Model); + var reviewMarked = await _db.GetMarkedMessages(sessionId); + var reviewMemDir = _memoryService.GetAgentMemoryDir(reviewAgent.AgentId); + var reviewSkills = await _db.GetAgentSkills(reviewAgent.AgentId); + var reviewWorkspace = workspace; // gleicher Workspace wie Worker + var reviewSystemText = SystemPromptBuilder.Build( + reviewAgent, session, reviewMarked, _memoryService, reviewMemDir, + reviewSkills, _skillService, userPersona: user?.GetEffectivePersona(), + resolvedWorkspace: reviewWorkspace); + + var reviewPrompt = new List(); + if (!string.IsNullOrWhiteSpace(reviewSystemText)) + reviewPrompt.Add(new { type = "text", text = reviewSystemText }); + + reviewContext = new ReviewContext + { + ModelId = reviewModelId, + SystemPrompt = reviewPrompt, + AgentId = reviewAgent.AgentId, + Workspace = reviewWorkspace, + UseMemory = reviewAgent.UseMemory, + WebSearchVersion = reviewAgent.WebSearchVersion, + ToolChoice = reviewAgent.ToolChoice ?? "any", + MaxReviewIterations = session.MaxReviewIterations > 0 ? session.MaxReviewIterations : 3, + Agent = reviewAgent + }; + } + } + return new ChatContext { ChainId = chainId, @@ -120,7 +156,8 @@ public class ChatService WebSearchVersion = agent.WebSearchVersion, MaxIterations = maxIterations, Agent = agent, - Session = session + Session = session, + Review = reviewContext }; } diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 9a3f7bf..834919e 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -171,6 +171,8 @@ public class DatabaseService TryAddColumn(conn, "sessions", "avatar_scale", "REAL NOT NULL DEFAULT 1.0"); TryAddColumn(conn, "agents", "avatar_scale", "REAL NOT NULL DEFAULT 1.0"); TryAddColumn(conn, "cronjobs", "notify_session_id", "TEXT"); + TryAddColumn(conn, "sessions", "review_agent_id", "TEXT"); + TryAddColumn(conn, "sessions", "max_review_iterations", "INTEGER NOT NULL DEFAULT 3"); conn.Execute(@" CREATE TABLE IF NOT EXISTS agent_skills ( @@ -367,7 +369,7 @@ public class DatabaseService // ── Sessions ── - public async Task CreateSession(string userId, string name = null, string description = null, string image = null, string agentId = null, string workspace = null, string context = null, string customCommands = null, double avatarScale = 1.0) + public async Task CreateSession(string userId, string name = null, string description = null, string image = null, string agentId = null, string workspace = null, string context = null, string customCommands = null, double avatarScale = 1.0, string reviewAgentId = null, int maxReviewIterations = 3) { using var conn = CreateConnection(); var now = DateTime.UtcNow.ToString("o"); @@ -383,12 +385,14 @@ public class DatabaseService Context = context, CustomCommands = customCommands, AvatarScale = avatarScale, + ReviewAgentId = reviewAgentId, + MaxReviewIterations = maxReviewIterations, CreatedAt = now, UpdatedAt = now }; await conn.ExecuteAsync(@" - INSERT INTO sessions (session_id, user_id, name, description, image, agent_id, workspace, context, custom_commands, avatar_scale, created_at, updated_at) - VALUES (@SessionId, @UserId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CustomCommands, @AvatarScale, @CreatedAt, @UpdatedAt) + INSERT INTO sessions (session_id, user_id, name, description, image, agent_id, workspace, context, custom_commands, avatar_scale, review_agent_id, max_review_iterations, created_at, updated_at) + VALUES (@SessionId, @UserId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CustomCommands, @AvatarScale, @ReviewAgentId, @MaxReviewIterations, @CreatedAt, @UpdatedAt) ", session); return session; } @@ -432,6 +436,8 @@ public class DatabaseService s.Context, s.CustomCommands, s.AvatarScale, + s.ReviewAgentId, + s.MaxReviewIterations, s.IsDeleted, s.CreatedAt, s.UpdatedAt, @@ -482,6 +488,8 @@ public class DatabaseService if (body.TryGetProperty("context", out var ctx)) { sets.Add("context = @context"); p.Add("context", ctx.GetString()); } if (body.TryGetProperty("customCommands", out var cc)) { sets.Add("custom_commands = @customCommands"); p.Add("customCommands", cc.ValueKind == System.Text.Json.JsonValueKind.Null ? null : cc.GetRawText()); } if (body.TryGetProperty("avatarScale", out var asc)) { sets.Add("avatar_scale = @avatarScale"); p.Add("avatarScale", asc.GetDouble()); } + if (body.TryGetProperty("reviewAgentId", out var ra)) { sets.Add("review_agent_id = @reviewAgentId"); p.Add("reviewAgentId", ra.ValueKind == System.Text.Json.JsonValueKind.Null ? null : ra.GetString()); } + if (body.TryGetProperty("maxReviewIterations", out var mri)) { sets.Add("max_review_iterations = @maxReviewIterations"); p.Add("maxReviewIterations", mri.GetInt32()); } await conn.ExecuteAsync($"UPDATE sessions SET {string.Join(", ", sets)} WHERE session_id = @sessionId", p); } diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css index 7a66c0a..5a781ab 100644 --- a/wwwroot/css/chat.css +++ b/wwwroot/css/chat.css @@ -973,3 +973,74 @@ text-align: center; padding: var(--page-padding); } + +/* Review Bubbles */ + +.review-bubble { + align-self: flex-start; + background: var(--f1-bg); + border: var(--border-width) solid var(--f1-border); + border-left: 3px solid var(--secondary); + max-width: 80%; + display: flex; + flex-direction: column; + gap: var(--badge-spacing); +} + +.review-header { + display: flex; + align-items: center; + gap: var(--badge-spacing); + color: var(--secondary); +} + +.review-round { + opacity: var(--semi-transparent); + font-size: var(--font-size); +} + +.review-content { + word-break: break-word; +} + +.review-tool-use { + font-size: var(--font-size); + opacity: var(--semi-transparent); + display: flex; + align-items: center; + gap: var(--badge-spacing); +} + +.review-status { + display: flex; + align-items: center; + gap: var(--badge-spacing); + padding-top: var(--badge-spacing); + border-top: var(--border-width) solid var(--f2-border); + font-weight: bold; +} + +.review-approved { + color: var(--success); +} + +.review-rejected { + color: var(--warning); +} + +/* Review Iterations Row */ + +.review-iterations-row { + display: flex; + align-items: center; + gap: var(--box-spacing); +} + +.review-iterations-row label { + margin: 0; + white-space: nowrap; +} + +.review-iterations-row input[type="number"] { + width: 5em; +} diff --git a/wwwroot/index.html b/wwwroot/index.html index bcb6310..4cea2b9 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -108,6 +108,15 @@
Kein Agent ausgewählt
+ +
+
Kein Review-Agent
+
+ +
Weitere Einstellungen