@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.
- package/README.md +33 -1
- package/dist/index.d.ts +21 -14
- package/dist/index.esm.js +289 -276
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +285 -272
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/embeddingsIndexDashboard/IndexEditor.tsx +8 -6
- package/src/embeddingsIndexDashboard/IndexFormInput.tsx +13 -3
- package/src/embeddingsIndexDashboard/IndexList.tsx +6 -3
- package/src/embeddingsIndexDashboard/QueryIndex.tsx +8 -103
- package/src/referenceInput/SemanticSearchAutocomplete.tsx +205 -0
- package/src/referenceInput/SemanticSearchReferenceInput.tsx +62 -140
- package/src/referenceInput/referencePlugin.tsx +35 -21
- package/src/schemas/typeDefExtensions.ts +21 -13
|
@@ -1,33 +1,58 @@
|
|
|
1
|
-
import {ObjectInputProps, ReferenceSchemaType, set, setIfMissing, typed, unset} from 'sanity'
|
|
2
1
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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,
|
|
13
|
-
import {DocumentPreview} from '../preview/DocumentPreview'
|
|
11
|
+
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
|
14
12
|
import {useDocumentPane} from 'sanity/desk'
|
|
15
|
-
import {
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
|
|
48
|
+
export function SemanticSearchReferenceInput(
|
|
49
|
+
props: ObjectInputProps & {defaultConfig?: EmbeddingsIndexConfig},
|
|
50
|
+
) {
|
|
51
|
+
const embeddingsIndexConfig = (props.schemaType as ReferenceSchemaType)?.options?.embeddingsIndex
|
|
27
52
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
67
|
-
const
|
|
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
|
|
190
|
-
() =>
|
|
191
|
-
[
|
|
137
|
+
const filterResult = useCallback(
|
|
138
|
+
(r: QueryResult) => r.value.documentId !== publicId(docRef.current._id),
|
|
139
|
+
[docRef],
|
|
192
140
|
)
|
|
193
141
|
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
<
|
|
205
|
-
id={id}
|
|
149
|
+
<SemanticSearchAutocomplete
|
|
206
150
|
ref={autocompleteRef}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
openButton={openButtonConfig}
|
|
210
|
-
onFocus={handleFocus}
|
|
151
|
+
typeFilter={typeFilter}
|
|
152
|
+
indexConfig={indexConfig}
|
|
211
153
|
onChange={handleChange}
|
|
212
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
}
|