|
| 1 | +'use client' |
| 2 | + |
| 3 | +import type { SimSearchSlackResult } from '@/lib/api/contracts/knowledge' |
| 4 | +import { matchSnippet } from '@/lib/knowledge/search/snippet' |
| 5 | +import { SourceCard } from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-card' |
| 6 | +import { |
| 7 | + isHttpUrl, |
| 8 | + type SourceTagData, |
| 9 | +} from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' |
| 10 | +import { useSimSearchSlack } from '@/hooks/queries/kb/knowledge' |
| 11 | + |
| 12 | +function toSource(result: SimSearchSlackResult, query: string): SourceTagData | null { |
| 13 | + if (!isHttpUrl(result.permalink)) return null |
| 14 | + return { |
| 15 | + url: result.permalink, |
| 16 | + title: `#${result.channelName}`, |
| 17 | + siteName: 'Slack', |
| 18 | + connectorType: 'slack', |
| 19 | + snippet: matchSnippet(result.text, query), |
| 20 | + author: result.authorName, |
| 21 | + updatedAt: result.sentAt ?? undefined, |
| 22 | + } |
| 23 | +} |
| 24 | + |
| 25 | +interface SlackSearchResultsProps { |
| 26 | + workspaceId: string |
| 27 | + query: string |
| 28 | + /** Asks the agent about one message; the prompt names it and links to it. */ |
| 29 | + onSummarize: (prompt: string) => void |
| 30 | +} |
| 31 | + |
| 32 | +/** |
| 33 | + * Slack messages matching the query, searched live as the signed-in person. |
| 34 | + * |
| 35 | + * Its own group rather than blended into the indexed results: Slack ranks with |
| 36 | + * its own relevance, which is not comparable to a vector distance, so |
| 37 | + * interleaving the two lists would present an ordering that means nothing. |
| 38 | + * Nothing renders while Slack is unconnected — the Sources strip is where a |
| 39 | + * person connects it — but a connection that stopped working says so, because |
| 40 | + * reconnecting is something they can act on. |
| 41 | + */ |
| 42 | +export function SlackSearchResults({ workspaceId, query, onSummarize }: SlackSearchResultsProps) { |
| 43 | + const { data, isPending, isPlaceholderData } = useSimSearchSlack(workspaceId, query) |
| 44 | + |
| 45 | + if (!data || isPending || isPlaceholderData) return null |
| 46 | + if (data.status === 'not_connected') return null |
| 47 | + |
| 48 | + if (data.status === 'needs_reauth') { |
| 49 | + return ( |
| 50 | + <p className='px-2 py-2 text-[var(--text-muted)] text-caption'> |
| 51 | + Reconnect Slack from Sources to search it. |
| 52 | + </p> |
| 53 | + ) |
| 54 | + } |
| 55 | + if (data.status === 'unavailable') { |
| 56 | + return ( |
| 57 | + <p className='px-2 py-2 text-[var(--text-muted)] text-caption'> |
| 58 | + Slack could not be searched this time. |
| 59 | + </p> |
| 60 | + ) |
| 61 | + } |
| 62 | + if (data.results.length === 0) return null |
| 63 | + |
| 64 | + return ( |
| 65 | + <div className='flex flex-col'> |
| 66 | + <p className='px-2 py-2 text-[var(--text-muted)] text-caption'> |
| 67 | + <span className='tabular-nums'> |
| 68 | + {data.results.length === 1 ? '1 Slack message' : `${data.results.length} Slack messages`} |
| 69 | + </span> |
| 70 | + {' · searched in Slack as you'} |
| 71 | + </p> |
| 72 | + {data.results.map((result) => { |
| 73 | + const source = toSource(result, query) |
| 74 | + return source ? ( |
| 75 | + <SourceCard |
| 76 | + key={`${result.channelId}:${result.messageTs}`} |
| 77 | + source={source} |
| 78 | + query={query} |
| 79 | + onSummarize={(cited) => |
| 80 | + onSummarize(`Summarize this Slack message from ${cited.title} (${cited.url})`) |
| 81 | + } |
| 82 | + /> |
| 83 | + ) : null |
| 84 | + })} |
| 85 | + </div> |
| 86 | + ) |
| 87 | +} |
0 commit comments