import type { JSX } from "react"; import { translate } from "../../../../i18n/index"; import { arrayValue, dedupeToolSectionContent, optionRecord, stringValue, ToolMarkdownBlock, ToolSection, type AgentToolRendererProps } from "./agentToolContentShared"; import { getWebSearchRenderData } from "./render-data/agentToolRenderData"; const MAX_SUMMARY_LENGTH = 3000; export function AgentWebSearchContent({ call, onLinkClick }: AgentToolRendererProps): JSX.Element | null { "use memo"; const web = getWebSearchRenderData(call); const queries = web.queries; const outputText = web.output; const links = normalizeLinks(call.output?.links, outputText); const queryText = webSearchQueryText(web.query, queries); const summary = extractSummary(outputText); const visibleSummary = dedupeToolSectionContent( summary ? summary.slice(0, MAX_SUMMARY_LENGTH) : null, queryText, links.map((link) => `${link.domain} ${link.title}`).join("\n") ); const hasRenderableContent = Boolean( queryText || links.length > 0 || visibleSummary || web.error ); if (!hasRenderableContent) { return null; } return (
{queryText ? ( ) : null} {links.length > 0 ? (
{links.map((link, index) => ( 0 ? "border-t border-[var(--line-2)]" : "" }`} > {link.domain} {link.title} ))}
) : null} {visibleSummary ? ( ) : null} {summary && summary.length > MAX_SUMMARY_LENGTH ? (
{translate("agentHost.agentTool.details.summaryTruncated")}
) : null} {web.error ? ( ) : null}
); } function webSearchQueryText( query: string | null, queries: readonly string[] ): string | null { const candidates = queries.length > 0 ? queries : query ? [query] : []; const deduped = [ ...new Set(candidates.map((value) => value.trim()).filter(Boolean)) ]; return deduped.length > 0 ? deduped.join("\n") : null; } function normalizeLinks( value: unknown, output: string ): Array<{ title: string; url: string; domain: string }> { const explicitLinks = arrayValue(value) ?.map(optionRecord) .filter( (candidate): candidate is Record => candidate !== null ) .flatMap((link) => { const url = stringValue(link.url); if (!url) { return []; } return [ { title: stringValue(link.title) ?? url, url, domain: domainForUrl(url) } ]; }) ?? []; if (explicitLinks.length > 0) { return explicitLinks; } const match = output.match(/^Links:\s*(\[[\s\S]*?\])(?:\n\n|\n|$)/); if (!match) { return extractQuotedLinks(output); } try { const linksJson = match[1]; if (!linksJson) { return extractQuotedLinks(output); } const parsed = JSON.parse(linksJson) as unknown[]; return parsed.flatMap((entry) => { const link = optionRecord(entry); const url = stringValue(link?.url); if (!url) { return []; } return [ { title: stringValue(link?.title) ?? url, url, domain: domainForUrl(url) } ]; }); } catch { return extractQuotedLinks(output); } } function extractQuotedLinks( output: string ): Array<{ title: string; url: string; domain: string }> { return Array.from( output.matchAll(/"title":"([^"]+)"\s*,\s*"url":"([^"]+)"/g) ).flatMap((entry) => { const title = entry[1]; const url = entry[2]; if (!title || !url) { return []; } return [{ title, url, domain: domainForUrl(url) }]; }); } function extractSummary(output: string): string | null { const trimmed = output.trim(); if (!trimmed) { return null; } const withLinksRemoved = trimmed .replace(/^Links:\s*\[[\s\S]*?\](?:\n\n|\n)?/, "") .trim(); return withLinksRemoved || (!trimmed.startsWith("Links:") ? trimmed : null); } function domainForUrl(url: string): string { try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return url; } }