Skip to content

Commit 5324a15

Browse files
committed
fix(search): restore standalone search and refine chat mode controls
1 parent 2d9399a commit 5324a15

21 files changed

Lines changed: 650 additions & 378 deletions

File tree

‎apps/sim/app/o/[organizationId]/components/organization-sidebar/navigation.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { Home, Integration } from '@sim/emcn/icons'
1+
import { Home, Integration, Search } from '@sim/emcn/icons'
22
import { organizationRoutes } from '@/lib/navigation/paths'
33
import type { SidebarNavItemData } from '@/app/workspace/[workspaceId]/w/components/sidebar/components'
44

5-
type OrganizationNavRoute = 'home' | 'integrations'
5+
type OrganizationNavRoute = 'home' | 'search' | 'integrations'
66

77
interface OrganizationNavEntry {
88
id: string
@@ -17,6 +17,7 @@ interface OrganizationNavEntry {
1717
*/
1818
const ORGANIZATION_NAV_ENTRIES: readonly OrganizationNavEntry[] = [
1919
{ id: 'home', label: 'Home', icon: Home, route: 'home' },
20+
{ id: 'search', label: 'Search', icon: Search, route: 'search' },
2021
{ id: 'integrations', label: 'Integrations', icon: Integration, route: 'integrations' },
2122
]
2223

@@ -27,7 +28,7 @@ export function buildOrganizationNavItems(
2728
): SidebarNavItemData[] {
2829
const routes = organizationRoutes(organizationId)
2930
return ORGANIZATION_NAV_ENTRIES.filter(({ route }) =>
30-
route === 'home' ? mothershipAvailable || searchAvailable : searchAvailable
31+
route === 'home' ? mothershipAvailable : searchAvailable
3132
).map(({ route, ...entry }) => ({
3233
...entry,
3334
href: routes[route],

‎apps/sim/app/o/[organizationId]/components/search-input-bar.tsx‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ interface SearchInputBarProps {
2121
placeholder?: string
2222
'aria-label'?: string
2323
leadingControls?: ReactNode
24-
trailingControls: ReactNode
24+
selectionControl?: ReactNode
25+
voiceControl?: ReactNode
26+
submitControl: ReactNode
2527
floating?: boolean
2628
}
2729

@@ -35,7 +37,9 @@ export function SearchInputBar({
3537
onPaste,
3638
placeholder = 'Search your sources',
3739
'aria-label': ariaLabel = 'Search your sources',
38-
trailingControls,
40+
selectionControl,
41+
voiceControl,
42+
submitControl,
3943
floating = false,
4044
}: SearchInputBarProps) {
4145
const scrollerRef = useRef<HTMLDivElement>(null)
@@ -84,7 +88,10 @@ export function SearchInputBar({
8488
leadingControls={
8589
leadingControls ?? <Search className='size-[16px] shrink-0 text-[var(--text-icon)]' />
8690
}
87-
trailingControls={trailingControls}
91+
showModelSelector={false}
92+
selectionControl={selectionControl}
93+
voiceControl={voiceControl}
94+
submitControl={submitControl}
8895
expanded={expanded}
8996
editor={
9097
<GrowingTextarea

‎apps/sim/app/o/[organizationId]/home/components/composer/composer.test.tsx‎

Lines changed: 28 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,6 @@ async function render(
178178
| 'showModeSelector'
179179
| 'onModeChange'
180180
| 'restoredContexts'
181-
| 'allowNoAssistant'
182181
| 'assistantSearchLevel'
183182
| 'onAssistantSearchLevelChange'
184183
> = { isSending: false }
@@ -197,7 +196,6 @@ async function render(
197196
<Composer
198197
requestMode={requestMode}
199198
assistantSearchLevel={assistantSearchLevel}
200-
allowNoAssistant={controls.allowNoAssistant}
201199
onAssistantSearchLevelChange={
202200
controls.onAssistantSearchLevelChange ?? setAssistantSearchLevel
203201
}
@@ -237,7 +235,7 @@ describe('organization voice composer', () => {
237235
expect(container.querySelector('[aria-label="Model and reasoning effort"]')).toBeNull()
238236
expect(container.querySelector('[aria-label="Fast mode"]')).toBeNull()
239237
expect(mic.previousElementSibling?.getAttribute('aria-label')).toBe('Search level')
240-
expect(mic.nextElementSibling?.getAttribute('aria-label')).toBe('Send')
238+
expect(mic.parentElement?.nextElementSibling?.getAttribute('aria-label')).toBe('Send')
241239
await act(async () => mic.click())
242240
expect(mocks.toggleListening).toHaveBeenCalledOnce()
243241
const speech = mocks.speech.mock.calls.at(-1)![0]
@@ -372,19 +370,12 @@ describe('organization image composer', () => {
372370
)
373371
})
374372

375-
it('uses the picker and lets an attachment be removed before sending', async () => {
373+
it('lets a pasted attachment be removed before sending without an attachment button', async () => {
376374
await render(true, '')
377375
const input = container.querySelector<HTMLInputElement>('input[type="file"]')!
378-
const click = vi.spyOn(input, 'click')
379-
await act(async () =>
380-
container.querySelector<HTMLButtonElement>('button[aria-label="Attach images"]')!.click()
381-
)
382-
expect(click).toHaveBeenCalledOnce()
376+
expect(container.querySelector('[aria-label="Attach images"]')).toBeNull()
383377
expect(input.accept).toContain('image/png')
384-
Object.defineProperty(input, 'files', {
385-
value: fileList([new File(['image'], 'screenshot.png', { type: 'image/png' })]),
386-
})
387-
await act(async () => input.dispatchEvent(new Event('change', { bubbles: true })))
378+
await paste([new File(['image'], 'screenshot.png', { type: 'image/png' })])
388379
await act(async () =>
389380
container
390381
.querySelector<HTMLButtonElement>('button[aria-label="Remove screenshot.png"]')!
@@ -505,7 +496,7 @@ it.each([true, false])(
505496
expect(container.querySelector('[aria-label="Model and reasoning effort"]')).toBeNull()
506497
expect(container.querySelector('[aria-label="Fast mode"]')).toBeNull()
507498
expect(Boolean(container.querySelector('[aria-label="Conversation mode"]'))).toBe(canChoose)
508-
expect(container.querySelector('[aria-label="Attach images"]')).not.toBeNull()
499+
expect(container.querySelector('[aria-label="Attach images"]')).toBeNull()
509500
}
510501
)
511502

@@ -590,25 +581,26 @@ it('shows global built-ins once with no workspace label and submits no invented
590581
])
591582
})
592583

593-
it('offers an icon-only controlled mode picker before the staging input controls', async () => {
584+
it('shows Build with a chevron in the shared chip and text-only modes in its menu', async () => {
594585
const onModeChange = vi.fn()
595586
await render(true, 'Preserved draft', 'agent', {
596587
isSending: false,
597588
showModeSelector: true,
598589
onModeChange,
599590
})
600591
const mode = container.querySelector<HTMLButtonElement>('[aria-label="Conversation mode"]')!
601-
expect(mode.previousElementSibling).toBeNull()
602-
expect(mode.nextElementSibling?.getAttribute('aria-label')).toBe('Add resources')
603-
expect(mode.textContent).toBe('')
604-
expect(mode.querySelector('svg')).not.toBeNull()
592+
expect(mode.parentElement?.previousElementSibling).toBeNull()
593+
expect(mode.parentElement?.nextElementSibling?.getAttribute('aria-label')).toBe('Add resources')
594+
expect(mode.textContent).toBe('Build')
595+
expect(mode.querySelectorAll('svg')).toHaveLength(1)
605596
await act(async () =>
606597
mode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
607598
)
608-
const search = [...document.querySelectorAll<HTMLElement>('[role="menuitemradio"]')].find(
599+
const search = [...document.querySelectorAll<HTMLElement>('[role="menuitem"]')].find(
609600
(item) => item.textContent === 'Search'
610601
)!
611-
expect(search.querySelector('svg')).not.toBeNull()
602+
expect(search.querySelector('svg')).toBeNull()
603+
expect(document.querySelector('[role="tooltip"]')).toBeNull()
612604
await act(async () => search.click())
613605
expect(onModeChange).toHaveBeenCalledExactlyOnceWith('assistant')
614606
expect(
@@ -628,7 +620,7 @@ it('allows changing the next message mode while a response is streaming', async
628620
await act(async () =>
629621
mode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
630622
)
631-
const search = [...document.querySelectorAll<HTMLElement>('[role="menuitemradio"]')].find(
623+
const search = [...document.querySelectorAll<HTMLElement>('[role="menuitem"]')].find(
632624
(item) => item.textContent === 'Search'
633625
)!
634626
await act(async () => search.click())
@@ -701,7 +693,7 @@ it.each(['skill', 'file'] as const)(
701693
await act(async () =>
702694
mode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
703695
)
704-
const search = [...document.querySelectorAll<HTMLElement>('[role="menuitemradio"]')].find(
696+
const search = [...document.querySelectorAll<HTMLElement>('[role="menuitem"]')].find(
705697
(item) => item.textContent === 'Search'
706698
)!
707699
await act(async () => search.click())
@@ -769,11 +761,13 @@ describe('Search levels', () => {
769761
onModeChange: vi.fn(),
770762
})
771763
const mode = container.querySelector('[aria-label="Conversation mode"]')!
772-
const row = mode.parentElement!.parentElement!
764+
const row = mode.parentElement!.parentElement!.parentElement!
773765
expect(
774766
row.querySelector<HTMLInputElement | HTMLTextAreaElement>('[aria-label="Ask Sim"]')
775767
).not.toBeNull()
776-
expect(mode.nextElementSibling?.getAttribute('aria-label')).toBe('Attach images')
768+
expect(mode.textContent).toBe('')
769+
expect(mode.querySelectorAll('svg')).toHaveLength(2)
770+
expect(mode.parentElement?.nextElementSibling).toBeNull()
777771
expect(row.querySelector('[aria-label="Search level"]')?.textContent).toBe('Auto')
778772
})
779773
})
@@ -790,32 +784,15 @@ it('keeps Build Fast independent of Search levels', async () => {
790784
await act(async () => useMothershipEffortStore.getState().setFastMode(false))
791785
})
792786

793-
it.each([true, false])(
794-
'only offers None before a conversation (allowed: %s)',
795-
async (allowNoAssistant) => {
796-
await render(true, '', 'assistant', { isSending: false, allowNoAssistant })
797-
const picker = container.querySelector<HTMLButtonElement>('[aria-label="Search level"]')!
798-
await act(async () =>
799-
picker.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
800-
)
801-
const options = [...document.querySelectorAll<HTMLElement>('[role="menuitemradio"]')].map(
787+
it('offers only Fast, Auto, and Max search levels', async () => {
788+
await render(true, '', 'assistant', { isSending: false })
789+
const picker = container.querySelector<HTMLButtonElement>('[aria-label="Search level"]')!
790+
await act(async () =>
791+
picker.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
792+
)
793+
expect(
794+
[...document.querySelectorAll<HTMLElement>('[role="menuitemradio"]')].map(
802795
(item) => item.textContent
803796
)
804-
expect(options).toEqual(
805-
allowNoAssistant ? ['None', 'Fast', 'Auto', 'Max'] : ['Fast', 'Auto', 'Max']
806-
)
807-
}
808-
)
809-
810-
it('keeps attachments out of results-only search', async () => {
811-
await render(true, 'Orion', 'assistant', {
812-
isSending: false,
813-
assistantSearchLevel: 'none',
814-
allowNoAssistant: true,
815-
})
816-
expect(container.querySelector('[aria-label="Attach images"]')).toBeNull()
817-
await paste([new File(['image'], 'test.png', { type: 'image/png' })])
818-
expect(mocks.upload).not.toHaveBeenCalled()
819-
expect(container.querySelector('[aria-label="Voice input"]')).not.toBeNull()
820-
expect(container.querySelector('[aria-label="Search"]')).not.toBeNull()
797+
).toEqual(['Fast', 'Auto', 'Max'])
821798
})

0 commit comments

Comments
 (0)