Skip to content

Commit 4740899

Browse files
committed
improvement(home): name both choices in Sources mode with a Search / Assistant toggle
1 parent 09775ce commit 4740899

14 files changed

Lines changed: 196 additions & 155 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/answer-toggle/answer-toggle.test.tsx

Lines changed: 0 additions & 71 deletions
This file was deleted.

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/answer-toggle/answer-toggle.tsx

Lines changed: 0 additions & 44 deletions
This file was deleted.

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/answer-toggle/index.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export { AnimatedPlaceholderEffect } from './animated-placeholder-effect'
2-
export { AnswerToggle } from './answer-toggle'
32
export { AttachedFilesList } from './attached-files-list'
43
export type { ParsedChipLink, PortableKind } from './chip-clipboard-codec'
54
export {
@@ -37,3 +36,4 @@ export { PromptEditor, usePromptEditor } from './prompt-editor'
3736
export { SendButton } from './send-button'
3837
export type { SkillsMenuHandle } from './skills-menu-dropdown/skills-menu-dropdown'
3938
export { SkillsMenuDropdown } from './skills-menu-dropdown/skills-menu-dropdown'
39+
export { SourcesModeToggle } from './sources-mode-toggle'

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/mode-switcher/mode-switcher.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ describe('ModeSwitcher', () => {
8383
openMenu()
8484

8585
const rows = items()
86-
expect(rows.map((row) => row.textContent)).toEqual(['Build', 'Search'])
86+
expect(rows.map((row) => row.textContent)).toEqual(['Build', 'Sources'])
8787
expect(rows[0].querySelector('svg')).not.toBeNull()
8888
expect(rows[1].querySelector('svg')).toBeNull()
8989
})
@@ -97,15 +97,15 @@ describe('ModeSwitcher', () => {
9797
})
9898

9999
expect(useMothershipModeStore.getState().mode).toBe('search')
100-
expect(trigger().textContent).toBe('Search')
100+
expect(trigger().textContent).toBe('Sources')
101101
expect(mockCaptureEvent).toHaveBeenCalledWith(null, 'chat_mode_changed', {
102102
workspace_id: 'workspace-1',
103103
mode: 'search',
104104
})
105105
expect(mockSetSearchQuery).not.toHaveBeenCalled()
106106
})
107107

