@sanity/embeddings-index-ui 1.0.3 → 1.1.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,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
52
+
53
+ const config = useEmeddingsConfig(embeddingsIndexConfig, props.defaultConfig)
27
54
 
28
- export function SemanticSearchReferenceInput(props: ObjectInputProps) {
29
- const defaultEnabled =
30
- (props.schemaType as ReferenceSchemaType)?.options?.embeddingsIndex?.searchMode === 'embeddings'
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,70 +111,17 @@ 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
- (nextId: string) => {
168
- if (!nextId) {
115
+ (result: QueryResult) => {
116
+ if (!result) {
169
117
  onChange(unset())
170
118
  onPathFocus([])
171
119
  return
172
120
  }
173
-
174
121
  const patches = [
175
122
  setIfMissing({}),
176
123
  set(schemaType.name, ['_type']),
177
- set(publicId(nextId), ['_ref']),
124
+ set(publicId(result.value.documentId), ['_ref']),
178
125
  unset(['_weak']),
179
126
  unset(['_strengthenOnPublish']),
180
127
  ]
@@ -186,54 +133,28 @@ function SemanticSearchInput(props: ObjectInputProps) {
186
133
  [onChange, onPathFocus, schemaType.name],
187
134
  )
188
135
 
189
- const openButtonConfig: AutocompleteOpenButtonProps = useMemo(
190
- () => ({onClick: () => runIndexQuery(queryRef.current)}),
191
- [runIndexQuery, queryRef],
136
+ const filterResult = useCallback(
137
+ (r: QueryResult) => r.value.documentId !== publicId(docRef.current._id),
138
+ [docRef],
192
139
  )
193
140
 
194
- const handleQueryChange = useCallback(
195
- (newValue: string | null) => {
196
- const newQuery = newValue ?? ''
197
- queryRef.current = newQuery
198
- setQuery(newQuery)
199
- },
200
- [setQuery],
141
+ const getEmptySearchValue = useCallback(() => JSON.stringify(docRef.current), [docRef])
142
+ const typeFilter = useMemo(
143
+ () => (schemaType as ReferenceSchemaType).to.map((refType) => refType.name),
144
+ [schemaType],
201
145
  )
202
146
 
203
147
  return (
204
- <Autocomplete
205
- id={id}
148
+ <SemanticSearchAutocomplete
206
149
  ref={autocompleteRef}
207
- data-testid="semantic-autocomplete"
208
- placeholder="Type to search..."
209
- openButton={openButtonConfig}
150
+ typeFilter={typeFilter}
151
+ indexConfig={indexConfig}
152
+ onSelect={handleChange}
210
153
  onFocus={handleFocus}
211
- onChange={handleChange}
212
- loading={searching}
213
154
  onBlur={handleBlur}
155
+ getEmptySearchValue={getEmptySearchValue}
156
+ filterResult={filterResult}
214
157
  readOnly={readOnly}
215
- filterOption={NO_FILTER}
216
- onQueryChange={handleQueryChange}
217
- options={options}
218
- renderOption={AutocompleteOption}
219
158
  />
220
159
  )
221
160
  }
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
  }