Skip to content

Commit 13b38fa

Browse files
committed
Optimize Anthropic internal web_search tool frontend rendering, optimize ToolCallBlock look and feel in white theme
1 parent 56beb9b commit 13b38fa

3 files changed

Lines changed: 121 additions & 28 deletions

File tree

src/BE/Services/Models/ChatServices/Anthropic/AnthropicChatService.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ static ChatSegment ExtractUsageFromStart(RawMessageStartEvent start)
6868
}
6969
else if (contentStart.ContentBlock.Value is WebSearchToolResultBlock toolResult)
7070
{
71-
string response = toolResult.Content.Json.ToString();
71+
string response = RemoveEncryptedContent(toolResult.Content.Json);
7272
yield return ChatSegment.FromToolCallResponse(toolResult.ToolUseID, response);
7373
}
7474
else if (contentStart.ContentBlock.Value is TextBlock textBlock)
@@ -152,6 +152,39 @@ static ChatSegment ExtractUsageFromStart(RawMessageStartEvent start)
152152
}
153153
}
154154

155+
/// <summary>
156+
/// Removes the encrypted_content field from web search results as it's very long,
157+
/// cannot be understood by the model, and wastes storage/bandwidth.
158+
/// </summary>
159+
private static string RemoveEncryptedContent(JsonElement json)
160+
{
161+
if (json.ValueKind == JsonValueKind.Array)
162+
{
163+
JsonArray results = [];
164+
foreach (JsonElement item in json.EnumerateArray())
165+
{
166+
if (item.ValueKind == JsonValueKind.Object)
167+
{
168+
JsonObject obj = [];
169+
foreach (JsonProperty prop in item.EnumerateObject())
170+
{
171+
if (prop.Name != "encrypted_content")
172+
{
173+
obj[prop.Name] = JsonNode.Parse(prop.Value.GetRawText());
174+
}
175+
}
176+
results.Add(obj);
177+
}
178+
else
179+
{
180+
results.Add(JsonNode.Parse(item.GetRawText()));
181+
}
182+
}
183+
return results.ToJsonString(JSON.JsonSerializerOptions);
184+
}
185+
return json.ToString();
186+
}
187+
155188
protected virtual AnthropicClient CreateAnthropicClient(ModelKey modelKey)
156189
{
157190
string url = (modelKey.Host ?? "https://api.anthropic.com").TrimEnd('/');

src/FE/components/Markdown/ToolCallBlock.tsx

Lines changed: 86 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ interface ToolCallBlockProps {
1414
chatStatus?: ChatSpanStatus;
1515
}
1616

17+
interface WebSearchResult {
18+
type?: string;
19+
title?: string;
20+
url?: string;
21+
page_age?: string;
22+
}
23+
1724
export const ToolCallBlock: FC<ToolCallBlockProps> = memo(({ toolCall, toolResponse, chatStatus }) => {
1825
const { t } = useTranslation();
1926
const [isParamsCopied, setIsParamsCopied] = useState<boolean>(false);
@@ -65,6 +72,22 @@ export const ToolCallBlock: FC<ToolCallBlockProps> = memo(({ toolCall, toolRespo
6572
return null;
6673
};
6774

75+
// 检查是否为web_search工具的结果数组
76+
const getWebSearchResults = (): WebSearchResult[] | null => {
77+
if (toolCall.n !== 'web_search' || !toolResponse) {
78+
return null;
79+
}
80+
try {
81+
const parsed = JSON.parse(toolResponse.r);
82+
if (Array.isArray(parsed) && parsed.length > 0 && parsed[0].type === 'web_search_result') {
83+
return parsed as WebSearchResult[];
84+
}
85+
} catch {
86+
return null;
87+
}
88+
return null;
89+
};
90+
6891
const copyToClipboard = (text: string, isParams: boolean) => (e: React.MouseEvent) => {
6992
if (!navigator.clipboard || !navigator.clipboard.writeText) {
7093
return;
@@ -83,6 +106,7 @@ export const ToolCallBlock: FC<ToolCallBlockProps> = memo(({ toolCall, toolRespo
83106
};
84107

85108
const code = getCodeIfAvailable();
109+
const webSearchResults = getWebSearchResults();
86110

87111
const toggleOpen = () => {
88112
setIsOpen(!isOpen);
@@ -92,28 +116,28 @@ export const ToolCallBlock: FC<ToolCallBlockProps> = memo(({ toolCall, toolRespo
92116
return (
93117
<div className="codeblock relative font-sans text-[16px]">
94118
{/* Tool header - 统一的标题栏 */}
95-
<div
96-
className="flex items-center gap-2 py-[6px] px-3 bg-[#3d3d3d] cursor-pointer hover:bg-[#454545] transition-all duration-200 ease-in-out"
97-
style={{
119+
<div
120+
className="flex items-center gap-2 py-[6px] px-3 bg-gray-200 dark:bg-gray-700 cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-600 transition-all duration-200 ease-in-out"
121+
style={{
98122
width: isOpen ? '100%' : collapsedWidth ? `${collapsedWidth}px` : 'fit-content',
99123
maxWidth: '100%',
100124
justifyContent: isOpen ? 'space-between' : 'flex-start',
101-
borderTopLeftRadius: 12,
125+
borderTopLeftRadius: 12,
102126
borderTopRightRadius: 12,
103127
borderBottomLeftRadius: isOpen ? 0 : 12,
104128
borderBottomRightRadius: isOpen ? 0 : 12,
105129
}}
106130
onClick={toggleOpen}
107131
>
108132
<div className="flex items-center gap-2">
109-
<span className="text-blue-400">🔧</span>
110-
<span className="text-sm text-white">{toolCall.n}</span>
133+
<span>🔧</span>
134+
<span className="text-sm text-gray-800 dark:text-white">{toolCall.n}</span>
111135
</div>
112-
<div
136+
<div
113137
className="flex items-center transition-transform duration-300 ease-in-out"
114138
style={{ transform: isOpen ? 'rotate(90deg)' : 'rotate(0deg)' }}
115139
>
116-
<IconChevronRight size={18} stroke="#9ca3af" />
140+
<IconChevronRight size={18} className="stroke-gray-500" />
117141
</div>
118142
</div>
119143

@@ -148,13 +172,13 @@ export const ToolCallBlock: FC<ToolCallBlockProps> = memo(({ toolCall, toolRespo
148172
<Tooltip>
149173
<TooltipTrigger asChild>
150174
<button
151-
className="flex items-center rounded bg-none p-1 text-xs text-white hover:bg-white/10"
175+
className="flex items-center rounded bg-none p-1 text-xs hover:bg-white/10"
152176
onClick={copyToClipboard(code, true)}
153177
>
154178
{isParamsCopied ? (
155-
<IconCheck stroke={'white'} size={16} />
179+
<IconCheck stroke="white" size={16} />
156180
) : (
157-
<IconClipboard stroke={'white'} size={16} />
181+
<IconClipboard stroke="white" size={16} />
158182
)}
159183
</button>
160184
</TooltipTrigger>
@@ -169,28 +193,28 @@ export const ToolCallBlock: FC<ToolCallBlockProps> = memo(({ toolCall, toolRespo
169193
// 普通的参数显示
170194
<div className="relative group">
171195
<div
172-
className="whitespace-pre-wrap break-words font-mono text-sm p-4 bg-[#282c34] text-[#abb2bf]"
196+
className="whitespace-pre-wrap break-words font-mono text-sm p-4 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300"
173197
style={{
174198
borderBottomRightRadius: toolResponse ? 0 : 12,
175199
borderBottomLeftRadius: toolResponse ? 0 : 12,
176200
}}
177201
>
178202
{toolCall.p}
179203
</div>
180-
204+
181205
{/* 参数区域的复制按钮 */}
182206
<div className="absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity">
183207
<TooltipProvider>
184208
<Tooltip>
185209
<TooltipTrigger asChild>
186210
<button
187-
className="flex items-center rounded bg-none p-1 text-xs text-white hover:bg-white/10"
211+
className="flex items-center rounded bg-none p-1 text-xs hover:bg-black/10 dark:hover:bg-white/10"
188212
onClick={copyToClipboard(toolCall.p, true)}
189213
>
190214
{isParamsCopied ? (
191-
<IconCheck stroke={'white'} size={16} />
215+
<IconCheck className="stroke-gray-600 dark:stroke-gray-300" size={16} />
192216
) : (
193-
<IconClipboard stroke={'white'} size={16} />
217+
<IconClipboard className="stroke-gray-600 dark:stroke-gray-300" size={16} />
194218
)}
195219
</button>
196220
</TooltipTrigger>
@@ -206,19 +230,19 @@ export const ToolCallBlock: FC<ToolCallBlockProps> = memo(({ toolCall, toolRespo
206230

207231
{/* Tool response - 统一的响应区域 */}
208232
{toolResponse && (
209-
<div
233+
<div
210234
className="overflow-hidden transition-all duration-300 ease-in-out"
211235
style={{
212236
maxHeight: isOpen ? '2000px' : '0',
213237
opacity: isOpen ? 1 : 0,
214238
}}
215239
>
216240
{/* Separator line */}
217-
<div className="bg-[#3d3d3d] h-[1px]" />
241+
<div className="bg-gray-300 dark:bg-gray-600 h-[1px]" />
218242

219243
{/* Response content */}
220-
<div
221-
className="relative group whitespace-pre-wrap break-words text-sm p-4 bg-[#282c34] text-[#abb2bf]"
244+
<div
245+
className={`relative group text-sm bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 ${webSearchResults ? 'p-2' : 'p-4'}`}
222246
style={{
223247
borderBottomRightRadius: 12,
224248
borderBottomLeftRadius: 12,
@@ -230,13 +254,13 @@ export const ToolCallBlock: FC<ToolCallBlockProps> = memo(({ toolCall, toolRespo
230254
<Tooltip>
231255
<TooltipTrigger asChild>
232256
<button
233-
className="flex items-center rounded bg-none p-1 text-xs text-white hover:bg-white/10"
257+
className="flex items-center rounded bg-none p-1 text-xs hover:bg-black/10 dark:hover:bg-white/10"
234258
onClick={copyToClipboard(toolResponse.r, false)}
235259
>
236260
{isResponseCopied ? (
237-
<IconCheck stroke={'white'} size={16} />
261+
<IconCheck className="stroke-gray-600 dark:stroke-gray-300" size={16} />
238262
) : (
239-
<IconClipboard stroke={'white'} size={16} />
263+
<IconClipboard className="stroke-gray-600 dark:stroke-gray-300" size={16} />
240264
)}
241265
</button>
242266
</TooltipTrigger>
@@ -246,7 +270,42 @@ export const ToolCallBlock: FC<ToolCallBlockProps> = memo(({ toolCall, toolRespo
246270
</Tooltip>
247271
</TooltipProvider>
248272
</div>
249-
{toolResponse.r}
273+
{webSearchResults ? (
274+
<table className="w-full border-collapse text-left m-0">
275+
<thead>
276+
<tr className="border-b border-gray-300 dark:border-gray-600">
277+
<th className="py-1 pr-3 font-medium">{t('Title')}</th>
278+
<th className="py-1 px-3 font-medium whitespace-nowrap">{t('Age')}</th>
279+
</tr>
280+
</thead>
281+
<tbody>
282+
{webSearchResults.map((result, index) => (
283+
<tr key={index} className="border-b border-gray-300 dark:border-gray-600 last:border-b-0 hover:bg-gray-200 dark:hover:bg-gray-700">
284+
<td className="py-1 pr-3" title={result.url}>
285+
{result.url ? (
286+
<a
287+
href={result.url}
288+
target="_blank"
289+
rel="noopener noreferrer"
290+
className="text-blue-600 dark:text-blue-400 hover:underline"
291+
onClick={(e) => e.stopPropagation()}
292+
>
293+
{result.title || result.url}
294+
</a>
295+
) : (result.title || '-')}
296+
</td>
297+
<td className="py-1 px-3 whitespace-nowrap">
298+
{result.page_age || '-'}
299+
</td>
300+
</tr>
301+
))}
302+
</tbody>
303+
</table>
304+
) : (
305+
<div className="whitespace-pre-wrap break-words">
306+
{toolResponse.r}
307+
</div>
308+
)}
250309
</div>
251310
</div>
252311
)}
@@ -256,9 +315,9 @@ export const ToolCallBlock: FC<ToolCallBlockProps> = memo(({ toolCall, toolRespo
256315
className="absolute -z-10 inline-flex items-center gap-2 py-[6px] px-3"
257316
style={{ visibility: 'hidden', pointerEvents: 'none', whiteSpace: 'nowrap' }}
258317
>
259-
<span className="text-blue-400">🔧</span>
260-
<span className="text-sm text-white">{toolCall.n}</span>
261-
<IconChevronRight size={18} stroke="#9ca3af" />
318+
<span>🔧</span>
319+
<span className="text-sm">{toolCall.n}</span>
320+
<IconChevronRight size={18} className="stroke-gray-500" />
262321
</div>
263322
</div>
264323
);

src/FE/locales/zh-CN.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@
315315
"1M output tokens price": "每百万token输出定价",
316316
"Remarks": "备注",
317317
"Title": "标题",
318+
"Age": "时间",
318319
"Chat Counts": "聊天次数",
319320
"Created Time": "创建时间",
320321
"Updated Time": "修改时间",

0 commit comments

Comments
 (0)