108-
it('drops the search query from the URL when leaving Search', () => {
108+
it('drops the search query from the URL when leaving Sources', () => {
109109
useMothershipModeStore.getState().setMode('search')
110110
mount()
111111
openMenu()

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/mode-switcher/mode-switcher.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ import {
2828

2929
const MODE_LABELS: Record<MothershipMode, string> = {
3030
build: 'Build',
31-
search: 'Search',
31+
search: 'Sources',
3232
}
3333

3434
/**
35-
* The composer's Build / Search switcher: a label-only `Chip` in its `round`
35+
* The composer's Build / Sources switcher: a label-only `Chip` in its `round`
3636
* shape — chip chrome throughout (`--text-body` label, `--surface-hover` on
3737
* hover, no text-color shift), fully round to sit in the toolbar's row of
3838
* round controls — opening a menu that checks the active mode, as
@@ -47,7 +47,7 @@ export const ModeSwitcher = memo(function ModeSwitcher() {
4747
const [, setSearchQueryParam] = useQueryState(searchQueryParam.key, searchQueryParam.parser)
4848
const [, setSearchFilters] = useQueryStates(searchFilterParsers, resourceUrlKeys)
4949

50-
/** Leaving Search drops the query from the URL, so a clean URL always means no search is showing. */
50+
/** Leaving Sources drops the query from the URL, so a clean URL always means no search is showing. */
5151
const handleSelect = (next: MothershipMode) => {
5252
if (next === mode) return
5353
setMode(next)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { SourcesModeToggle } from './sources-mode-toggle'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { act } from 'react'
5+
import { createRoot, type Root } from 'react-dom/client'
6+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7+
8+
const { mockCaptureEvent } = vi.hoisted(() => ({ mockCaptureEvent: vi.fn() }))
9+
10+
vi.mock('next/navigation', () => ({
11+
useParams: () => ({ workspaceId: 'workspace-1' }),
12+
}))
13+
vi.mock('posthog-js/react', () => ({ usePostHog: () => null }))
14+
vi.mock('@/lib/posthog/client', () => ({ captureEvent: mockCaptureEvent }))
15+
16+
import { SourcesModeToggle } from '@/app/workspace/[workspaceId]/home/components/user-input/components/sources-mode-toggle/sources-mode-toggle'
17+
import { useMothershipModeStore } from '@/stores/mothership-mode/store'
18+
19+
let root: Root | null = null
20+
let container: HTMLDivElement | null = null
21+
22+
function mount() {
23+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
24+
container = document.createElement('div')
25+
document.body.appendChild(container)
26+
root = createRoot(container)
27+
act(() => root?.render(<SourcesModeToggle />))
28+
}
29+
30+
function radios(): HTMLButtonElement[] {
31+
return Array.from(container?.querySelectorAll<HTMLButtonElement>('[role="radio"]') ?? [])
32+
}
33+
34+
function click(radio: HTMLButtonElement) {
35+
act(() => {
36+
radio.dispatchEvent(new MouseEvent('click', { bubbles: true, button: 0 }))
37+
})
38+
}
39+
40+
beforeEach(() => {
41+
mockCaptureEvent.mockClear()
42+
useMothershipModeStore.getState().reset()
43+
})
44+
45+
afterEach(() => {
46+
if (root) act(() => root?.unmount())
47+
container?.remove()
48+
root = null
49+
container = null
50+
})
51+
52+
describe('SourcesModeToggle', () => {
53+
it('renders nothing outside Sources mode', () => {
54+
mount()
55+
expect(radios()).toHaveLength(0)
56+
})
57+
58+
it('names both choices with Search selected by default, and switches to Assistant on click', () => {
59+
useMothershipModeStore.getState().setMode('search')
60+
mount()
61+
62+
expect(radios().map((radio) => radio.textContent)).toEqual(['Search', 'Assistant'])
63+
expect(radios().map((radio) => radio.getAttribute('aria-checked'))).toEqual(['true', 'false'])
64+
65+
click(radios()[1])
66+
67+
expect(useMothershipModeStore.getState().assistant).toBe(true)
68+
expect(radios().map((radio) => radio.getAttribute('aria-checked'))).toEqual(['false', 'true'])
69+
expect(mockCaptureEvent).toHaveBeenCalledWith(null, 'chat_sources_mode_changed', {
70+
workspace_id: 'workspace-1',
71+
mode: 'assistant',
72+
})
73+
})
74+
75+
it('does not report re-selecting the current choice', () => {
76+
useMothershipModeStore.getState().setMode('search')
77+
mount()
78+
79+
click(radios()[0])
80+
81+
expect(useMothershipModeStore.getState().assistant).toBe(false)
82+
expect(mockCaptureEvent).not.toHaveBeenCalled()
83+
})
84+
})
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use client'
2+
3+
import { memo } from 'react'
4+
import { Chip, Tooltip } from '@sim/emcn'
5+
import { useParams } from 'next/navigation'
6+
import { usePostHog } from 'posthog-js/react'
7+
import { captureEvent } from '@/lib/posthog/client'
8+
import { useMothershipModeStore } from '@/stores/mothership-mode/store'
9+
10+
const OPTIONS = [
11+
{
12+
assistant: false,
13+
label: 'Search',
14+
hint: 'Enterprise search: list the documents that match, from every source you can read',
15+
},
16+
{
17+
assistant: true,
18+
label: 'Assistant',
19+
hint: 'Answer in natural language from your sources, citing them, using your tools when needed',
20+
},
21+
] as const
22+
23+
/**
24+
* Sources mode's two ways to use the sources, as a pair of round chips that
25+
* act as one radio group: Search lists the matching documents; Assistant
26+
* answers the question in natural language from them. The selected chip is
27+
* the one in its selected state, so both choices are always named and the
28+
* current one is never in doubt.
29+
*/
30+
export const SourcesModeToggle = memo(function SourcesModeToggle() {
31+
const { workspaceId } = useParams<{ workspaceId: string }>()
32+
const posthog = usePostHog()
33+
const mode = useMothershipModeStore((state) => state.mode)
34+
const assistant = useMothershipModeStore((state) => state.assistant)
35+
const setAssistant = useMothershipModeStore((state) => state.setAssistant)
36+
37+
if (mode !== 'search') return null
38+
39+
const select = (next: boolean) => {
40+
if (next === assistant) return
41+
setAssistant(next)
42+
captureEvent(posthog, 'chat_sources_mode_changed', {
43+
workspace_id: workspaceId,
44+
mode: next ? 'assistant' : 'search',
45+
})
46+
}
47+
48+
return (
49+
<div role='radiogroup' aria-label='How to use your sources' className='flex items-center gap-1'>
50+
{OPTIONS.map((option) => (
51+
<Tooltip.Root key={option.label}>
52+
<Tooltip.Trigger asChild>
53+
<Chip
54+
shape='round'
55+
role='radio'
56+
aria-checked={option.assistant === assistant}
57+
active={option.assistant === assistant}
58+
onClick={() => select(option.assistant)}
59+
>
60+
{option.label}
61+
</Chip>
62+
</Tooltip.Trigger>
63+
<Tooltip.Content side='top'>{option.hint}</Tooltip.Content>
64+
</Tooltip.Root>
65+
))}
66+
</div>
67+
)
68+
})

apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ import { MOTHERSHIP_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
2121
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
2222
import {
2323
AnimatedPlaceholderEffect,
24-
AnswerToggle,
2524
AttachedFilesList,
2625
DropOverlay,
2726
MicButton,
2827
MicrophonePermissionHelp,
2928
ModeSwitcher,
3029
PromptEditor,
3130
SendButton,
31+
SourcesModeToggle,
3232
usePromptEditor,
3333
} from '@/app/workspace/[workspaceId]/home/components/user-input/components'
3434
import { handleMothershipAddContextEvent } from '@/app/workspace/[workspaceId]/home/components/user-input/mothership-context-event'
@@ -720,7 +720,7 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
720720
</Tooltip.Root>
721721
</div>
722722
<div className='flex items-center gap-1.5'>
723-
{canSearch && <AnswerToggle />}
723+
{canSearch && <SourcesModeToggle />}
724724
{canSearch && <ModeSwitcher />}
725725
{isSttSupported && (
726726
<MicButton

0 commit comments

Comments
 (0)