@sanity/embeddings-index-ui 3.0.1 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,87 +0,0 @@
1
- import {Box, Button, Card, Flex, Label, Stack, Text} from '@sanity/ui'
2
- import {useCallback} from 'react'
3
-
4
- import {IndexState} from '../api/embeddingsApi'
5
-
6
- export interface IndexListProps {
7
- loading: boolean
8
- selectedIndex?: IndexState
9
- indexes: IndexState[]
10
- onIndexSelected: (index: IndexState) => void
11
- }
12
-
13
- export function IndexList(props: IndexListProps) {
14
- const {loading, selectedIndex, indexes, onIndexSelected} = props
15
- return (
16
- <Card tone="default" style={{opacity: loading ? 0.5 : 1}}>
17
- <Stack space={2}>
18
- <Card borderBottom flex={1} paddingBottom={2} padding={3}>
19
- <Flex>
20
- <Box flex={1}>
21
- <Label muted>Index name</Label>
22
- </Box>
23
- <Box flex={1}>
24
- <Label muted>Dataset</Label>
25
- </Box>
26
- <Box flex={1}>
27
- <Label muted>Status</Label>
28
- </Box>
29
- <Box flex={1}>
30
- <Label muted>Progress</Label>
31
- </Box>
32
- </Flex>
33
- </Card>
34
- {indexes.length ? (
35
- <Stack space={2} style={{maxHeight: 200, overflow: 'auto'}}>
36
- {indexes.map((index) => (
37
- <IndexRow
38
- selectedIndex={selectedIndex}
39
- index={index}
40
- onIndexSelected={onIndexSelected}
41
- key={index.indexName}
42
- />
43
- ))}
44
- </Stack>
45
- ) : (
46
- <Text muted>No indexes found.</Text>
47
- )}
48
- </Stack>
49
- </Card>
50
- )
51
- }
52
-
53
- function IndexRow(props: {
54
- selectedIndex?: IndexState
55
- index: IndexState
56
- onIndexSelected: (index: IndexState) => void
57
- }) {
58
- const {selectedIndex, index, onIndexSelected} = props
59
- const onSelect = useCallback(() => onIndexSelected(index), [onIndexSelected, index])
60
- return (
61
- <Button
62
- tone={selectedIndex?.indexName === index.indexName ? 'primary' : 'default'}
63
- mode={selectedIndex?.indexName === index.indexName ? 'default' : 'ghost'}
64
- onClick={onSelect}
65
- key={index.indexName}
66
- padding={3}
67
- >
68
- <Flex>
69
- <Box flex={1}>
70
- <strong>{index.indexName}</strong>
71
- </Box>
72
- <Box flex={1}>{index.dataset}</Box>
73
- <Box flex={1}>{index.status}</Box>
74
- <Box flex={1}>
75
- {index.startDocumentCount
76
- ? Math.floor(
77
- ((index.startDocumentCount - index.remainingDocumentCount) /
78
- index.startDocumentCount) *
79
- 100,
80
- )
81
- : '?'}
82
- %
83
- </Box>
84
- </Flex>
85
- </Button>
86
- )
87
- }
@@ -1,32 +0,0 @@
1
- import {useCallback, useMemo} from 'react'
2
- import {useRouter} from 'sanity/router'
3
-
4
- import {QueryResult} from '../api/embeddingsApi'
5
- import {SemanticSearchAutocomplete} from '../referenceInput/SemanticSearchAutocomplete'
6
- import {EmbeddingsIndexConfig} from '../schemas/typeDefExtensions'
7
-
8
- export function QueryIndex(props: {indexName: string}) {
9
- const {indexName} = props
10
- const getEmpty = useCallback(() => 'anything', [])
11
- const indexConfig: EmbeddingsIndexConfig = useMemo(
12
- () => ({indexName, maxResults: 8}),
13
- [indexName],
14
- )
15
-
16
- const {resolveIntentLink, navigateUrl} = useRouter()
17
- const onSelect = useCallback(
18
- (hit: QueryResult) => {
19
- navigateUrl({
20
- path: resolveIntentLink('edit', {id: hit.value.documentId, type: hit.value.type}),
21
- })
22
- },
23
- [resolveIntentLink, navigateUrl],
24
- )
25
- return (
26
- <SemanticSearchAutocomplete
27
- getEmptySearchValue={getEmpty}
28
- indexConfig={indexConfig}
29
- onSelect={onSelect}
30
- />
31
- )
32
- }
@@ -1,16 +0,0 @@
1
- import {EarthGlobeIcon} from '@sanity/icons'
2
- import {definePlugin, Tool} from 'sanity'
3
-
4
- import {EmbeddingsIndexTool} from './EmbeddingsIndexTool'
5
-
6
- export const embeddingsIndexTool: Tool = {
7
- name: 'embeddings-index',
8
- title: 'Embeddings',
9
- icon: EarthGlobeIcon,
10
- component: EmbeddingsIndexTool,
11
- }
12
-
13
- export const embeddingsIndexDashboard = definePlugin({
14
- name: '@sanity/embeddings-index-dashboard',
15
- tools: [embeddingsIndexTool],
16
- })
@@ -1,37 +0,0 @@
1
- import {useMemo} from 'react'
2
- import {ObjectSchemaType, Schema} from 'sanity'
3
-
4
- import {isType} from '../utils/types'
5
-
6
- const defaultProjection = '{...}'
7
-
8
- // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
9
- export function useDefaultIndex(schema: Schema, dataset: string) {
10
- const defaultFilter = useMemo(
11
- () =>
12
- `_type in [${schema
13
- .getTypeNames()
14
- .map((n) => schema.get(n))
15
- .filter((schemaType): schemaType is ObjectSchemaType =>
16
- Boolean(schemaType && isType(schemaType, 'document')),
17
- )
18
- .filter(
19
- (documentType) =>
20
- !documentType.name.startsWith('sanity.') &&
21
- !documentType.name.startsWith('assist.') &&
22
- documentType.name !== 'document',
23
- )
24
- .map((documentType) => `"${documentType.name}"`)
25
- .join(',\n')}]`,
26
- [schema],
27
- )
28
-
29
- return useMemo(
30
- () => ({
31
- dataset,
32
- projection: defaultProjection,
33
- filter: defaultFilter,
34
- }),
35
- [defaultFilter, dataset],
36
- )
37
- }
package/src/index.ts DELETED
@@ -1,10 +0,0 @@
1
- import './schemas/typeDefExtensions'
2
-
3
- import {embeddingsIndexDashboard} from './embeddingsIndexDashboard/dashboardPlugin'
4
- import {embeddingsIndexReferenceInput} from './referenceInput/referencePlugin'
5
-
6
- export {embeddingsIndexReferenceInput}
7
-
8
- export {embeddingsIndexDashboard}
9
-
10
- export * from './api/embeddingsApi'
@@ -1,139 +0,0 @@
1
- import {ErrorOutlineIcon} from '@sanity/icons'
2
- import {Box, Button, ButtonProps, Card} from '@sanity/ui'
3
- import {CSSProperties, useMemo} from 'react'
4
- import {useObservable} from 'react-rx'
5
- import {
6
- DefaultPreview,
7
- getPreviewStateObservable,
8
- getPreviewValueWithFallback,
9
- SanityDefaultPreview,
10
- SanityDocument,
11
- SchemaType,
12
- useDocumentPreviewStore,
13
- useSchema,
14
- } from 'sanity'
15
- import {useIntentLink} from 'sanity/router'
16
-
17
- interface ResultPreviewProps {
18
- documentId: string
19
- schemaTypeName: string
20
- button?: boolean
21
- style?: CSSProperties
22
- }
23
-
24
- export function DocumentPreview({
25
- documentId,
26
- style,
27
- schemaTypeName,
28
- ...buttonProps
29
- }: ResultPreviewProps & ButtonProps) {
30
- const schema = useSchema()
31
- const schemaType = schemaTypeName ? schema.get(schemaTypeName) : undefined
32
-
33
- if (!schemaTypeName) {
34
- return (
35
- <Card style={{minHeight: '36px'}}>
36
- <DefaultPreview
37
- withShadow={false}
38
- withBorder={false}
39
- title={'Loading...'}
40
- schemaType={schemaType}
41
- isPlaceholder
42
- />
43
- </Card>
44
- )
45
- }
46
-
47
- if (!schemaType) {
48
- return (
49
- <Card>
50
- <DefaultPreview
51
- withShadow={false}
52
- withBorder={false}
53
- media={() => <ErrorOutlineIcon />}
54
- title={
55
- <>
56
- Unknown type <code>{schemaTypeName ?? 'N/A'}</code> for {documentId}
57
- </>
58
- }
59
- />
60
- </Card>
61
- )
62
- }
63
-
64
- return (
65
- <DocumentPreviewInner
66
- documentId={documentId}
67
- schemaTypeName={schemaTypeName}
68
- schemaType={schemaType}
69
- style={style}
70
- {...buttonProps}
71
- />
72
- )
73
- }
74
-
75
- function DocumentPreviewInner({
76
- documentId,
77
- schemaType,
78
- style,
79
- button,
80
- }: ResultPreviewProps & {schemaType: SchemaType} & ButtonProps) {
81
- const documentPreviewStore = useDocumentPreviewStore()
82
-
83
- const previewStateObservable = useMemo(
84
- () => getPreviewStateObservable(documentPreviewStore, schemaType, documentId),
85
- [documentId, documentPreviewStore, schemaType],
86
- )
87
-
88
- const {
89
- snapshot,
90
- original,
91
- isLoading: previewIsLoading,
92
- } = useObservable(previewStateObservable, {
93
- snapshot: null,
94
- isLoading: true,
95
- original: null,
96
- })
97
-
98
- const sanityDocument = useMemo(() => {
99
- return {
100
- _id: documentId,
101
- _type: schemaType?.name,
102
- } as SanityDocument
103
- }, [documentId, schemaType?.name])
104
-
105
- const {onClick: onIntentClick, href} = useIntentLink({
106
- intent: 'edit',
107
- params: {
108
- id: documentId,
109
- type: schemaType?.name,
110
- },
111
- })
112
-
113
- const preview = (
114
- <SanityDefaultPreview
115
- {...getPreviewValueWithFallback({
116
- snapshot,
117
- original,
118
- fallback: sanityDocument,
119
- })}
120
- isPlaceholder={previewIsLoading ?? true}
121
- layout="default"
122
- icon={schemaType?.icon}
123
- />
124
- )
125
- if (button) {
126
- return (
127
- <Button
128
- as={'a'}
129
- href={href}
130
- onClick={onIntentClick}
131
- mode="ghost"
132
- style={{width: '100%', ...style}}
133
- >
134
- {preview}
135
- </Button>
136
- )
137
- }
138
- return <Box style={{width: '100%'}}>{preview}</Box>
139
- }
@@ -1,213 +0,0 @@
1
- /* eslint-disable max-nested-callbacks */
2
- import {Autocomplete, AutocompleteOpenButtonProps, Box, Button, Flex, Text} from '@sanity/ui'
3
- import {
4
- FocusEventHandler,
5
- forwardRef,
6
- useCallback,
7
- useEffect,
8
- useId,
9
- useMemo,
10
- useRef,
11
- useState,
12
- } from 'react'
13
- import {typed} from 'sanity'
14
-
15
- import {queryIndex, QueryResult} from '../api/embeddingsApi'
16
- import {useApiClient} from '../api/embeddingsApiHooks'
17
- import {DocumentPreview} from '../preview/DocumentPreview'
18
- import {EmbeddingsIndexConfig} from '../schemas/typeDefExtensions'
19
-
20
- export interface SemanticSearchAutocompleteProps {
21
- indexConfig: EmbeddingsIndexConfig
22
- getEmptySearchValue: () => string
23
- typeFilter?: string[]
24
- filterResult?: (hit: QueryResult) => boolean
25
- onSelect?: (value: QueryResult) => void
26
- onFocus?: FocusEventHandler<HTMLInputElement>
27
- onBlur?: FocusEventHandler<HTMLInputElement>
28
- readOnly?: boolean
29
- }
30
-
31
- interface Option {
32
- result: QueryResult
33
- value: string
34
- }
35
-
36
- const NO_RESULTS_VALUE = '' as const
37
-
38
- interface NoResultOption {
39
- value: string
40
- }
41
-
42
- const NO_OPTIONS: NoResultOption[] = []
43
- const NO_FILTER = () => true
44
-
45
- export const SemanticSearchAutocomplete = forwardRef(function SemanticSearchAutocomplete(
46
- props: SemanticSearchAutocompleteProps,
47
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
- ref: any,
49
- ) {
50
- const {
51
- indexConfig,
52
- filterResult,
53
- getEmptySearchValue,
54
- readOnly,
55
- onFocus,
56
- onBlur,
57
- onSelect,
58
- typeFilter,
59
- } = props
60
- const id = useId()
61
- const [query, setQuery] = useState('')
62
- const queryRef = useRef(query)
63
- const debouncedQuery = useDebouncedValue(query, 300)
64
- const prevDebouncedQuery = useRef(debouncedQuery)
65
-
66
- const [searching, setSearching] = useState(false)
67
- const [options, setOptions] = useState<Option[] | NoResultOption[]>(NO_OPTIONS)
68
-
69
- const client = useApiClient()
70
-
71
- const runIndexQuery = useCallback(
72
- (queryString: string) => {
73
- setSearching(true)
74
- const indexName = indexConfig?.indexName
75
- const maxResults = indexConfig?.maxResults
76
-
77
- if (!indexName) {
78
- throw new Error(`Reference option embeddingsIndex.indexName is required, but was missing`)
79
- }
80
-
81
- queryIndex(
82
- {
83
- query: queryString.trim().length ? queryString : (getEmptySearchValue() ?? ''),
84
- indexName,
85
- maxResults,
86
- filter: {
87
- type: typeFilter,
88
- },
89
- },
90
- client,
91
- )
92
- .then((result: QueryResult[]) => {
93
- if (queryRef.current === queryString) {
94
- setSearching(false)
95
- setOptions([])
96
- setOptions([])
97
- const resultOptions = result
98
- .filter((hit) => (filterResult ? filterResult(hit) : true))
99
- .map((r) => typed<Option>({result: r, value: r.value.documentId}))
100
- if (resultOptions.length) {
101
- setOptions(resultOptions)
102
- } else {
103
- setOptions([{value: NO_RESULTS_VALUE}])
104
- }
105
- }
106
- })
107
- .catch((e) => {
108
- if (queryRef.current === queryString) {
109
- setSearching(false)
110
- }
111
- throw e
112
- })
113
- },
114
- [client, indexConfig, getEmptySearchValue, filterResult, typeFilter],
115
- )
116
-
117
- useEffect(() => {
118
- if (prevDebouncedQuery.current !== debouncedQuery) {
119
- runIndexQuery(debouncedQuery)
120
- }
121
- prevDebouncedQuery.current = debouncedQuery
122
- }, [debouncedQuery, runIndexQuery])
123
-
124
- const openButtonConfig: AutocompleteOpenButtonProps = useMemo(
125
- () => ({onClick: () => runIndexQuery(queryRef.current)}),
126
- [runIndexQuery, queryRef],
127
- )
128
-
129
- const handleQueryChange = useCallback(
130
- (newValue: string | null) => {
131
- const newQuery = newValue ?? ''
132
- queryRef.current = newQuery
133
- setQuery(newQuery)
134
- },
135
- [setQuery],
136
- )
137
-
138
- const handleChange = useCallback(
139
- (value: string) => {
140
- if (value === NO_RESULTS_VALUE) {
141
- setOptions(NO_OPTIONS)
142
- return
143
- }
144
- const option = (options as Option[])
145
- .filter((r): r is Option => 'result' in r)
146
- .find((r) => r.result.value.documentId === value)
147
- if (option && onSelect) {
148
- onSelect(option.result)
149
- }
150
- },
151
- [onSelect, options],
152
- )
153
-
154
- return (
155
- <Autocomplete
156
- id={id}
157
- ref={ref}
158
- data-testid="semantic-autocomplete"
159
- placeholder="Type to search..."
160
- openButton={openButtonConfig}
161
- onFocus={onFocus}
162
- onChange={handleChange}
163
- loading={searching}
164
- onBlur={onBlur}
165
- readOnly={readOnly}
166
- filterOption={NO_FILTER}
167
- onQueryChange={handleQueryChange}
168
- options={options}
169
- renderOption={AutocompleteOption}
170
- />
171
- )
172
- })
173
-
174
- function AutocompleteOption(props: Option | NoResultOption) {
175
- if ('result' in props) {
176
- const value = props.result.value
177
- return (
178
- <Button mode="bleed" padding={1} style={{width: '100%'}}>
179
- <Flex gap={2} align="center">
180
- <Box flex={1}>
181
- <DocumentPreview documentId={value.documentId} schemaTypeName={value.type} />
182
- </Box>
183
- <Box padding={2}>
184
- <Text size={1} muted title={'Relevance'}>
185
- {Math.floor(props.result.score * 100)}%
186
- </Text>
187
- </Box>
188
- </Flex>
189
- </Button>
190
- )
191
- }
192
-
193
- return (
194
- <Button mode="bleed" padding={1} style={{width: '100%'}} disabled>
195
- <Flex gap={2} align="center">
196
- No results.
197
- </Flex>
198
- </Button>
199
- )
200
- }
201
-
202
- function useDebouncedValue<T>(value: T, ms: number) {
203
- const [debouncedValue, setDebouncedValue] = useState(value)
204
-
205
- useEffect(() => {
206
- const timeoutId = setTimeout(() => {
207
- setDebouncedValue(value)
208
- }, ms)
209
- return () => clearTimeout(timeoutId)
210
- }, [value, ms])
211
-
212
- return debouncedValue
213
- }
@@ -1,169 +0,0 @@
1
- import {EarthGlobeIcon, LinkIcon} from '@sanity/icons'
2
- import {Box, Button, Flex, Spinner} from '@sanity/ui'
3
- import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
4
- import {
5
- ObjectInputProps,
6
- ReferenceBaseOptions,
7
- ReferenceSchemaType,
8
- set,
9
- setIfMissing,
10
- unset,
11
- } from 'sanity'
12
- import {useDocumentPane} from 'sanity/desk'
13
-
14
- import {QueryResult} from '../api/embeddingsApi'
15
- import {FeatureDisabledNotice, FeatureError, useIsFeatureEnabledContext} from '../api/isEnabled'
16
- import {EmbeddingsIndexConfig} from '../schemas/typeDefExtensions'
17
- import {publicId} from '../utils/id'
18
- import {SemanticSearchAutocomplete} from './SemanticSearchAutocomplete'
19
-
20
- function useEmeddingsConfig(
21
- embeddingsIndexConfig: ReferenceBaseOptions['embeddingsIndex'],
22
- defaultConfig: EmbeddingsIndexConfig | undefined,
23
- ) {
24
- return useMemo(() => {
25
- if (embeddingsIndexConfig === true || !embeddingsIndexConfig) {
26
- if (!defaultConfig?.indexName) {
27
- throw new Error(
28
- 'Default embeddingsIndex config is missing. When options.embeddingsIndex: true, embeddingsIndexReferenceInput plugin config is required.',
29
- )
30
- }
31
- return defaultConfig
32
- }
33
-
34
- const finalConfig = {
35
- ...defaultConfig,
36
- ...embeddingsIndexConfig,
37
- }
38
-
39
- if (!finalConfig?.indexName) {
40
- throw new Error(
41
- 'indexName is missing. Either set it in options.embeddingsIndex or configure defaults using plugin config.',
42
- )
43
- }
44
-
45
- return finalConfig
46
- }, [defaultConfig, embeddingsIndexConfig])
47
- }
48
-
49
- export function SemanticSearchReferenceInput(
50
- props: ObjectInputProps & {defaultConfig?: EmbeddingsIndexConfig},
51
- ) {
52
- const embeddingsIndexConfig = (props.schemaType as ReferenceSchemaType)?.options?.embeddingsIndex
53
-
54
- const config = useEmeddingsConfig(embeddingsIndexConfig, props.defaultConfig)
55
-
56
- const defaultEnabled = config.searchMode === 'embeddings'
57
-
58
- const featureState = useIsFeatureEnabledContext()
59
-
60
- const [semantic, setSemantic] = useState<boolean>(defaultEnabled)
61
- const toggleSemantic = useCallback(() => setSemantic((current) => !current), [])
62
-
63
- return (
64
- <Flex gap={2} flex={1} style={{width: '100%'}}>
65
- {semantic && featureState == 'loading' ? (
66
- <Box padding={2}>
67
- <Spinner />
68
- </Box>
69
- ) : null}
70
-
71
- {semantic && featureState == 'disabled' ? (
72
- <FeatureDisabledNotice urlSuffix="?ref=embeddings-ref" />
73
- ) : null}
74
-
75
- {semantic && featureState === 'error' ? (
76
- <Box padding={4}>
77
- <FeatureError />
78
- </Box>
79
- ) : null}
80
-
81
- <Box flex={1} style={{maxHeight: 36, overflow: 'hidden'}}>
82
- {semantic && featureState == 'enabled' ? (
83
- <SemanticSearchInput {...props} indexConfig={config} />
84
- ) : (
85
- props.renderDefault(props)
86
- )}
87
- </Box>
88
- <Button
89
- icon={semantic ? EarthGlobeIcon : LinkIcon}
90
- onClick={toggleSemantic}
91
- mode="bleed"
92
- title={
93
- semantic ? 'Switch to standard reference search' : 'Switch to semantic reference search'
94
- }
95
- />
96
- </Flex>
97
- )
98
- }
99
-
100
- function SemanticSearchInput(props: ObjectInputProps & {indexConfig: EmbeddingsIndexConfig}) {
101
- const {indexConfig, onPathFocus, onChange, readOnly, schemaType, value} = props
102
-
103
- const {value: currentDocument} = useDocumentPane()
104
- const docRef = useRef(currentDocument)
105
- const autocompleteRef = useRef<HTMLInputElement>(null)
106
-
107
- useEffect(() => {
108
- docRef.current = currentDocument
109
- }, [currentDocument])
110
-
111
- useEffect(() => {
112
- // if this component is rendered, and there is a value, replace was selected
113
- if (value?._ref) {
114
- autocompleteRef.current?.focus()
115
- }
116
- // intentional empty deps
117
- // eslint-disable-next-line react-hooks/exhaustive-deps
118
- }, [])
119
-
120
- const handleFocus = useCallback(() => onPathFocus(['_ref']), [onPathFocus])
121
- const handleBlur = useCallback(() => onPathFocus([]), [onPathFocus])
122
-
123
- const handleChange = useCallback(
124
- (result: QueryResult) => {
125
- if (!result) {
126
- onChange(unset())
127
- onPathFocus([])
128
- return
129
- }
130
- const patches = [
131
- setIfMissing({}),
132
- set(schemaType.name, ['_type']),
133
- set(publicId(result.value.documentId), ['_ref']),
134
- unset(['_weak']),
135
- unset(['_strengthenOnPublish']),
136
- ]
137
-
138
- onChange(patches)
139
- // Move focus away from _ref and one level up
140
- onPathFocus([])
141
- },
142
- [onChange, onPathFocus, schemaType.name],
143
- )
144
-
145
- const filterResult = useCallback(
146
- (r: QueryResult) => r.value.documentId !== publicId(docRef.current._id),
147
- [docRef],
148
- )
149
-
150
- const getEmptySearchValue = useCallback(() => JSON.stringify(docRef.current), [docRef])
151
- const typeFilter = useMemo(
152
- () => (schemaType as ReferenceSchemaType).to.map((refType) => refType.name),
153
- [schemaType],
154
- )
155
-
156
- return (
157
- <SemanticSearchAutocomplete
158
- ref={autocompleteRef}
159
- typeFilter={typeFilter}
160
- indexConfig={indexConfig}
161
- onSelect={handleChange}
162
- onFocus={handleFocus}
163
- onBlur={handleBlur}
164
- getEmptySearchValue={getEmptySearchValue}
165
- filterResult={filterResult}
166
- readOnly={readOnly}
167
- />
168
- )
169
- }