@sanity/embeddings-index-ui 1.0.3 → 1.1.0

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,33 +1,58 @@
1
- import {ObjectInputProps, ReferenceSchemaType, set, setIfMissing, typed, unset} from 'sanity'
2
1
  import {
3
- Autocomplete,
4
- Box,
5
- Button,
6
- Flex,
7
- Text,
8
- AutocompleteOpenButtonProps,
9
- Spinner,
10
- } from '@sanity/ui'
2
+ ObjectInputProps,
3
+ ReferenceBaseOptions,
4
+ ReferenceSchemaType,
5
+ set,
6
+ setIfMissing,
7
+ unset,
8
+ } from 'sanity'
9
+ import {Box, Button, Flex, Spinner} from '@sanity/ui'
11
10
  import {EarthGlobeIcon, LinkIcon} from '@sanity/icons'
12
- import {useCallback, useEffect, useId, useMemo, useRef, useState} from 'react'
13
- import {DocumentPreview} from '../preview/DocumentPreview'
11
+ import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
14
12
  import {useDocumentPane} from 'sanity/desk'
15
- import {queryIndex, QueryResult} from '../api/embeddingsApi'
13
+ import {QueryResult} from '../api/embeddingsApi'
16
14
  import {publicId} from '../utils/id'
17
- import {useApiClient} from '../api/embeddingsApiHooks'
18
15
  import {FeatureDisabledNotice, useIsFeatureEnabledContext} from '../api/isEnabled'
