Skip to content

Commit bad9bef

Browse files
committed
feat(search): add private history and responsive result states
1 parent 458d384 commit bad9bef

29 files changed

Lines changed: 30477 additions & 182 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {
2+
clearSearchHistoryContract,
3+
listSearchHistoryContract,
4+
recordSearchHistoryContract,
5+
} from '@/lib/api/contracts/knowledge/search-history'
6+
import {
7+
defineInternalJsonRoute,
8+
internalOrchestrationErrorPolicy,
9+
internalRateLimits,
10+
internalSessionAuth,
11+
} from '@/lib/api/server/routes'
12+
import {
13+
clearSearchHistory,
14+
listSearchHistory,
15+
recordSearchHistory,
16+
searchHistoryOperations,
17+
} from '@/lib/knowledge/application/search-history'
18+
19+
export const GET = defineInternalJsonRoute({
20+
contract: listSearchHistoryContract,
21+
auth: internalSessionAuth,
22+
operation: searchHistoryOperations.list,
23+
rateLimit: internalRateLimits.user({ bucketName: 'search-history-read' }),
24+
errorPolicy: internalOrchestrationErrorPolicy,
25+
mapInput: ({ params }) => ({ organizationId: params.id }),
26+
useCase: listSearchHistory,
27+
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
28+
})
29+
export const POST = defineInternalJsonRoute({
30+
contract: recordSearchHistoryContract,
31+
auth: internalSessionAuth,
32+
operation: searchHistoryOperations.record,
33+
rateLimit: internalRateLimits.user({ bucketName: 'search-history-write' }),
34+
errorPolicy: internalOrchestrationErrorPolicy,
35+
parseOptions: { maxBodyBytes: 24 * 1024 },
36+
mapInput: ({ params, body }) => ({ organizationId: params.id, event: body }),
37+
useCase: recordSearchHistory,
38+
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
39+
})
40+
export const DELETE = defineInternalJsonRoute({
41+
contract: clearSearchHistoryContract,
42+
auth: internalSessionAuth,
43+
operation: searchHistoryOperations.clear,
44+
rateLimit: internalRateLimits.user({ bucketName: 'search-history-write' }),
45+
errorPolicy: internalOrchestrationErrorPolicy,
46+
mapInput: ({ params }) => ({ organizationId: params.id }),
47+
useCase: clearSearchHistory,
48+
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
49+
})
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'use client'
2+
3+
import { type ReactNode, useState } from 'react'
4+
import { Chip, cn, toast } from '@sim/emcn'
5+
import { Clock } from '@sim/emcn/icons'
6+
import { inter } from '@/app/_styles/fonts/inter/inter'
7+
import { SourceCard } from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-card'
8+
import { useClearSearchHistory, useSearchHistory } from '@/hooks/queries/search-history'
9+
10+
interface SearchLandingHistoryProps {
11+
organizationId: string
12+
userId: string
13+
onSearch: (query: string) => void
14+
children: ReactNode
15+
}
16+
17+
/** Private shortcuts under the composer; ordinary buttons retain native Tab/Enter navigation. */
18+
export function SearchLandingHistory({
19+
organizationId,
20+
userId,
21+
onSearch,
22+
children,
23+
}: SearchLandingHistoryProps) {
24+
const history = useSearchHistory(organizationId)
25+
const clear = useClearSearchHistory(organizationId, userId)
26+
const [selection, setSelection] = useState<'sources' | 'queries' | null>(null)
27+
const sources = history.data?.sources.slice(0, 5) ?? []
28+
const queries = history.data?.queries.slice(0, 5) ?? []
29+
const selected = selection ?? (sources.length > 0 ? 'sources' : 'queries')
30+
return (
31+
<div className={cn('w-full min-w-0', inter.className)}>
32+
{children}
33+
{(sources.length > 0 || queries.length > 0) && (
34+
<section aria-label='Recent activity' className='mt-6 px-2'>
35+
<div className='mb-2 flex flex-wrap items-center justify-between gap-2'>
36+
<div role='group' aria-label='History type' className='flex items-center gap-1'>
37+
<Chip
38+
active={selected === 'sources'}
39+
aria-pressed={selected === 'sources'}
40+
onClick={() => setSelection('sources')}
41+
>
42+
Recently viewed
43+
</Chip>
44+
<Chip
45+
active={selected === 'queries'}
46+
aria-pressed={selected === 'queries'}
47+
onClick={() => setSelection('queries')}
48+
>
49+
Recent searches
50+
</Chip>
51+
</div>
52+
<Chip
53+
disabled={clear.isPending}
54+
onClick={() =>
55+
clear.mutate(undefined, { onError: (error) => toast.error(error.message) })
56+
}
57+
>
58+
Clear history
59+
</Chip>
60+
</div>
61+
{selected === 'sources' ? (
62+
sources.length > 0 ? (
63+
sources.map((source) => <SourceCard key={source.url} source={source} dense />)
64+
) : (
65+
<p className='px-2 py-2 text-[var(--text-tertiary)] text-small'>
66+
Sources you open will appear here.
67+
</p>
68+
)
69+
) : queries.length > 0 ? (
70+
<div className='flex flex-col gap-1'>
71+
{queries.map(({ query }) => (
72+
<Chip key={query} fullWidth leftIcon={Clock} onClick={() => onSearch(query)}>
73+
{query}
74+
</Chip>
75+
))}
76+
</div>
77+
) : (
78+
<p className='px-2 py-2 text-[var(--text-tertiary)] text-small'>
79+
Your recent searches will appear here.
80+
</p>
81+
)}
82+
</section>
83+
)}
84+
{history.isError && (
85+
<div className='mt-6 flex items-center gap-2 px-4 text-[var(--text-tertiary)] text-small'>
86+
Recent activity couldn’t load.
87+
<Chip onClick={() => void history.refetch()} disabled={history.isFetching}>
88+
Try again
89+
</Chip>
90+
</div>
91+
)}
92+
</div>
93+
)
94+
}

