diff --git a/Services/AnthropicService.cs b/Services/AnthropicService.cs index 26c85e9..aca9904 100644 --- a/Services/AnthropicService.cs +++ b/Services/AnthropicService.cs @@ -450,30 +450,35 @@ public class AnthropicService var result = new ReviewResult { ReviewIterationCount = reviewIterations }; - // Prüfe ob [APPROVED] - if (!string.IsNullOrEmpty(reviewText) && reviewText.Contains("[APPROVED]")) + // Default = Approved (analog zum Preparer, wo Default = Forward ist). + // Nur bei explizitem [FEEDBACK]-Tag wird Feedback an den Worker zurückgespielt. + // [APPROVED] wird weiterhin erkannt, ist aber nicht mehr zwingend nötig. + if (!string.IsNullOrEmpty(reviewText) && reviewText.Contains("[FEEDBACK]")) { + // Explizites Feedback – an Worker zurückspielen + var feedbackText = reviewText.Replace("[FEEDBACK]", "").Replace("[APPROVED]", "").Trim(); + result.HasFeedback = true; + result.Feedback = $"[Review-Feedback von {review.Agent?.Name ?? "Review-Agent"} (Runde {reviewRound + 1}/{review.MaxReviewIterations})]:\n{feedbackText}"; + + await TryWriteSse(response, "review_feedback", new + { + round = reviewRound + 1, + maxRounds = review.MaxReviewIterations, + feedback = feedbackText, + agentName = review.Agent?.Name ?? "Review-Agent" + }, cancellationToken); + } + else + { + // Default: Approved (kein [FEEDBACK]-Tag → alles OK) 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; } @@ -803,6 +808,16 @@ public class AnthropicService is_error = isError }); + await TryWriteSse(response, $"{agentType}_tool_end", new + { + toolName, + toolId, + input = toolInput.ToString(), + result = toolResult.Length > 500 ? toolResult[..500] + "..." : toolResult, + isError, + agentName + }, cancellationToken); + await _db.SaveMessage(new MessageRecord { ChainId = chainId, diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css index 2167ecb..b2dd785 100644 --- a/wwwroot/css/chat.css +++ b/wwwroot/css/chat.css @@ -1124,12 +1124,9 @@ 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-tool-box { + margin: var(--badge-spacing) 0; + border-radius: var(--border-radius); } .review-status { diff --git a/wwwroot/js/chat.js b/wwwroot/js/chat.js index 6f69591..0c13ec6 100644 --- a/wwwroot/js/chat.js +++ b/wwwroot/js/chat.js @@ -87,6 +87,10 @@ function handleStreamEvent(eventType, data) { addReviewToolUse(data); break; + case 'preparer_tool_end': case 'review_tool_end': case 'finalizer_tool_end': + finishReviewToolBox(data); + break; + case 'preparer_done': finalizeReviewBubble(data.forwarded ? 'forwarded' : 'query', data, 'preparer'); break; diff --git a/wwwroot/js/history.js b/wwwroot/js/history.js index 0d839de..1970f9f 100644 --- a/wwwroot/js/history.js +++ b/wwwroot/js/history.js @@ -320,11 +320,8 @@ function _renderReviewGroup(tagGroup, archived) { text = text.replace(/^\[(Review|Preparer|Finalizer): [^\]]+\]\s*/, ''); if (text) textParts.push(text); } else if (m.type === 'tool_use') { - // Insert tool-use indicator - var toolDiv = document.createElement('div'); - toolDiv.className = 'review-tool-use'; - toolDiv.innerHTML = ' ' + esc(m.toolName) + ''; - // Flush accumulated text before tool indicator + if (!isToolVisible(m.toolName)) continue; + // Flush accumulated text before tool box if (textParts.length > 0) { var contentDiv = document.createElement('div'); contentDiv.className = 'review-content markdown'; @@ -333,9 +330,41 @@ function _renderReviewGroup(tagGroup, archived) { bubble.appendChild(contentDiv); textParts = []; } + // Full tool-box (same as normal tool boxes) + var toolDiv = document.createElement('div'); + toolDiv.className = 'tool-box review-tool-box' + (archived ? ' archived' : ''); + // Look ahead for matching tool_result + var toolResult = null; + var toolIsError = false; + for (var j = i + 1; j < tagGroup.length; j++) { + if (tagGroup[j].type === 'tool_result' && tagGroup[j].toolCallId === m.toolCallId) { + toolResult = tagGroup[j].content; + toolIsError = tagGroup[j].isError; + break; + } + } + renderToolBoxContent(toolDiv, m.toolName, m.content, toolResult, toolIsError); bubble.appendChild(toolDiv); + } else if (m.type === 'web_search_result') { + if (!isToolVisible('web_search')) continue; + if (textParts.length > 0) { + var contentDiv = document.createElement('div'); + contentDiv.className = 'review-content markdown'; + contentDiv.innerHTML = renderMarkdown(textParts.join('\n\n')); + parseFluentEmojis(contentDiv); + bubble.appendChild(contentDiv); + textParts = []; + } + var wsSources = []; + try { wsSources = JSON.parse(m.content || '[]'); } catch(e) {} + if (wsSources.length > 0) { + var wsDiv = document.createElement('div'); + wsDiv.className = 'tool-box review-tool-box' + (archived ? ' archived' : ''); + renderToolBoxContent(wsDiv, 'web_search', null, null, false, wsSources); + bubble.appendChild(wsDiv); + } } - // tool_result and web_search_result are skipped (same as streaming) + // tool_result is consumed by tool_use look-ahead above } // Flush remaining text diff --git a/wwwroot/js/stream-ui.js b/wwwroot/js/stream-ui.js index c8ba5e0..d904367 100644 --- a/wwwroot/js/stream-ui.js +++ b/wwwroot/js/stream-ui.js @@ -87,15 +87,32 @@ function appendToReviewText(text) { scrollDown(); } +let _currentReviewToolBox = null; + function addReviewToolUse(data) { if (!_reviewBubble) return; - const toolDiv = document.createElement('div'); - toolDiv.className = 'review-tool-use'; - toolDiv.innerHTML = ' ' + esc(data.toolName) + ''; + if (!isToolVisible(data.toolName)) { + _currentReviewToolBox = null; + return; + } + var toolDiv = document.createElement('div'); + toolDiv.className = 'tool-box review-tool-box streamed'; + toolDiv.dataset.toolName = data.toolName; + var displayName = TOOL_DISPLAY_NAMES[data.toolName] || data.toolName; + toolDiv.innerHTML = '' + esc(displayName) + ' '; _reviewBubble.appendChild(toolDiv); + _currentReviewToolBox = toolDiv; scrollDown(); } +function finishReviewToolBox(data) { + if (_currentReviewToolBox) { + renderToolBoxContent(_currentReviewToolBox, data.toolName, data.input, data.result, data.isError, data.sources); + _currentReviewToolBox = null; + scrollDown(); + } +} + function finalizeReviewBubble(type, data, agentType) { if (!_reviewBubble) return; const status = document.createElement('div'); @@ -120,5 +137,6 @@ function finalizeReviewBubble(type, data, agentType) { _reviewBubble = null; _reviewTextEl = null; _reviewText = ''; + _currentReviewToolBox = null; scrollDown(); }