16
+ import {EmbeddingsIndexConfig} from '../schemas/typeDefExtensions'
17
+ import {SemanticSearchAutocomplete} from './SemanticSearchAutocomplete'
18
+
19
+ function useEmeddingsConfig(
20
+ embeddingsIndexConfig: ReferenceBaseOptions['embeddingsIndex'],
21
+ defaultConfig: EmbeddingsIndexConfig | undefined,
22
+ ) {
23
+ return useMemo(() => {
24
+ if (embeddingsIndexConfig === true || !embeddingsIndexConfig) {
25
+ if (!defaultConfig?.indexName) {
26
+ throw new Error(
27
+ 'Default embeddingsIndex config is missing. When options.embeddingsIndex: true, embeddingsIndexReferenceInput plugin config is required.',
28
+ )
29
+ }
30
+ return defaultConfig
31
+ }
32
+
33
+ const finalConfig = {
34
+ ...defaultConfig,
35
+ ...embeddingsIndexConfig,
36
+ }
19
37
 
20
- interface Option {
21
- result: QueryResult
22
- value: string
38
+ if (!finalConfig?.indexName) {
39
+ throw new Error(
40
+ 'indexName is missing. Either set it in options.embeddingsIndex or configure defaults using plugin config.',
41
+ )
42
+ }
43
+
44
+ return finalConfig
45
+ }, [defaultConfig, embeddingsIndexConfig])
23
46
  }
24
47
 
25
- const NO_OPTIONS: Option[] = []
26
- const NO_FILTER = () => true
48
+ export function SemanticSearchReferenceInput(
49
+ props: ObjectInputProps & {defaultConfig?: EmbeddingsIndexConfig},
50
+ ) {
51
+ const embeddingsIndexConfig = (props.schemaType as ReferenceSchemaType)?.options?.embeddingsIndex
27
52
 
28
- export function SemanticSearchReferenceInput(props: ObjectInputProps) {
29
- const defaultEnabled =
30
- (props.schemaType as ReferenceSchemaType)?.options?.embeddingsIndex?.searchMode === 'embeddings'
53
+ const config = useEmeddingsConfig(embeddingsIndexConfig, props.defaultConfig)
54
+
55
+ const defaultEnabled = config.searchMode === 'embeddings'
31
56
 
32
57
  const featureState = useIsFeatureEnabledContext()
33
58
 
@@ -46,7 +71,7 @@ export function SemanticSearchReferenceInput(props: ObjectInputProps) {
46
71
 
47
72
  <Box flex={1} style={{maxHeight: 36, overflow: 'hidden'}}>
48
73
  {semantic && featureState == 'enabled' ? (
49
- <SemanticSearchInput {...props} />
74
+ <SemanticSearchInput {...props} indexConfig={config} />
50
75
  ) : (
51
76
  props.renderDefault(props)
52
77
  )}
@@ -63,38 +88,13 @@ export function SemanticSearchReferenceInput(props: ObjectInputProps) {
63
88
  )
64
89
  }
65
90
 
66
- function useDebouncedValue<T>(value: T, ms: number) {
67
- const [debouncedValue, setDebouncedValue] = useState(value)
68
-
69
- useEffect(() => {
70
- const timeoutId = setTimeout(() => {
71
- setDebouncedValue(value)
72
- }, ms)
73
- return () => clearTimeout(timeoutId)
74
- }, [value, ms])
75
-
76
- return debouncedValue
77
- }
78
-
79
- function SemanticSearchInput(props: ObjectInputProps) {
80
- const {onPathFocus, onChange, readOnly, schemaType, value} = props
91
+ function SemanticSearchInput(props: ObjectInputProps & {indexConfig: EmbeddingsIndexConfig}) {
92
+ const {indexConfig, onPathFocus, onChange, readOnly, schemaType, value} = props
81
93
 
82
94
  const {value: currentDocument} = useDocumentPane()
83
95
  const docRef = useRef(currentDocument)
84
96
  const autocompleteRef = useRef<HTMLInputElement>(null)
85
97
 
86
- const id = useId()
87
-
88
- const [query, setQuery] = useState('')
89
- const queryRef = useRef(query)
90
- const debouncedQuery = useDebouncedValue(query, 300)
91
- const prevDebouncedQuery = useRef(debouncedQuery)
92
-
93
- const [searching, setSearching] = useState(false)
94
- const [options, setOptions] = useState(NO_OPTIONS)
95
-
96
- const client = useApiClient()
97
-
98
98
  useEffect(() => {
99
99
  docRef.current = currentDocument
100
100
  }, [currentDocument])
@@ -111,58 +111,6 @@ function SemanticSearchInput(props: ObjectInputProps) {
111
111
  const handleFocus = useCallback(() => onPathFocus(['_ref']), [onPathFocus])
112
112
  const handleBlur = useCallback(() => onPathFocus([]), [onPathFocus])
113
113
 
114
- const runIndexQuery = useCallback(
115
- (queryString: string) => {
116
- setSearching(true)
117
- const refSchema = schemaType as ReferenceSchemaType
118
- const indexName = refSchema.options?.embeddingsIndex?.indexName
119
- const maxResults = refSchema.options?.embeddingsIndex?.maxResults
120
- const typeFilter = refSchema.to.map((ref) => ref.name)
121
-
122
- if (!indexName) {
123
- throw new Error(
124
- `Reference option embeddingsIndex.indexName is required, but was missing in type ${refSchema.name}`,
125
- )
126
- }
127
-
128
- queryIndex(
129
- {
130
- query: queryString.trim().length ? queryString : JSON.stringify(docRef.current) ?? '',
131
- indexName,
132
- maxResults,
133
- filter: {
134
- type: typeFilter,
135
- },
136
- },
137
- client,
138
- )
139
- .then((result: QueryResult[]) => {
140
- if (queryRef.current === queryString) {
141
- setSearching(false)
142
- setOptions(
143
- result
144
- .filter((r) => r.value.documentId !== publicId(docRef.current._id))
145
- .map((r) => typed<Option>({result: r, value: r.value.documentId})),
146
- )
147
- }
148
- })
149
- .catch((e) => {
150
- if (queryRef.current === queryString) {
151
- setSearching(false)
152
- }
153
- throw e
154
- })
155
- },
156
- [client, schemaType],
157
- )
158
-
159
- useEffect(() => {
160
- if (prevDebouncedQuery.current !== debouncedQuery) {
161
- runIndexQuery(debouncedQuery)
162
- }
163
- prevDebouncedQuery.current = debouncedQuery
164
- }, [debouncedQuery, runIndexQuery])
165
-
166
114
  const handleChange = useCallback(
167
115
  (nextId: string) => {
168
116
  if (!nextId) {
@@ -186,54 +134,28 @@ function SemanticSearchInput(props: ObjectInputProps) {
186
134
  [onChange, onPathFocus, schemaType.name],
187
135
  )
188
136
 
189
- const openButtonConfig: AutocompleteOpenButtonProps = useMemo(
190
- () => ({onClick: () => runIndexQuery(queryRef.current)}),
191
- [runIndexQuery, queryRef],
137
+ const filterResult = useCallback(
138
+ (r: QueryResult) => r.value.documentId !== publicId(docRef.current._id),
139
+ [docRef],
192
140
  )
193
141
 
194
- const handleQueryChange = useCallback(
195
- (newValue: string | null) => {
196
- const newQuery = newValue ?? ''
197
- queryRef.current = newQuery
198
- setQuery(newQuery)
199
- },
200
- [setQuery],
142
+ const getEmptySearchValue = useCallback(() => JSON.stringify(docRef.current), [docRef])
143
+ const typeFilter = useMemo(
144
+ () => (schemaType as ReferenceSchemaType).to.map((refType) => refType.name),
145
+ [schemaType],
201
146
  )
202
147
 
203
148
  return (
204
- <Autocomplete
205
- id={id}
149
+ <SemanticSearchAutocomplete
206
150
  ref={autocompleteRef}
207
- data-testid="semantic-autocomplete"
208
- placeholder="Type to search..."
209
- openButton={openButtonConfig}
210
- onFocus={handleFocus}
151
+ typeFilter={typeFilter}
152
+ indexConfig={indexConfig}
211
153
  onChange={handleChange}
212
- loading={searching}
154
+ onFocus={handleFocus}
213
155
  onBlur={handleBlur}
156
+ getEmptySearchValue={getEmptySearchValue}
157
+ filterResult={filterResult}
214
158
  readOnly={readOnly}
215
- filterOption={NO_FILTER}
216
- onQueryChange={handleQueryChange}
217
- options={options}
218
- renderOption={AutocompleteOption}
219
159
  />
220
160
  )
221
161
  }
222
-
223
- function AutocompleteOption(props: Option) {
224
- const value = props.result.value
225
- return (
226
- <Button mode="bleed" padding={1} style={{width: '100%'}}>
227
- <Flex gap={2} align="center">
228
- <Box flex={1}>
229
- <DocumentPreview documentId={value.documentId} schemaTypeName={value.type} />
230
- </Box>
231
- <Box padding={2}>
232
- <Text size={1} muted title={'Relevance'}>
233
- {Math.floor(props.result.score * 100)}%
234
- </Text>
235
- </Box>
236
- </Flex>
237
- </Button>
238
- )
239
- }
@@ -2,28 +2,42 @@ import {definePlugin, isObjectInputProps, ObjectInputProps, ReferenceSchemaType}
2
2
  import {SemanticSearchReferenceInput} from './SemanticSearchReferenceInput'
3
3
  import {isType} from '../utils/types'
4
4
  import {FeatureEnabledProvider} from '../api/isEnabled'
5
+ import {EmbeddingsIndexConfig} from '../schemas/typeDefExtensions'
5
6
 
6
- export const embeddingsIndexReferenceInput = definePlugin({
7
- name: '@sanity/embeddings-index-reference-input',
8
- studio: {
9
- components: {
10
- layout: (props) => {
11
- return <FeatureEnabledProvider>{props.renderDefault(props)}</FeatureEnabledProvider>
7
+ export const embeddingsIndexReferenceInput = definePlugin<EmbeddingsIndexConfig | void>(
8
+ (defaultConfig) => {
9
+ const config = typeof defaultConfig === 'object' ? defaultConfig : undefined
10
+
11
+ return {
12
+ name: '@sanity/embeddings-index-reference-input',
13
+ studio: {
14
+ components: {
15
+ layout: (props) => {
16
+ return <FeatureEnabledProvider>{props.renderDefault(props)}</FeatureEnabledProvider>
17
+ },
18
+ },
12
19
  },
13
- },
14
- },
15
- form: {
16
- components: {
17
- input: (props) => {
18
- if (
19
- isObjectInputProps(props) &&
20
- isType(props.schemaType, 'reference') &&
21
- (props.schemaType as ReferenceSchemaType).options?.embeddingsIndex?.indexName
22
- ) {
23
- return <SemanticSearchReferenceInput {...(props as ObjectInputProps)} />
24
- }
25
- return props.renderDefault(props)
20
+ form: {
21
+ components: {
22
+ input: (props) => {
23
+ const embeddingsIndexConfig = (props.schemaType as ReferenceSchemaType)?.options
24
+ ?.embeddingsIndex
25
+ if (
26
+ isObjectInputProps(props) &&
27
+ isType(props.schemaType, 'reference') &&
28
+ (embeddingsIndexConfig === true || embeddingsIndexConfig?.indexName)
29
+ ) {
30
+ return (
31
+ <SemanticSearchReferenceInput
32
+ {...(props as ObjectInputProps)}
33
+ defaultConfig={config}
34
+ />
35
+ )
36
+ }
37
+ return props.renderDefault(props)
38
+ },
39
+ },
26
40
  },
27
- },
41
+ }
28
42
  },
29
- })
43
+ )
@@ -1,19 +1,27 @@
1
1
  import 'sanity'
2
+
3
+ export interface EmbeddingsIndexConfig {
4
+ /**
5
+ * Name of the index
6
+ */
7
+ indexName: string
8
+ maxResults?: number
9
+ /**
10
+ * Determines if which search mode is enabled by default for the reference field.
11
+ * Default is the studio default search, while 'embeddings' enables
12
+ * Defaults to 'default' behaviour
13
+ */
14
+ searchMode?: 'embeddings' | 'default'
15
+ }
16
+
2
17
  /* eslint-disable no-unused-vars */
3
18
  declare module 'sanity' {
4
19
  interface ReferenceBaseOptions {
5
- embeddingsIndex?: {
6
- /**
7
- * Name of the index
8
- */
9
- indexName: string
10
- maxResults?: number
11
- /**
12
- * Determines if which search mode is enabled by default for the reference field.
13
- * Default is the studio default search, while 'embeddings' enables
14
- * Defaults to 'default' behaviour
15
- */
16
- searchMode?: 'embeddings' | 'default'
17
- }
20
+ /**
21
+ * Enables toggleable semantic search for a reference field.
22
+ *
23
+ * When `true`: will use default plugin configuration (if no config has been for the plugin provided ,this will throw an error)
24
+ */
25
+ embeddingsIndex?: true | EmbeddingsIndexConfig
18
26
  }
19
27
  }