返回 DeepSeek-Reasonix
WebBlock.tsx
1 // Ported from DeepSeek Harness c291e7961a (MIT); host Markdown and safe links are retained.
2 const clsx = (...values: Array<string | false | undefined>) => values.filter(Boolean).join(' ')
3 import { Markdown } from '../Markdown'
4 import { RichMarkdownLink } from '../githubLink'
5 import { Globe } from 'lucide-react'
6 import css from './WebBlock.styles'
7
8 /**
9 * One citeable source drawn in a search card: the projection of the contract's
10 * `WebSource`, with the optional fields kept optional so a provider that
11 * returned only a URL still renders (its hostname becomes the label).
12 */
13 export interface WebSourceView {
14 /** The source URL; becomes a safe external link when it is http(s). */
15 url: string
16 /** The source title; when absent the URL's hostname labels the link. */
17 title?: string | undefined
18 /** A short excerpt or summary shown under the link. */
19 snippet?: string | undefined
20 /** Publication/crawl timestamp, a provider-supplied string shown under the link. */
21 publishedAt?: string | undefined
22 }
23
24 /** A `web_search` card: an optional answer over a capped citation list. */
25 export interface WebSearchBlockProps {
26 kind: 'search'
27 /** Localized chrome supplied by the owning render site. */
28 labels: WebBlockLabels
29 /** The provider-generated answer, rendered as markdown above the sources. */
30 answer?: string | undefined
31 /** The cited sources, in provider order. */
32 sources: WebSourceView[]
33 /** True when the tool cut the source list to its result cap. */
34 truncated: boolean
35 /** Extra class merged onto the wrapper (callers position; this component draws). */
36 className?: string | undefined
37 }
38
39 /** A `web_fetch` card: the retrieval summary for one fetched URL. */
40 export interface WebFetchBlockProps {
41 kind: 'fetch'
42 /** Localized chrome supplied by the owning render site. */
43 labels: WebBlockLabels
44 /** The final URL after allowed redirects; becomes a safe external link when http(s). */
45 url: string
46 /** HTTP status code of the fetched response. */
47 statusCode: number
48 /** True when the provider or the output cap cut the fetched content. */
49 truncated: boolean
50 /** Extra class merged onto the wrapper (callers position; this component draws). */
51 className?: string | undefined
52 }
53
54 /** A completed web retrieval card, discriminated by `kind`. */
55 export type WebBlockProps = WebSearchBlockProps | WebFetchBlockProps
56
57 /** Localized chrome for {@link WebBlock}. */
58 export interface WebBlockLabels {
59 noResults: string
60 sourcesTruncated: string
61 http: string
62 contentTruncated: string
63 }
64
65 /**
66 * The URL to link to, or undefined when the URL must render as plain text. Only
67 * http(s) becomes a navigable external anchor, so a `javascript:`/`data:`/`file:`
68 * URL or an unparseable string never reaches the DOM as an href. This is the
69 * http(s) subset of the allowlist MarkdownText applies to untrusted links —
70 * MarkdownText also permits `mailto:`, deliberately excluded here since a
71 * retrieval URL is never a mail address.
72 * @param url - the source or fetch URL, from tool result content.
73 * @returns the href to use, or undefined for plain text.
74 */
75 function safeHref(url: string): string | undefined {
76 try {
77 const { protocol } = new URL(url)
78 return protocol === 'http:' || protocol === 'https:' ? url : undefined
79 } catch {
80 return undefined
81 }
82 }
83
84 /**
85 * The link's visible label: the title when the provider gave one, otherwise the
86 * URL's hostname, falling back to the raw URL when it does not parse OR parses
87 * to an empty hostname (a `file:`/`data:`/`javascript:` URL), so a label is
88 * never blank.
89 * @param url - the source URL.
90 * @param title - the provider title, if any.
91 * @returns the label text.
92 */
93 function linkLabel(url: string, title: string | undefined): string {
94 if (title !== undefined && title !== '') return title
95 try {
96 const { hostname } = new URL(url)
97 return hostname === '' ? url : hostname
98 } catch {
99 return url
100 }
101 }
102
103 /**
104 * A single URL rendered as a safe external anchor, or as plain text when the
105 * URL is not an http(s) link.
106 * @param props.url - the URL to render.
107 * @param props.label - the visible label.
108 * @param props.className - class for the anchor or the plain span.
109 * @returns the anchor or span element.
110 */
111 function SafeLink({ url, label, className }: { url: string; label: string; className?: string | undefined }) {
112 const href = safeHref(url)
113 if (href === undefined) return <span className={className}>{label}</span>
114 return (
115 <span className={className}><RichMarkdownLink href={href}>
116 <Globe className={css.linkIcon} />
117 {label}
118 </RichMarkdownLink></span>
119 )
120 }
121
122 /**
123 * One source row in a search card: the safe link plus its snippet and date. The
124 * `<li value>` pins the source's 1-based citation index explicitly rather than
125 * relying on the `<ol>`'s implicit numbering, so a row reads by its real index
126 * even inside the scroll container.
127 * @param props.source - the source to render.
128 * @param props.ordinal - the source's 1-based position in the full list.
129 * @returns the source list item.
130 */
131 function SourceItem({ source, ordinal }: { source: WebSourceView; ordinal: number }) {
132 return (
133 <li className={css.source} value={ordinal}>
134 <SafeLink url={source.url} label={linkLabel(source.url, source.title)} className={css.sourceLink} />
135 {source.snippet !== undefined && source.snippet !== '' && (
136 <div className={css.snippet}>{source.snippet}</div>
137 )}
138 {source.publishedAt !== undefined && source.publishedAt !== '' && (
139 <div className={css.published}>{source.publishedAt}</div>
140 )}
141 </li>
142 )
143 }
144
145 /**
146 * The search card body: the answer over the full source list, which scrolls in
147 * place once it exceeds the `.sources` container height.
148 * @param props - see {@link WebSearchBlockProps}.
149 * @returns the search card element.
150 */
151 function WebSearchBlock({ answer, sources, truncated, labels, className }: WebSearchBlockProps) {
152 // A provider may legitimately return no answer and no sources; the chat WebRow
153 // does not show the raw result content, so without this the user would see an
154 // empty card. Mirror the backend's `No results found.` render text.
155 const empty = (answer === undefined || answer === '') && sources.length === 0
156 return (
157 <div className={clsx(css.block, className)} data-web="search">
158 {answer !== undefined && answer !== '' && (
159 <div className={css.answer}><Markdown text={answer} /></div>
160 )}
161 {empty ? (
162 <div className={css.empty}>{labels.noResults}</div>
163 ) : (
164 <ol className={css.sources}>
165 {sources.map((source, index) => <SourceItem key={index} source={source} ordinal={index + 1} />)}
166 </ol>
167 )}
168 {truncated && <div className={css.truncated}>{labels.sourcesTruncated}</div>}
169 </div>
170 )
171 }
172
173 /**
174 * The fetch card body: the linked URL and its HTTP status.
175 * @param props - see {@link WebFetchBlockProps}.
176 * @returns the fetch card element.
177 */
178 function WebFetchBlock({ url, statusCode, truncated, labels, className }: WebFetchBlockProps) {
179 return (
180 <div className={clsx(css.block, css.fetch, className)} data-web="fetch">
181 <SafeLink url={url} label={url} className={css.fetchUrl} />
182 <div className={css.fetchMeta}>
183 <span className={css.status}>{labels.http} {statusCode}</span>
184 {truncated && <span className={css.truncated}>{labels.contentTruncated}</span>}
185 </div>
186 </div>
187 )
188 }
189
190 /**
191 * Render a completed web retrieval as a structured card.
192 * @param props - see {@link WebBlockProps}; `kind` selects the search or fetch body.
193 * @returns the web card element.
194 */
195 export function WebBlock(props: WebBlockProps) {
196 return props.kind === 'search' ? <WebSearchBlock {...props} /> : <WebFetchBlock {...props} />
197 }
198
198 lines Plain Text