Skip to content

Commit 47c70e9

Browse files
committed
feat(search): unify Home search levels and composer
1 parent 1d57208 commit 47c70e9

58 files changed

Lines changed: 2014 additions & 1000 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { MothershipChatSkeleton } from '@/app/workspace/[workspaceId]/home/components/mothership-chat/components/mothership-chat-skeleton'
2+
3+
/** Keep the transcript shape while the saved conversation is loading. */
4+
export default function OrganizationChatLoading() {
5+
return (
6+
<div className='flex h-full min-h-0 flex-col bg-[var(--bg)]'>
7+
<div className='mt-[var(--workspace-content-title-bar-inset)] min-h-0 flex-1 overflow-hidden px-6 pt-4 [scrollbar-gutter:stable_both-edges]'>
8+
<MothershipChatSkeleton />
9+
</div>
10+
</div>
11+
)
12+
}

‎apps/sim/app/o/[organizationId]/chat/[chatId]/page.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { getSession } from '@/lib/auth'
55
import { getAccessibleCopilotChatAuth } from '@/lib/mothership/chat/lifecycle'
66
import { WORKSPACE_SETTINGS_PATH } from '@/lib/navigation/paths'
77
import { getOrganizationSurfaceContext } from '@/lib/organizations/surface'
8+
import OrganizationChatLoading from '@/app/o/[organizationId]/chat/[chatId]/loading'
89
import { OrganizationHome } from '@/app/o/[organizationId]/home/organization-home'
9-
import OrganizationSearchLoading from '@/app/o/[organizationId]/search/loading'
1010

1111
export const metadata: Metadata = { title: 'Chat' }
1212

