11'use client'
22
33import { useMemo } from 'react'
4- import { Button , Chip , OverflowText } from '@sim/emcn'
5- import { FileText } from '@sim/emcn/icons'
6- import { formatDate } from '@sim/utils/formatting'
4+ import { Button , Chip } from '@sim/emcn'
75import { useQueryStates } from 'nuqs'
8- import type { WorkspaceKnowledgeSearchResult } from '@/lib/api/contracts/knowledge'
6+ import type {
7+ WorkspaceKnowledgeSearchResult ,
8+ WorkspaceSearchFilters ,
9+ } from '@/lib/api/contracts/knowledge'
10+ import { getBaseUrl } from '@/lib/core/utils/urls'
911import { matchSnippet } from '@/lib/knowledge/search/snippet'
1012import { connectorDisplayName } from '@/lib/sim-search/connectors'
11- import { searchedKnowledgeBases } from '@/lib/sim-search/knowledge-bases'
12- import {
13- highlightTerms ,
14- SOURCE_ROW_CLASSES ,
15- SOURCE_ROW_MARK_CLASSES ,
16- SourceCard ,
17- } from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-card'
13+ import { SourceCard } from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-card'
1814import {
1915 isHttpUrl ,
2016 type SourceTagData ,
@@ -78,14 +74,18 @@ export function indexingSourceNames(
7874
7975/**
8076 * A result as the source card renders it: the row's second line names the
81- * source app, or the knowledge base for an upload. A document without an
82- * http(s) source URL cannot be opened, and a connector-supplied value of any
83- * other scheme is never handed to the browser as a link.
77+ * source app, or the knowledge base for an upload. Without an HTTP(S) source
78+ * URL, the link opens the canonical document in Sim.
8479 */
85- function toSource ( result : WorkspaceKnowledgeSearchResult , query : string ) : SourceTagData | null {
86- if ( ! isHttpUrl ( result . sourceUrl ) ) return null
80+ function toSource (
81+ result : WorkspaceKnowledgeSearchResult ,
82+ query : string ,
83+ workspaceId : string
84+ ) : SourceTagData {
8785 return {
88- url : result . sourceUrl ,
86+ url : isHttpUrl ( result . sourceUrl )
87+ ? result . sourceUrl
88+ : `${ getBaseUrl ( ) } /workspace/${ encodeURIComponent ( workspaceId ) } /knowledge/${ encodeURIComponent ( result . knowledgeBaseId ) } /${ encodeURIComponent ( result . documentId ) } ` ,
8989 title : result . documentName ?? undefined ,
9090 siteName : result . connectorType
9191 ? connectorDisplayName ( result . connectorType )
@@ -113,53 +113,18 @@ function handleResultsKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
113113 links [ next ] . focus ( )
114114}
115115
116- interface UnlinkedResultRowProps {
117- result : WorkspaceKnowledgeSearchResult
118- query : string
119- }
120-
121- /**
122- * A document with nowhere to open, such as an upload: the same row as a
123- * linked result, with the file mark in place of a brand mark, so the list's
124- * columns and the matched passage stay aligned whatever the document is.
125- */
126- function UnlinkedResultRow ( { result, query } : UnlinkedResultRowProps ) {
127- const meta = [
128- result . knowledgeBaseName ,
129- result . author ,
130- result . sourceModifiedAt ? formatDate ( new Date ( result . sourceModifiedAt ) ) : null ,
131- ] . filter ( ( part ) : part is string => Boolean ( part ) )
132- return (
133- < div className = { SOURCE_ROW_CLASSES } >
134- < span className = { SOURCE_ROW_MARK_CLASSES } >
135- < FileText className = 'size-[16px] text-[var(--text-icon)]' />
136- </ span >
137- < div className = 'flex min-w-0 flex-1 flex-col gap-0.5' >
138- < OverflowText
139- label = { result . documentName ?? 'Untitled document' }
140- className = 'text-[var(--text-primary)] text-sm'
141- />
142- < OverflowText label = { meta . join ( ' · ' ) } className = 'text-[var(--text-muted)] text-caption' />
143- < p className = 'line-clamp-2 text-[var(--text-body)] text-small leading-snug' >
144- { highlightTerms ( matchSnippet ( result . content , query ) , query ) }
145- </ p >
146- </ div >
147- </ div >
148- )
149- }
150-
151116interface KnowledgeSearchResultsProps {
152117 workspaceId : string
153118 query : string
154- /** Asks the agent about one document; the prompt names it and links to it . */
155- onSummarize : ( prompt : string ) => void
119+ /** Binds the Assistant turn to the selected canonical document . */
120+ onSummarize : ( prompt : string , filters : WorkspaceSearchFilters ) => void
156121 /** Asks the agent the query itself, for a prose answer with citations. */
157- onAnswer : ( query : string ) => void
122+ onAnswer : ( query : string , filters : WorkspaceSearchFilters ) => void
158123}
159124
160125/**
161126 * The composer's Search mode: the documents the signed-in person may read that
162- * match their query, across every knowledge base in the workspace , as rows
127+ * match their query in the canonical Enterprise Search index , as rows
163128 * that open the source. A header says how many and that the search ran as
164129 * them; while a connected source is still indexing it says so, and the list
165130 * grows as documents land. Filters by source and recency appear only once the
@@ -177,14 +142,26 @@ export function KnowledgeSearchResults({
177142 isPending : basesPending ,
178143 error : basesError ,
179144 } = useKnowledgeBasesQuery ( workspaceId )
180- const knowledgeBaseIds = searchedKnowledgeBases ( knowledgeBases , workspaceId ) . map ( ( kb ) => kb . id )
145+ const index = knowledgeBases . find (
146+ ( base ) => base . workspaceId === workspaceId && base . isSearchIndex
147+ )
148+ const knowledgeBaseIds = index ? [ index . id ] : [ ]
149+ const [ filters , setFilters ] = useQueryStates ( searchFilterParsers , resourceUrlKeys )
150+ const searchFilters = useMemo < WorkspaceSearchFilters > ( ( ) => {
151+ const window = UPDATED_WINDOWS . find ( ( entry ) => entry . id === filters . updated )
152+ return {
153+ ...( filters . source ? { source : filters . source } : { } ) ,
154+ ...( window ?. days
155+ ? { modifiedAfter : new Date ( Date . now ( ) - window . days * DAY_MS ) . toISOString ( ) }
156+ : { } ) ,
157+ }
158+ } , [ filters . source , filters . updated ] )
181159 const {
182160 data : results ,
183161 isPending,
184162 isFetching,
185- isPlaceholderData,
186163 error,
187- } = useWorkspaceKnowledgeSearch ( workspaceId , knowledgeBaseIds , query )
164+ } = useWorkspaceKnowledgeSearch ( workspaceId , query , searchFilters )
188165 /**
189166 * With per-member access off, member-scoped documents are hidden, so the
190167 * indexing list is not worth asking for.
@@ -199,28 +176,16 @@ export function KnowledgeSearchResults({
199176 : EMPTY_MEMBER_CONNECTORS
200177 const indexing = indexingSourceNames ( memberConnectors , knowledgeBaseIds )
201178 const documents = useMemo ( ( ) => groupResultsByDocument ( results ?? [ ] ) , [ results ] )
202- const sourceTypes = useMemo (
203- ( ) => [ ...new Set ( documents . map ( ( result ) => result . connectorType ?? UPLOAD_SOURCE ) ) ] ,
204- [ documents ]
205- )
206- const [ filters , setFilters ] = useQueryStates ( searchFilterParsers , resourceUrlKeys )
179+ const sourceTypes = [
180+ ...new Set ( [
181+ ...( filters . source ? [ filters . source ] : [ ] ) ,
182+ ...documents . map ( ( result ) => result . connectorType ?? UPLOAD_SOURCE ) ,
183+ ] ) ,
184+ ]
207185 const filtersActive = filters . source !== null || filters . updated !== 'any'
208186 /** The controls appear once the list is long and mixed, and stay while a filter from the link is active. */
209187 const showFilters =
210188 filtersActive || ( documents . length >= FILTERS_MIN_RESULTS && sourceTypes . length > 1 )
211- const visible = useMemo ( ( ) => {
212- if ( ! filtersActive ) return documents
213- const window = UPDATED_WINDOWS . find ( ( entry ) => entry . id === filters . updated )
214- const cutoff = window ?. days ? Date . now ( ) - window . days * DAY_MS : null
215- return documents . filter ( ( result ) => {
216- if ( filters . source && ( result . connectorType ?? UPLOAD_SOURCE ) !== filters . source ) return false
217- if ( cutoff !== null ) {
218- const modified = result . sourceModifiedAt ? Date . parse ( result . sourceModifiedAt ) : Number . NaN
219- if ( Number . isNaN ( modified ) || modified < cutoff ) return false
220- }
221- return true
222- } )
223- } , [ documents , filtersActive , filters . source , filters . updated ] )
224189
225190 const failure = basesError ?? error
226191 if ( failure ) {
@@ -233,8 +198,7 @@ export function KnowledgeSearchResults({
233198 </ p >
234199 )
235200 }
236- /** Kept results belong to the previous query; a new query shows its own state. */
237- if ( isPending || isPlaceholderData || ( isFetching && ! results ) ) {
201+ if ( isPending || ( isFetching && ! results ) ) {
238202 return < p className = 'px-2 py-2 text-[var(--text-muted)] text-caption' > Searching…</ p >
239203 }
240204
@@ -253,7 +217,7 @@ export function KnowledgeSearchResults({
253217 { ' · searched as you' }
254218 { indexingNote && < span className = 'block' > { indexingNote } </ span > }
255219 </ span >
256- < Button variant = 'ghost' size = 'sm' onClick = { ( ) => onAnswer ( query ) } >
220+ < Button variant = 'ghost' size = 'sm' onClick = { ( ) => onAnswer ( query , searchFilters ) } >
257221 Answer with Sim
258222 </ Button >
259223 </ div >
@@ -289,27 +253,28 @@ export function KnowledgeSearchResults({
289253 ) ) }
290254 </ div >
291255 ) }
292- { visible . length === 0 ? (
256+ { documents . length === 0 ? (
293257 < p className = 'px-2 py-2 text-[var(--text-muted)] text-caption' >
294- { documents . length === 0
295- ? ` No documents you can read match “ ${ query } ”.`
296- : ' No documents match these filters.' }
258+ { filtersActive
259+ ? ' No documents match these filters.'
260+ : ` No documents you can read match “ ${ query } ”.` }
297261 </ p >
298262 ) : (
299263 < div className = 'flex flex-col' onKeyDown = { handleResultsKeyDown } >
300- { visible . map ( ( result ) => {
301- const source = toSource ( result , query )
302- return source ? (
264+ { documents . map ( ( result ) => {
265+ const source = toSource ( result , query , workspaceId )
266+ return (
303267 < SourceCard
304268 key = { result . documentId }
305269 source = { source }
306270 query = { query }
307271 onSummarize = { ( cited ) =>
308- onSummarize ( `Summarize "${ cited . title ?? cited . url } " (${ cited . url } )` )
272+ onSummarize ( `Summarize "${ cited . title ?? cited . url } "` , {
273+ ...searchFilters ,
274+ documentIds : [ result . documentId ] ,
275+ } )
309276 }
310277 />
311- ) : (
312- < UnlinkedResultRow key = { result . documentId } result = { result } query = { query } />
313278 )
314279 } ) }
315280 </ div >
0 commit comments