feat: Web-Search Sources anzeigen (Streaming + History + Archive + Shared)

Backend:
- web_search_tool_result Sources extrahieren (url, title)
- Als 'web_search_result' Message-Typ in DB persistieren
- Sources im web_search_end SSE-Event mitsenden
- Alle 3 Chat-Pfade: StreamChat, RunAgentChat, ChatNonStreaming

Frontend:
- renderToolBoxContent: Sources-Sektion mit klickbaren Domain-Links
- finishToolBox/addToolBoxStatic: sources-Parameter durchgereicht
- History: web_search_result Message-Typ in allen 3 Render-Pfaden
- web_search immer sichtbar (auch bei 'modifications' Filter)

CSS:
- .web-search-sources: Flex-Wrap Container
- .web-search-source: Domain-Pill mit Hover-Effekt, Title-Ellipsis
This commit is contained in:
Meleki committed 2026-03-07 09:56:09 +01:00
1 parent 116f21105c
commit f64052caf4
5 files changed
+221 -7

No files matched your search

+118 -1
View File
@@ -161,6 +161,31 @@ public class AnthropicService
Content = el.GetProperty("input").ToString()
});
}
else if (blockType == "web_search_tool_result")
{
// Sources aus dem Ergebnis-Block extrahieren und speichern
var sources = new List<object>();
if (el.TryGetProperty("content", out var searchContent) && searchContent.ValueKind == JsonValueKind.Array)
{
foreach (var item in searchContent.EnumerateArray())
{
var itemType = item.TryGetProperty("type", out var it) ? it.GetString() : "";
if (itemType == "web_search_result")
{
var url = item.TryGetProperty("url", out var u) ? u.GetString() ?? "" : "";
var title = item.TryGetProperty("title", out var tt) ? tt.GetString() ?? "" : "";
if (!string.IsNullOrEmpty(url))
sources.Add(new { url, title });
}
}
}
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId, Iteration = iterations,
Role = "assistant", Type = "web_search_result",
Content = JsonSerializer.Serialize(sources)
});
}
}
// Tool Results persistieren
@@ -638,6 +663,44 @@ public class AnthropicService
IsError = isError
});
}
else if (blockType == "web_search_tool_result")
{
assistantContent.Add(JsonSerializer.Deserialize<JsonElement>(block.GetRawText()));
var sources = new List<object>();
if (block.TryGetProperty("content", out var searchContent) && searchContent.ValueKind == JsonValueKind.Array)
{
foreach (var item in searchContent.EnumerateArray())
{
var itemType = item.TryGetProperty("type", out var it) ? it.GetString() : "";
if (itemType == "web_search_result")
{
var url = item.TryGetProperty("url", out var u2) ? u2.GetString() ?? "" : "";
var title = item.TryGetProperty("title", out var tt2) ? tt2.GetString() ?? "" : "";
if (!string.IsNullOrEmpty(url))
sources.Add(new { url, title });
}
}
}
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId,
Iteration = baseIterations + iterations,
Role = "assistant",
Type = "web_search_result",
Content = JsonSerializer.Serialize(sources)
});
}
else if (blockType == "server_tool_use")
{
var toolId = block.GetProperty("id").GetString() ?? "";
var toolName = block.GetProperty("name").GetString() ?? "";
JsonElement inputEl;
try { inputEl = block.GetProperty("input"); }
catch { inputEl = JsonSerializer.Deserialize<JsonElement>("{}"); }
assistantContent.Add(new { type = "server_tool_use", id = toolId, name = toolName, input = inputEl });
}
}
messages.Add(new Dictionary<string, object> { ["role"] = "assistant", ["content"] = assistantContent });
@@ -808,6 +871,40 @@ public class AnthropicService
IsError = isError
});
}
else if (blockType == "web_search_tool_result")
{
assistantContent.Add(JsonSerializer.Deserialize<JsonElement>(block.GetRawText()));
var sources = new List<object>();
if (block.TryGetProperty("content", out var searchContent) && searchContent.ValueKind == JsonValueKind.Array)
{
foreach (var item in searchContent.EnumerateArray())
{
var itemType = item.TryGetProperty("type", out var it) ? it.GetString() : "";
if (itemType == "web_search_result")
{
var url = item.TryGetProperty("url", out var u2) ? u2.GetString() ?? "" : "";
var title = item.TryGetProperty("title", out var tt2) ? tt2.GetString() ?? "" : "";
if (!string.IsNullOrEmpty(url))
sources.Add(new { url, title });
}
}
}
await _db.SaveMessage(new MessageRecord
{
ChainId = ctx.ChainId, Iteration = iterations,
Role = "assistant", Type = "web_search_result",
Content = JsonSerializer.Serialize(sources)
});
}
else if (blockType == "server_tool_use")
{
var toolId = block.GetProperty("id").GetString() ?? "";
var toolName = block.GetProperty("name").GetString() ?? "";
JsonElement inputEl;
try { inputEl = block.GetProperty("input"); }
catch { inputEl = JsonSerializer.Deserialize<JsonElement>("{}"); }
assistantContent.Add(new { type = "server_tool_use", id = toolId, name = toolName, input = inputEl });
}
}
messages.Add(new Dictionary<string, object> { ["role"] = "assistant", ["content"] = assistantContent });
@@ -890,6 +987,7 @@ public class AnthropicService
var blockTypes = new Dictionary<int, string>();
var webSearchResultBlocks = new Dictionary<int, object>();
var webSearchSources = new Dictionary<int, List<object>>();
var executedTools = new List<ToolResult>();
var streamErrors = new List<object>();
@@ -964,6 +1062,24 @@ public class AnthropicService
// Gesamten Block als JSON speichern für die Conversation-History
var rawBlock = JsonSerializer.Deserialize<JsonElement>(cb.GetRawText());
webSearchResultBlocks[idx] = rawBlock;
// Sources extrahieren für Frontend
var sources = new List<object>();
if (cb.TryGetProperty("content", out var searchContent) && searchContent.ValueKind == JsonValueKind.Array)
{
foreach (var item in searchContent.EnumerateArray())
{
var itemType = item.TryGetProperty("type", out var it) ? it.GetString() : "";
if (itemType == "web_search_result")
{
var url = item.TryGetProperty("url", out var u) ? u.GetString() ?? "" : "";
var title = item.TryGetProperty("title", out var tt) ? tt.GetString() ?? "" : "";
if (!string.IsNullOrEmpty(url))
sources.Add(new { url, title });
}
}
}
webSearchSources[idx] = sources;
}
break;
}
@@ -1055,7 +1171,8 @@ public class AnthropicService
{
// Web-Search-Ergebnis von Anthropic – als Raw-JSON in assistantContent aufnehmen
assistantContent.Add(webSearchResultBlocks[idx]);
await WriteSseEvent(response, "web_search_end", new { id = currentToolId }, cancellationToken);
var sources = webSearchSources.TryGetValue(idx, out var s) ? s : new List<object>();
await WriteSseEvent(response, "web_search_end", new { id = currentToolId, sources }, cancellationToken);
}
}
break;