Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { ReactNode } from 'react'
import { cn, pageHeadingClassName } from '@sim/emcn'

interface OrganizationLandingProps {
heading: string
children: ReactNode
}

/** Keeps the Home and Search composers centered independently of the shortcuts below them. */
export function OrganizationLanding({ heading, children }: OrganizationLandingProps) {
return (
<div className='min-h-0 flex-1 overflow-y-auto [scrollbar-gutter:stable_both-edges]'>
<div className='flex min-h-full flex-col items-center justify-center px-6 pt-[2vh] pb-[22vh]'>
<h1 className={cn(pageHeadingClassName, 'mb-7 max-w-chat')}>{heading}</h1>
<div className='relative w-full max-w-chat'>{children}</div>
</div>
</div>
)
}
146 changes: 74 additions & 72 deletions apps/sim/app/o/[organizationId]/components/search-landing-history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,86 +29,88 @@ export function SearchLandingHistory({
const queries = data?.queries.slice(0, 5) ?? []
const selected = selection ?? (sources.length > 0 ? 'sources' : 'queries')
return (
<div className={cn('w-full min-w-0', inter.className)}>
<div className={cn('relative w-full min-w-0', inter.className)}>
{children}
{(sources.length > 0 || queries.length > 0) && (
<section aria-label='Recent activity' className='mt-6 px-2'>
<div className='mb-2 flex flex-wrap items-center justify-between gap-2'>
<div role='group' aria-label='History type' className='flex items-center gap-1'>
<div className='absolute inset-x-0 top-full'>
{(sources.length > 0 || queries.length > 0) && (
<section aria-label='Recent activity' className='mt-6 px-2'>
<div className='mb-2 flex flex-wrap items-center justify-between gap-2'>
<div role='group' aria-label='History type' className='flex items-center gap-1'>
<Chip
active={selected === 'sources'}
aria-pressed={selected === 'sources'}
onClick={() => setSelection('sources')}
>
Recently viewed
</Chip>
<Chip
active={selected === 'queries'}
aria-pressed={selected === 'queries'}
onClick={() => setSelection('queries')}
>
Recent searches
</Chip>
</div>
<Chip
active={selected === 'sources'}
aria-pressed={selected === 'sources'}
onClick={() => setSelection('sources')}
disabled={clear.isPending}
onClick={() =>
clear.mutate(undefined, { onError: (error) => toast.error(error.message) })
}
>
Recently viewed
Clear history
</Chip>
<Chip
active={selected === 'queries'}
aria-pressed={selected === 'queries'}
onClick={() => setSelection('queries')}
</div>
<div className='grid grid-cols-1'>
<div
className={cn(
'col-start-1 row-start-1 min-w-0',
selected !== 'sources' && 'invisible'
)}
inert={selected !== 'sources'}
aria-hidden={selected !== 'sources'}
>
Recent searches
</Chip>
{sources.length > 0 ? (
sources.map((source) => <SourceCard key={source.url} source={source} dense />)
) : (
<p className='px-2 py-2 text-[var(--text-tertiary)] text-small'>
Sources you open will appear here.
</p>
)}
</div>
<div
className={cn(
'col-start-1 row-start-1 min-w-0',
selected !== 'queries' && 'invisible'
)}
inert={selected !== 'queries'}
aria-hidden={selected !== 'queries'}
>
{queries.length > 0 ? (
queries.map(({ query }) => (
<div key={query} className='py-1'>
<Chip fullWidth leftIcon={Clock} onClick={() => onSearch(query)}>
{query}
</Chip>
</div>
))
) : (
<p className='px-2 py-2 text-[var(--text-tertiary)] text-small'>
Your recent searches will appear here.
</p>
)}
</div>
</div>
<Chip
disabled={clear.isPending}
onClick={() =>
clear.mutate(undefined, { onError: (error) => toast.error(error.message) })
}
>
Clear history
</section>
)}
{history.isError && (
<div className='mt-6 flex items-center gap-2 px-4 text-[var(--text-tertiary)] text-small'>
Recent activity couldn’t load.
<Chip onClick={() => void history.refetch()} disabled={history.isFetching}>
Try again
</Chip>
</div>
<div className='grid grid-cols-1'>
<div
className={cn(
'col-start-1 row-start-1 min-w-0',
selected !== 'sources' && 'invisible'
)}
inert={selected !== 'sources'}
aria-hidden={selected !== 'sources'}
>
{sources.length > 0 ? (
sources.map((source) => <SourceCard key={source.url} source={source} dense />)
) : (
<p className='px-2 py-2 text-[var(--text-tertiary)] text-small'>
Sources you open will appear here.
</p>
)}
</div>
<div
className={cn(
'col-start-1 row-start-1 min-w-0',
selected !== 'queries' && 'invisible'
)}
inert={selected !== 'queries'}
aria-hidden={selected !== 'queries'}
>
{queries.length > 0 ? (
queries.map(({ query }) => (
<div key={query} className='py-1'>
<Chip fullWidth leftIcon={Clock} onClick={() => onSearch(query)}>
{query}
</Chip>
</div>
))
) : (
<p className='px-2 py-2 text-[var(--text-tertiary)] text-small'>
Your recent searches will appear here.
</p>
)}
</div>
</div>
</section>
)}
{history.isError && (
<div className='mt-6 flex items-center gap-2 px-4 text-[var(--text-tertiary)] text-small'>
Recent activity couldn’t load.
<Chip onClick={() => void history.refetch()} disabled={history.isFetching}>
Try again
</Chip>
</div>
)}
)}
</div>
</div>
)
}
48 changes: 22 additions & 26 deletions apps/sim/app/o/[organizationId]/home/organization-home.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { useCallback, useEffect, useState, useSyncExternalStore } from 'react'
import { cn, pageHeadingClassName, toast } from '@sim/emcn'
import { toast } from '@sim/emcn'
import { useQueryClient } from '@tanstack/react-query'
import Link from 'next/link'
import { useQueryStates } from 'nuqs'
Expand All @@ -16,6 +16,7 @@ import {
getMothershipAttachmentUrl,
} from '@/lib/mothership/chat/attachment-preview'
import { createSearchResource } from '@/lib/mothership/resources/search'
import { OrganizationLanding } from '@/app/o/[organizationId]/components/organization-landing'
import { Composer } from '@/app/o/[organizationId]/home/components/composer'
import { GetStarted } from '@/app/o/[organizationId]/home/components/get-started'
import { organizationHomeParsers } from '@/app/o/[organizationId]/home/search-params'
Expand Down Expand Up @@ -396,32 +397,27 @@ function OrganizationHomeContent({
}
/>
) : (
<div className='min-h-0 flex-1 overflow-y-auto [scrollbar-gutter:stable_both-edges]'>
{/* Asymmetric padding biases the group up so the full cluster (heading + input + steps) sits at the optical center */}
<div className='flex min-h-full flex-col items-center justify-center px-6 pt-[2vh] pb-[22vh]'>
<h1 className={cn(pageHeadingClassName, 'mb-7 max-w-chat')}>
{requestMode === 'assistant'
? `Search ${organization.name}`
: requestMode === 'plan'
? `What should we understand and plan${firstName ? `, ${firstName}` : ''}?`
: `What should we get done${firstName ? `, ${firstName}` : ''}?`}
</h1>
<div className='relative w-full max-w-chat'>
{composer}
{/* Anchored out of flow so expanding/collapsing never shifts the centered input */}
<div className='absolute inset-x-0 top-full'>
{requestMode === 'agent' ? (
<SuggestedActions
organizationId={organization.id}
onSelectPrompt={(prompt) => setDraft(mentionifyIntegrations(prompt))}
/>
) : searchAccess.memberScoped ? (
<GetStarted />
) : null}
</div>
</div>
<OrganizationLanding
heading={
requestMode === 'assistant'
? `Search ${organization.name}`
: requestMode === 'plan'
? `What should we understand and plan${firstName ? `, ${firstName}` : ''}?`
: `What should we get done${firstName ? `, ${firstName}` : ''}?`
}
>
{composer}
<div className='absolute inset-x-0 top-full'>
{requestMode === 'agent' ? (
<SuggestedActions
organizationId={organization.id}
onSelectPrompt={(prompt) => setDraft(mentionifyIntegrations(prompt))}
/>
) : searchAccess.memberScoped ? (
<GetStarted />
) : null}
</div>
</div>
</OrganizationLanding>
)}
</div>
)
Expand Down
27 changes: 8 additions & 19 deletions apps/sim/app/o/[organizationId]/search/search-results-view.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { type ReactNode, useRef } from 'react'
import {
cn,
pageHeadingClassName,
scrollFadeAttributes,
scrollFadeClass,
useScrollEdges,
} from '@sim/emcn'
import { cn, scrollFadeAttributes, scrollFadeClass, useScrollEdges } from '@sim/emcn'
import { HEADER_ACTION_CLUSTER, PAGE_HEADER_BAR } from '@/components/page-header-bar'
import type { WorkspaceSearchFilters } from '@/lib/api/contracts/knowledge'
import type { SearchResource } from '@/lib/mothership/generated/resources'
import { OrganizationLanding } from '@/app/o/[organizationId]/components/organization-landing'
import { PAGE_COLUMN_CLASS } from '@/app/o/[organizationId]/components/organization-page'
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
import { KnowledgeSearchResults } from '@/app/workspace/[workspaceId]/home/components/knowledge-search-results'
Expand Down Expand Up @@ -42,11 +37,11 @@ export function SearchResultsView({
})
return (
<div className='flex h-full min-h-0 flex-col bg-[var(--bg)]'>
<div className={PAGE_HEADER_BAR}>
<div className={HEADER_ACTION_CLUSTER} />
</div>
{searching ? (
<>
<div className={PAGE_HEADER_BAR}>
<div className={HEADER_ACTION_CLUSTER} />
</div>
<div className={cn(PAGE_COLUMN_CLASS, SIDEBAR_DIVIDER_PAD_ABOVE_CLASS, 'shrink-0 pt-8')}>
{composer}
</div>
Expand All @@ -73,15 +68,9 @@ export function SearchResultsView({
</div>
</>
) : (
<div className='min-h-0 flex-1 overflow-y-auto [scrollbar-gutter:stable_both-edges]'>
{/* Asymmetric padding biases the group up so heading and field sit at the optical center, as on Home */}
<div className='flex min-h-full flex-col items-center justify-center px-6 pt-[2vh] pb-[22vh]'>
<h1 className={cn(pageHeadingClassName, 'mb-7 max-w-chat')}>
Search {organization.name}
</h1>
<div className='w-full max-w-chat'>{composer}</div>
</div>
</div>
<OrganizationLanding heading={`Search ${organization.name}`}>
{composer}
</OrganizationLanding>
)}
</div>
)
Expand Down
32 changes: 15 additions & 17 deletions apps/sim/app/o/[organizationId]/search/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useEffect, useRef } from 'react'
import { ComposerActionButton, toast } from '@sim/emcn'
import { ArrowUp, Loader } from '@sim/emcn/icons'
import { ArrowUp, StopFilled } from '@sim/emcn/icons'
import { useIsFetching, useQueryClient } from '@tanstack/react-query'
import { useRouter } from 'next/navigation'
import { useQueryStates } from 'nuqs'
Expand Down Expand Up @@ -39,14 +39,13 @@ interface SearchFieldProps {
function SearchField({ userId, initialValue, onSubmit }: SearchFieldProps) {
const inputRef = useRef<HTMLTextAreaElement>(null)
const { organization } = useOrganizationContext()
const isSearching =
useIsFetching({
queryKey: knowledgeKeys.searchQuery(
resourceScopeKey({ kind: 'organization', organizationId: organization.id }),
initialValue.trim(),
userId
),
}) > 0
const queryClient = useQueryClient()
const queryKey = knowledgeKeys.searchQuery(
resourceScopeKey({ kind: 'organization', organizationId: organization.id }),
initialValue.trim(),
userId
)
const isSearching = useIsFetching({ queryKey }) > 0
const latestDraftKey = `${userId}:organization:${organization.id}:search`
const latestDraft = useMothershipDraftsStore((state) => state.drafts[latestDraftKey])
const ownerQuery = initialValue || latestDraft?.searchQuery || ''
Expand All @@ -60,7 +59,6 @@ function SearchField({ userId, initialValue, onSubmit }: SearchFieldProps) {
setDraft(draftKey, payload)
setDraft(latestDraftKey, payload)
}
const pending = isSearching && value.trim() === initialValue.trim()
const submit = (text = value) => {
if (!text.trim() || (isSearching && text.trim() === initialValue.trim())) return
const { clearDraft } = useMothershipDraftsStore.getState()
Expand Down Expand Up @@ -99,14 +97,14 @@ function SearchField({ userId, initialValue, onSubmit }: SearchFieldProps) {
submitControl={
<ComposerActionButton
type='button'
onClick={() => submit()}
disabled={!canSubmit || pending}
aria-label={pending ? 'Searching' : 'Search'}
aria-busy={pending}
active={canSubmit}
onClick={() => (isSearching ? void queryClient.cancelQueries({ queryKey }) : submit())}
disabled={!canSubmit && !isSearching}
aria-label={isSearching ? 'Stop search' : 'Search'}
aria-busy={isSearching}
active={canSubmit || isSearching}
>
{pending ? (
<Loader className='size-[16px] animate-spin text-white motion-reduce:animate-none dark:text-black' />
{isSearching ? (
<StopFilled className='block size-[14px] fill-white dark:fill-black' />
) : (
<ArrowUp className='block size-[16px] text-white dark:text-black' />
)}
Expand Down
Loading
Loading