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
Expand Up @@ -8,7 +8,6 @@ import { truncate } from '@sim/utils/string'
import { ChevronDown, ChevronUp, FileText, Pencil, Tag } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { useQueryStates } from 'nuqs'
import { SearchHighlight } from '@/components/ui/search-highlight'
import type { ChunkData } from '@/lib/knowledge/types'
import { formatTokenCount } from '@/lib/tokenization'
import type {
Expand Down Expand Up @@ -38,7 +37,7 @@ import {
documentParsers,
documentUrlKeys,
} from '@/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/search-params'
import { ActionBar } from '@/app/workspace/[workspaceId]/knowledge/[id]/components'
import { ActionBar, SearchHighlight } from '@/app/workspace/[workspaceId]/knowledge/[id]/components'
import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import { AlertCircle, Pencil, Plus, Tag, X } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { useQueryState, useQueryStates } from 'nuqs'
import { usePostHog } from 'posthog-js/react'
import { SearchHighlight } from '@/components/ui/search-highlight'
import { ALL_TAG_SLOTS, type AllTagSlot, getFieldTypeForSlot } from '@/lib/knowledge/constants'
import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types'
import { type FilterFieldType, getOperatorsForFieldType } from '@/lib/knowledge/filters/types'
Expand Down Expand Up @@ -59,6 +58,7 @@ import {
ConnectorsSection,
DocumentContextMenu,
RenameDocumentModal,
SearchHighlight,
} from '@/app/workspace/[workspaceId]/knowledge/[id]/components'
import {
addConnectorParam,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { ConnectorsSection } from './connectors-section'
export { DocumentContextMenu } from './document-context-menu'
export { EditConnectorModal } from './edit-connector-modal'
export { RenameDocumentModal } from './rename-document-modal'
export { SearchHighlight } from './search-highlight'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SearchHighlight } from './search-highlight'
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ interface SearchHighlightProps {
className?: string
}

/**
* Renders `text` with substrings matching any whitespace-separated term of
* `searchQuery` wrapped in the highlight-match colors. Falls back to plain
* text when the query is empty.
*/
export function SearchHighlight({ text, searchQuery, className = '' }: SearchHighlightProps) {
if (!searchQuery.trim()) {
return <span className={className}>{text}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,9 @@ import { cn } from '@sim/emcn'
import { File, Workflow } from '@sim/emcn/icons'
import { Command } from 'cmdk'
import type { CommandItemProps } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils'
import {
COMMAND_ITEM_CLASSNAME,
fuzzyMatch,
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils'
import { COMMAND_ITEM_CLASSNAME } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils'
import { getTileIconColorClass } from '@/blocks/icon-color'

interface Segment {
text: string
hit: boolean
}

function buildSegments(text: string, positions: readonly number[]): Segment[] {
const hits = new Set(positions)
const segments: Segment[] = []
for (let i = 0; i < text.length; i++) {
const hit = hits.has(i)
const last = segments[segments.length - 1]
if (last && last.hit === hit) last.text += text[i]
else segments.push({ text: text[i], hit })
}
return segments
}

/**
* Renders `text` with the characters that match `query` emphasized. Falls back
* to plain text when there is no query or no positional match against the
* display text (e.g. the row matched on a hidden id rather than its label).
*/
export const HighlightedText = memo(
function HighlightedText({ text, query }: { text: string; query?: string }) {
if (!query) return <>{text}</>
const { positions } = fuzzyMatch(text, query)
if (positions.length === 0) return <>{text}</>
return (
<>
{buildSegments(text, positions).map((segment, index) =>
segment.hit ? (
<span key={index} className='font-medium'>
{segment.text}
</span>
) : (
<span key={index}>{segment.text}</span>
)
)}
</>
)
},
(prev, next) => prev.text === next.text && prev.query === next.query
)

export const MemoizedCommandItem = memo(
function CommandItem({
value,
Expand All @@ -64,7 +17,6 @@ export const MemoizedCommandItem = memo(
bgColor,
showColoredIcon,
label,
query,
}: CommandItemProps) {
return (
<Command.Item value={value} onSelect={onSelect} className={COMMAND_ITEM_CLASSNAME}>
Expand All @@ -81,9 +33,7 @@ export const MemoizedCommandItem = memo(
)}
/>
</div>
<span className='truncate text-[var(--text-body)]'>
<HighlightedText text={label} query={query} />
</span>
<span className='truncate text-[var(--text-body)]'>{label}</span>
</Command.Item>
)
},
Expand All @@ -92,8 +42,7 @@ export const MemoizedCommandItem = memo(
prev.icon === next.icon &&
prev.bgColor === next.bgColor &&
prev.showColoredIcon === next.showColoredIcon &&
prev.label === next.label &&
prev.query === next.query
prev.label === next.label
)

export const MemoizedActionItem = memo(
Expand All @@ -103,21 +52,17 @@ export const MemoizedActionItem = memo(
icon: Icon,
name,
shortcut,
query,
}: {
value: string
onSelect: () => void
icon: ComponentType<{ className?: string }>
name: string
shortcut?: string
query?: string
}) {
return (
<Command.Item value={value} onSelect={onSelect} className={COMMAND_ITEM_CLASSNAME}>
<Icon className='size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='truncate text-[var(--text-body)]'>
<HighlightedText text={name} query={query} />
</span>
<span className='truncate text-[var(--text-body)]'>{name}</span>
{shortcut && (
<span className='ml-auto flex-shrink-0 text-[var(--text-subtle)] text-small'>
{shortcut}
Expand All @@ -130,8 +75,7 @@ export const MemoizedActionItem = memo(
prev.value === next.value &&
prev.icon === next.icon &&
prev.name === next.name &&
prev.shortcut === next.shortcut &&
prev.query === next.query
prev.shortcut === next.shortcut
)

export const MemoizedWorkflowItem = memo(
Expand All @@ -141,24 +85,20 @@ export const MemoizedWorkflowItem = memo(
name,
folderPath,
isCurrent,
query,
}: {
value: string
onSelect: () => void
name: string
folderPath?: string[]
isCurrent?: boolean
query?: string
}) {
return (
<Command.Item value={value} onSelect={onSelect} className={COMMAND_ITEM_CLASSNAME}>
<div className='relative flex size-[16px] flex-shrink-0 items-center justify-center'>
<Workflow className='size-[14px] text-[var(--text-icon)]' />
</div>
<span className='flex min-w-0 max-w-[75%] flex-shrink-0 text-[var(--text-body)]'>
<span className='truncate'>
<HighlightedText text={name} query={query} />
</span>
<span className='truncate'>{name}</span>
{isCurrent && <span className='flex-shrink-0 whitespace-pre'> (current)</span>}
</span>
{folderPath && folderPath.length > 0 && (
Expand All @@ -181,7 +121,6 @@ export const MemoizedWorkflowItem = memo(
prev.value === next.value &&
prev.name === next.name &&
prev.isCurrent === next.isCurrent &&
prev.query === next.query &&
(prev.folderPath === next.folderPath ||
(prev.folderPath?.length === next.folderPath?.length &&
(prev.folderPath ?? []).every((segment, i) => segment === next.folderPath?.[i])))
Expand All @@ -193,23 +132,19 @@ export const MemoizedFileItem = memo(
onSelect,
name,
folderPath,
query,
}: {
value: string
onSelect: () => void
name: string
folderPath?: string[]
query?: string
}) {
return (
<Command.Item value={value} onSelect={onSelect} className={COMMAND_ITEM_CLASSNAME}>
<div className='relative flex size-[16px] flex-shrink-0 items-center justify-center'>
<File className='size-[14px] text-[var(--text-icon)]' />
</div>
<span className='flex min-w-0 max-w-[75%] flex-shrink-0 font-base text-[var(--text-body)]'>
<span className='truncate'>
<HighlightedText text={name} query={query} />
</span>
<span className='truncate'>{name}</span>
</span>
{folderPath && folderPath.length > 0 && (
<span className='ml-auto flex min-w-0 pl-2 font-base text-[var(--text-subtle)] text-small'>
Expand All @@ -230,7 +165,6 @@ export const MemoizedFileItem = memo(
(prev, next) =>
prev.value === next.value &&
prev.name === next.name &&
prev.query === next.query &&
(prev.folderPath === next.folderPath ||
(prev.folderPath?.length === next.folderPath?.length &&
(prev.folderPath ?? []).every((segment, i) => segment === next.folderPath?.[i])))
Expand All @@ -241,22 +175,18 @@ export const MemoizedTaskItem = memo(
value,
onSelect,
name,
query,
}: {
value: string
onSelect: () => void
name: string
query?: string
}) {
return (
<Command.Item value={value} onSelect={onSelect} className={COMMAND_ITEM_CLASSNAME}>
<span className='truncate text-[var(--text-body)]'>
<HighlightedText text={name} query={query} />
</span>
<span className='truncate text-[var(--text-body)]'>{name}</span>
</Command.Item>
)
},
(prev, next) => prev.value === next.value && prev.name === next.name && prev.query === next.query
(prev, next) => prev.value === next.value && prev.name === next.name
)

export const MemoizedWorkspaceItem = memo(
Expand All @@ -265,30 +195,23 @@ export const MemoizedWorkspaceItem = memo(
onSelect,
name,
isCurrent,
query,
}: {
value: string
onSelect: () => void
name: string
isCurrent?: boolean
query?: string
}) {
return (
<Command.Item value={value} onSelect={onSelect} className={COMMAND_ITEM_CLASSNAME}>
<span className='flex min-w-0 text-[var(--text-body)]'>
<span className='truncate'>
<HighlightedText text={name} query={query} />
</span>
<span className='truncate'>{name}</span>
{isCurrent && <span className='flex-shrink-0 whitespace-pre'> (current)</span>}
</span>
</Command.Item>
)
},
(prev, next) =>
prev.value === next.value &&
prev.name === next.name &&
prev.isCurrent === next.isCurrent &&
prev.query === next.query
prev.value === next.value && prev.name === next.name && prev.isCurrent === next.isCurrent
)

export const MemoizedPageItem = memo(
Expand All @@ -298,21 +221,17 @@ export const MemoizedPageItem = memo(
icon: Icon,
name,
shortcut,
query,
}: {
value: string
onSelect: () => void
icon: ComponentType<{ className?: string }>
name: string
shortcut?: string
query?: string
}) {
return (
<Command.Item value={value} onSelect={onSelect} className={COMMAND_ITEM_CLASSNAME}>
<Icon className='size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='truncate text-[var(--text-body)]'>
<HighlightedText text={name} query={query} />
</span>
<span className='truncate text-[var(--text-body)]'>{name}</span>
{shortcut && (
<span className='ml-auto flex-shrink-0 text-[var(--text-subtle)] text-small'>
{shortcut}
Expand All @@ -325,8 +244,7 @@ export const MemoizedPageItem = memo(
prev.value === next.value &&
prev.icon === next.icon &&
prev.name === next.name &&
prev.shortcut === next.shortcut &&
prev.query === next.query
prev.shortcut === next.shortcut
)

export const MemoizedIconItem = memo(
Expand All @@ -335,26 +253,18 @@ export const MemoizedIconItem = memo(
onSelect,
name,
icon: Icon,
query,
}: {
value: string
onSelect: () => void
name: string
icon: ComponentType<{ className?: string }>
query?: string
}) {
return (
<Command.Item value={value} onSelect={onSelect} className={COMMAND_ITEM_CLASSNAME}>
<Icon className='size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='truncate text-[var(--text-body)]'>
<HighlightedText text={name} query={query} />
</span>
<span className='truncate text-[var(--text-body)]'>{name}</span>
</Command.Item>
)
},
(prev, next) =>
prev.value === next.value &&
prev.name === next.name &&
prev.icon === next.icon &&
prev.query === next.query
(prev, next) => prev.value === next.value && prev.name === next.name && prev.icon === next.icon
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export {
HighlightedText,
MemoizedActionItem,
MemoizedCommandItem,
MemoizedFileItem,
Expand Down
Loading
Loading