‎apps/sim/app/o/[organizationId]/home/organization-home.tsx‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ import {
1616
getMothershipAttachmentUrl,
1717
} from '@/lib/mothership/chat/attachment-preview'
1818
import { createSearchResource } from '@/lib/mothership/resources/search'
19+
import { SearchLandingHistory } from '@/app/o/[organizationId]/components/search-landing-history'
1920
import { Composer } from '@/app/o/[organizationId]/home/components/composer'
2021
import { GetStarted } from '@/app/o/[organizationId]/home/components/get-started'
2122
import { organizationHomeParsers } from '@/app/o/[organizationId]/home/search-params'
2223
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
2324
import { organizationSearchUrlKeys } from '@/app/o/[organizationId]/search/search-params'
2425
import { ChatResourcePanel } from '@/app/workspace/[workspaceId]/home/components/chat-resource-panel'
26+
import { useSearchHistoryActions } from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-history-context'
2527
import { SearchIntegrationConnection } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/search-integration-connection'
2628
import { MothershipChat } from '@/app/workspace/[workspaceId]/home/components/mothership-chat'
2729
import { SuggestedActions } from '@/app/workspace/[workspaceId]/home/components/suggested-actions'
@@ -223,6 +225,7 @@ function OrganizationHomeContent({
223225
useEffect(() => {
224226
if (chat.error) toast.error(chat.error)
225227
}, [chat.error])
228+
const { recordQuery } = useSearchHistoryActions()
226229
const { sendMessage } = chat
227230
const { mutate: markRead } = useMarkMothershipChatRead({ organizationId: organization.id })
228231
const firstName = userName?.split(' ')[0] ?? ''
@@ -263,6 +266,7 @@ function OrganizationHomeContent({
263266
assistantSearch?: WorkspaceSearchFilters
264267
) => {
265268
if (requestMode !== 'assistant' && !canBuild) return
269+
if (requestMode === 'assistant') recordQuery(message)
266270
setSelectedMode(requestMode)
267271
if (requestMode !== 'assistant') panel.prepareResourceViewForAgentTurn()
268272
void sendMessage(message, fileAttachments, contexts, {
@@ -334,6 +338,14 @@ function OrganizationHomeContent({
334338
<div className='flex h-full min-h-0 min-w-[min(480px,100%)] flex-1 flex-col bg-[var(--bg)]'>
335339
{hasChat ? (
336340
<MothershipChat
341+
onViewSources={(messageId, requestId) =>
342+
addResource({
343+
type: 'sources',
344+
id: 'cited-sources',
345+
title: 'Sources',
346+
sources: { messageId, ...(requestId ? { requestId } : {}) },
347+
})
348+
}
337349
SearchConnectionComponent={SearchIntegrationConnection}
338350
messages={chat.messages}
339351
isSending={chat.isSending}
@@ -396,9 +408,18 @@ function OrganizationHomeContent({
396408
: `What should we get done${firstName ? `, ${firstName}` : ''}?`}
397409
</h1>
398410
<div className='relative w-full max-w-chat'>
399-
{composer}
400-
{/* Anchored out of flow so expanding/collapsing never shifts the centered input */}
401-
<div className='absolute inset-x-0 top-full'>
411+
{requestMode === 'assistant' && searchAccess.memberScoped && userId ? (
412+
<SearchLandingHistory
413+
organizationId={organization.id}
414+
userId={userId}
415+
onSearch={(query) => submit(query)}
416+
>
417+
{composer}
418+
</SearchLandingHistory>
419+
) : (
420+
composer
421+
)}
422+
<div className={requestMode === 'agent' ? 'absolute inset-x-0 top-full' : 'mt-4'}>
402423
{requestMode === 'agent' ? (
403424
<SuggestedActions
404425
organizationId={organization.id}

‎apps/sim/app/o/[organizationId]/providers/organization-provider.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { createContext, type ReactNode, useContext } from 'react'
44
import { useSession } from '@/lib/auth/auth-client'
55
import type { OrganizationSurfaceContext } from '@/lib/organizations/surface'
6+
import { SourceHistoryProvider } from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-history-context'
67
import { SocketProvider } from '@/app/workspace/providers/socket-provider'
78
import { useMothershipChatEvents } from '@/hooks/use-mothership-chat-events'
89
import { useSeedDeploymentShape } from '@/hooks/use-seed-deployment-shape'
@@ -43,7 +44,12 @@ export function OrganizationProvider({ children, context }: OrganizationProvider
4344
: undefined
4445
}
4546
>
46-
{children}
47+
<SourceHistoryProvider
48+
organizationId={context.searchAccess.memberScoped ? context.organization.id : undefined}
49+
userId={session?.user?.id}
50+
>
51+
{children}
52+
</SourceHistoryProvider>
4753
</SocketProvider>
4854
</OrganizationContextValue.Provider>
4955
)

0 commit comments

Comments
 (0)