From aff8f628829ebe1cb09fc53d9ce53e0e2f8d6e8e Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Fri, 31 Jul 2026 03:29:14 +0200 Subject: [PATCH] feat(client): richer web search source list The panel only showed favicon, title, and domain while the search artifact carries much more. Source rows now include the result snippet (clamped to two lines) and date under the domain, and a Serper answer box renders as a lead card above the list with its title and snippet. Streaming behavior is unchanged; the answer box is attachment-only since it never carries citation anchors. --- .../Chat/Messages/Content/WebSearch.tsx | 62 +++++++++++++++++-- .../Content/__tests__/WebSearch.test.tsx | 33 ++++++++++ 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/client/src/components/Chat/Messages/Content/WebSearch.tsx b/client/src/components/Chat/Messages/Content/WebSearch.tsx index 9ba2647bed..a2f2be3082 100644 --- a/client/src/components/Chat/Messages/Content/WebSearch.tsx +++ b/client/src/components/Chat/Messages/Content/WebSearch.tsx @@ -7,6 +7,7 @@ import type { ValidSource, SearchResultData, PartMetadata, + AnswerBoxResult, } from 'librechat-data-provider'; import { disclosureChevronClassName, toolPanelSpacingClassName } from './disclosure'; import { FaviconImage, getCleanDomain } from '~/components/Web/SourceHovercard'; @@ -175,6 +176,22 @@ export default function WebSearch({ return []; }, [searchResults, attachments, ownTurn]); + /** Direct-answer card shown at the top of the source list. Attachment-only: + * answer boxes are provider extras (Serper) that never carry citations, so + * the streaming path does not need them. */ + const answerBox = useMemo((): AnswerBoxResult | undefined => { + if (!attachments) { + return undefined; + } + for (const att of attachments) { + const answer = att.type === Tools.web_search ? att[Tools.web_search]?.answerBox : undefined; + if (answer) { + return answer; + } + } + return undefined; + }, [attachments]); + // Show favicons from the raw SERP results immediately rather than waiting for // each source to flip to `processed`; the agents scrape barrier would otherwise // freeze the stack on "Searching the web" for the slowest scrape's duration. @@ -284,8 +301,23 @@ export default function WebSearch({ 'mt-1.5 max-h-[280px] overflow-y-auto rounded-lg border border-border-light', )} > + {answerBox && (answerBox.title || answerBox.snippet) && ( +
+ {answerBox.title && ( +
+ {answerBox.title} +
+ )} + {answerBox.snippet && ( +
+ {answerBox.snippet} +
+ )} +
+ )} {allSources.map((source, i) => { const domain = getCleanDomain(source.link); + const snippet = 'snippet' in source ? source.snippet : undefined; return ( 0 && 'border-t border-border-light', + 'flex gap-2.5 px-3 py-2 transition-colors hover:bg-surface-hover', + snippet ? 'items-start' : 'items-center', + (i > 0 || !!answerBox) && 'border-t border-border-light', )} > - - - {source.title || domain} + + + + {source.title || domain} + + {snippet && ( + + {snippet} + + )} + + + {domain} + {source.date && ( + + {source.date} + + )} - {domain} ); })} diff --git a/client/src/components/Chat/Messages/Content/__tests__/WebSearch.test.tsx b/client/src/components/Chat/Messages/Content/__tests__/WebSearch.test.tsx index 59692eed4e..b4907d0e0e 100644 --- a/client/src/components/Chat/Messages/Content/__tests__/WebSearch.test.tsx +++ b/client/src/components/Chat/Messages/Content/__tests__/WebSearch.test.tsx @@ -291,6 +291,39 @@ describe('WebSearch', () => { expect(matches.length).toBeGreaterThanOrEqual(1); }); + it('renders snippets, dates, and the answer box in the source list', () => { + const attachment = { + type: Tools.web_search, + [Tools.web_search]: { + turn: 0, + organic: [ + { + link: 'https://example.com/context', + title: 'Context windows explained', + snippet: 'How context windows change what assistants can do.', + date: 'Jun 12, 2026', + }, + ], + answerBox: { + title: 'What is a context window?', + snippet: 'The amount of text a model can consider at once.', + }, + }, + } as unknown as TAttachment; + + renderWebSearch({ attachments: [attachment] }); + + expect(screen.getByText('What is a context window?')).toBeInTheDocument(); + expect( + screen.getByText('The amount of text a model can consider at once.'), + ).toBeInTheDocument(); + expect( + screen.getByText('How context windows change what assistants can do.'), + ).toBeInTheDocument(); + expect(screen.getByText('Jun 12, 2026')).toBeInTheDocument(); + expect(screen.getByText('example.com')).toBeInTheDocument(); + }); + it('uses standard tool-row spacing and reveals its chevron on hover or focus', () => { const searchResults = makeSearchResults({ 0: { organic: [makeSource('https://example.com', 'Example')] },