@@ -29,7 +29,7 @@ export default async function OrganizationChatPage({
2929
if (chat.mode === 'assistant') {
3030
if (!context.searchAccess.memberScoped) redirect(WORKSPACE_SETTINGS_PATH)
3131
return (
32-
<Suspense fallback={<OrganizationSearchLoading />}>
32+
<Suspense fallback={<OrganizationChatLoading />}>
3333
<OrganizationHome
3434
requestMode='assistant'
3535
userName={session.user.name ?? undefined}
@@ -40,7 +40,7 @@ export default async function OrganizationChatPage({
4040
}
4141
if (!context.mothershipAvailable) redirect(WORKSPACE_SETTINGS_PATH)
4242
return (
43-
<Suspense fallback={<OrganizationSearchLoading />}>
43+
<Suspense fallback={<OrganizationChatLoading />}>
4444
<OrganizationHome
4545
userName={session.user.name ?? undefined}
4646
chatId={chatId}

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { Home, Integration, Search } from '@sim/emcn/icons'
1+
import { Home, Integration } 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' | 'search' | 'integrations'
5+
type OrganizationNavRoute = 'home' | 'integrations'
66

77
interface OrganizationNavEntry {
88
id: string
@@ -17,7 +17,6 @@ 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' },
2120
{ id: 'integrations', label: 'Integrations', icon: Integration, route: 'integrations' },
2221
]
2322

@@ -28,7 +27,7 @@ export function buildOrganizationNavItems(
2827
): SidebarNavItemData[] {
2928
const routes = organizationRoutes(organizationId)
3029
return ORGANIZATION_NAV_ENTRIES.filter(({ route }) =>
31-
route === 'home' ? mothershipAvailable : searchAvailable
30+
route === 'home' ? mothershipAvailable || searchAvailable : searchAvailable
3231
).map(({ route, ...entry }) => ({
3332
...entry,
3433
href: routes[route],
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import {
2+
type ClipboardEventHandler,
3+
type ReactNode,
4+
type RefObject,
5+
useLayoutEffect,
6+
useRef,
7+
useState,
8+
} from 'react'
9+
import { cn } from '@sim/emcn'
10+
import { Search } from '@sim/emcn/icons'
11+
import { PROMPT_TEXT_CLASSES } from '@/app/workspace/[workspaceId]/home/components/user-input/components/constants'
12+
import { GrowingTextarea } from '@/app/workspace/[workspaceId]/home/components/user-input/components/growing-textarea'
13+
import { InputToolbar } from '@/app/workspace/[workspaceId]/home/components/user-input/components/input-toolbar'
14+
15+
interface SearchInputBarProps {
16+
inputRef: RefObject<HTMLTextAreaElement | null>
17+
value: string
18+
onChange: (value: string) => void
19+
onSubmit: () => void
20+
onPaste?: ClipboardEventHandler<HTMLTextAreaElement>
21+
placeholder?: string
22+
'aria-label'?: string
23+
leadingControls?: ReactNode
24+
trailingControls: ReactNode
25+
floating?: boolean
26+
}
27+
28+
/** Canonical Search bar shared by raw search and conversational Search. */
29+
export function SearchInputBar({
30+
leadingControls,
31+
inputRef,
32+
value,
33+
onChange,
34+
onSubmit,
35+
onPaste,
36+
placeholder = 'Search your sources',
37+
'aria-label': ariaLabel = 'Search your sources',
38+
trailingControls,
39+
floating = false,
40+
}: SearchInputBarProps) {
41+
const scrollerRef = useRef<HTMLDivElement>(null)
42+
const leadingRef = useRef<HTMLDivElement>(null)
43+
const trailingRef = useRef<HTMLDivElement>(null)
44+
const textMeasureRef = useRef<HTMLSpanElement>(null)
45+
const [expanded, setExpanded] = useState(value.includes('\n'))
46+
47+
/** Measure against the compact width so expanding the field cannot cause a wrap/collapse loop. */
48+
useLayoutEffect(() => {
49+
const bar = scrollerRef.current
50+
const leading = leadingRef.current
51+
const trailing = trailingRef.current
52+
const text = textMeasureRef.current
53+
if (!bar || !leading || !trailing || !text) return
54+
const measure = () => {
55+
const style = getComputedStyle(bar)
56+
const compactWidth =
57+
bar.clientWidth -
58+
Number.parseFloat(style.paddingLeft) -
59+
Number.parseFloat(style.paddingRight) -
60+
leading.offsetWidth -
61+
trailing.offsetWidth -
62+
Number.parseFloat(getComputedStyle(leading.parentElement!).columnGap) * 2
63+
setExpanded(
64+
value.includes('\n') || text.getBoundingClientRect().width > Math.max(0, compactWidth)
65+
)
66+
}
67+
measure()
68+
const observer = new ResizeObserver(measure)
69+
for (const element of [bar, leading, trailing]) observer.observe(element)
70+
return () => observer.disconnect()
71+
}, [value])
72+
73+
return (
74+
<div
75+
ref={scrollerRef}
76+
className={cn(
77+
'relative min-h-[46px] w-full rounded-[23px] border border-[var(--border-1)] bg-[var(--white)] py-[7px] pr-2.5 pl-4 dark:bg-[var(--surface-4)]',
78+
floating && 'shadow-ambient'
79+
)}
80+
>
81+
<InputToolbar
82+
leadingRef={leadingRef}
83+
trailingRef={trailingRef}
84+
leadingControls={
85+
leadingControls ?? <Search className='size-[16px] shrink-0 text-[var(--text-icon)]' />
86+
}
87+
trailingControls={trailingControls}
88+
expanded={expanded}
89+
editor={
90+
<GrowingTextarea
91+
inputRef={inputRef}
92+
scrollerRef={scrollerRef}
93+
value={value}
94+
compact={!expanded}
95+
maxHeight={200}
96+
onChange={(event) => onChange(event.target.value)}
97+
onKeyDown={(event) => {
98+
if (event.key === 'Enter' && !event.shiftKey && !event.nativeEvent.isComposing) {
99+
event.preventDefault()
100+
onSubmit()
101+
}
102+
}}
103+
onPaste={onPaste}
104+
placeholder={placeholder}
105+
aria-label={ariaLabel}
106+
autoComplete='off'
107+
spellCheck={false}
108+
/>
109+
}
110+
/>
111+
<div aria-hidden className='pointer-events-none absolute h-0 w-0 overflow-hidden'>
112+
<span ref={textMeasureRef} className={cn(PROMPT_TEXT_CLASSES, 'whitespace-pre')}>
113+
{value}
114+
</span>
115+
</div>
116+
</div>
117+
)
118+
}

0 commit comments

Comments
 (0)