@sanity/embeddings-index-ui 1.0.2 → 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 +295 -221
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +291 -217
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/api/isEnabled.tsx +53 -0
- package/src/embeddingsIndexDashboard/EmbeddingsIndexTool.tsx +11 -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 +78 -133
- package/src/referenceInput/referencePlugin.tsx +37 -15
- package/src/schemas/typeDefExtensions.ts +21 -13
|
@@ -1,32 +1,80 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
ObjectInputProps,
|
|
3
|
+
ReferenceBaseOptions,
|
|
4
|
+
ReferenceSchemaType,
|
|
5
|
+
set,
|
|
6
|
+
setIfMissing,
|
|
7
|
+
unset,
|
|
8
|
+
} from 'sanity'
|
|
9
|
+
import {Box, Button, Flex, Spinner} from '@sanity/ui'
|
|
3
10
|
import {EarthGlobeIcon, LinkIcon} from '@sanity/icons'
|
|
4
|
-
import {useCallback, useEffect,
|
|
5
|
-
import {DocumentPreview} from '../preview/DocumentPreview'
|
|
11
|
+
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
|
6
12
|
import {useDocumentPane} from 'sanity/desk'
|
|
7
|
-
import {
|
|
13
|
+
import {QueryResult} from '../api/embeddingsApi'
|
|
8
14
|
import {publicId} from '../utils/id'
|
|
9
|
-
import {
|
|
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
|
+
}
|
|
10
37
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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])
|
|
14
46
|
}
|
|
15
47
|
|
|
16
|
-
|
|
17
|
-
|
|
48
|
+
export function SemanticSearchReferenceInput(
|
|
49
|
+
props: ObjectInputProps & {defaultConfig?: EmbeddingsIndexConfig},
|
|
50
|
+
) {
|
|
51
|
+
const embeddingsIndexConfig = (props.schemaType as ReferenceSchemaType)?.options?.embeddingsIndex
|
|
18
52
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
53
|
+
const config = useEmeddingsConfig(embeddingsIndexConfig, props.defaultConfig)
|
|
54
|
+
|
|
55
|
+
const defaultEnabled = config.searchMode === 'embeddings'
|
|
56
|
+
|
|
57
|
+
const featureState = useIsFeatureEnabledContext()
|
|
22
58
|
|
|
23
59
|
const [semantic, setSemantic] = useState<boolean>(defaultEnabled)
|
|
24
60
|
const toggleSemantic = useCallback(() => setSemantic((current) => !current), [])
|
|
25
61
|
|
|
26
62
|
return (
|
|
27
63
|
<Flex gap={2} flex={1} style={{width: '100%'}}>
|
|
64
|
+
{semantic && featureState == 'loading' ? (
|
|
65
|
+
<Box padding={2}>
|
|
66
|
+
<Spinner />
|
|
67
|
+
</Box>
|
|
68
|
+
) : null}
|
|
69
|
+
|
|
70
|
+
{semantic && featureState == 'disabled' ? <FeatureDisabledNotice /> : null}
|
|
71
|
+
|
|
28
72
|
<Box flex={1} style={{maxHeight: 36, overflow: 'hidden'}}>
|
|
29
|
-
{semantic
|
|
73
|
+
{semantic && featureState == 'enabled' ? (
|
|
74
|
+
<SemanticSearchInput {...props} indexConfig={config} />
|
|
75
|
+
) : (
|
|
76
|
+
props.renderDefault(props)
|
|
77
|
+
)}
|
|
30
78
|
</Box>
|
|
31
79
|
<Button
|
|
32
80
|
icon={semantic ? EarthGlobeIcon : LinkIcon}
|
|
@@ -40,38 +88,13 @@ export function SemanticSearchReferenceInput(props: ObjectInputProps) {
|
|
|
40
88
|
)
|
|
41
89
|
}
|
|
42
90
|
|
|
43
|
-
function
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
const timeoutId = setTimeout(() => {
|
|
48
|
-
setDebouncedValue(value)
|
|
49
|
-
}, ms)
|
|
50
|
-
return () => clearTimeout(timeoutId)
|
|
51
|
-
}, [value, ms])
|
|
52
|
-
|
|
53
|
-
return debouncedValue
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function SemanticSearchInput(props: ObjectInputProps) {
|
|
57
|
-
const {onPathFocus, onChange, readOnly, schemaType, value} = props
|
|
91
|
+
function SemanticSearchInput(props: ObjectInputProps & {indexConfig: EmbeddingsIndexConfig}) {
|
|
92
|
+
const {indexConfig, onPathFocus, onChange, readOnly, schemaType, value} = props
|
|
58
93
|
|
|
59
94
|
const {value: currentDocument} = useDocumentPane()
|
|
60
95
|
const docRef = useRef(currentDocument)
|
|
61
96
|
const autocompleteRef = useRef<HTMLInputElement>(null)
|
|
62
97
|
|
|
63
|
-
const id = useId()
|
|
64
|
-
|
|
65
|
-
const [query, setQuery] = useState('')
|
|
66
|
-
const queryRef = useRef(query)
|
|
67
|
-
const debouncedQuery = useDebouncedValue(query, 300)
|
|
68
|
-
const prevDebouncedQuery = useRef(debouncedQuery)
|
|
69
|
-
|
|
70
|
-
const [searching, setSearching] = useState(false)
|
|
71
|
-
const [options, setOptions] = useState(NO_OPTIONS)
|
|
72
|
-
|
|
73
|
-
const client = useApiClient()
|
|
74
|
-
|
|
75
98
|
useEffect(() => {
|
|
76
99
|
docRef.current = currentDocument
|
|
77
100
|
}, [currentDocument])
|
|
@@ -88,58 +111,6 @@ function SemanticSearchInput(props: ObjectInputProps) {
|
|
|
88
111
|
const handleFocus = useCallback(() => onPathFocus(['_ref']), [onPathFocus])
|
|
89
112
|
const handleBlur = useCallback(() => onPathFocus([]), [onPathFocus])
|
|
90
113
|
|
|
91
|
-
const runIndexQuery = useCallback(
|
|
92
|
-
(queryString: string) => {
|
|
93
|
-
setSearching(true)
|
|
94
|
-
const refSchema = schemaType as ReferenceSchemaType
|
|
95
|
-
const indexName = refSchema.options?.embeddingsIndex?.indexName
|
|
96
|
-
const maxResults = refSchema.options?.embeddingsIndex?.maxResults
|
|
97
|
-
const typeFilter = refSchema.to.map((ref) => ref.name)
|
|
98
|
-
|
|
99
|
-
if (!indexName) {
|
|
100
|
-
throw new Error(
|
|
101
|
-
`Reference option embeddingsIndex.indexName is required, but was missing in type ${refSchema.name}`,
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
queryIndex(
|
|
106
|
-
{
|
|
107
|
-
query: queryString.trim().length ? queryString : JSON.stringify(docRef.current) ?? '',
|
|
108
|
-
indexName,
|
|
109
|
-
maxResults,
|
|
110
|
-
filter: {
|
|
111
|
-
type: typeFilter,
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
client,
|
|
115
|
-
)
|
|
116
|
-
.then((result: QueryResult[]) => {
|
|
117
|
-
if (queryRef.current === queryString) {
|
|
118
|
-
setSearching(false)
|
|
119
|
-
setOptions(
|
|
120
|
-
result
|
|
121
|
-
.filter((r) => r.value.documentId !== publicId(docRef.current._id))
|
|
122
|
-
.map((r) => typed<Option>({result: r, value: r.value.documentId})),
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
})
|
|
126
|
-
.catch((e) => {
|
|
127
|
-
if (queryRef.current === queryString) {
|
|
128
|
-
setSearching(false)
|
|
129
|
-
}
|
|
130
|
-
throw e
|
|
131
|
-
})
|
|
132
|
-
},
|
|
133
|
-
[client, schemaType],
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
useEffect(() => {
|
|
137
|
-
if (prevDebouncedQuery.current !== debouncedQuery) {
|
|
138
|
-
runIndexQuery(debouncedQuery)
|
|
139
|
-
}
|
|
140
|
-
prevDebouncedQuery.current = debouncedQuery
|
|
141
|
-
}, [debouncedQuery, runIndexQuery])
|
|
142
|
-
|
|
143
114
|
const handleChange = useCallback(
|
|
144
115
|
(nextId: string) => {
|
|
145
116
|
if (!nextId) {
|
|
@@ -163,54 +134,28 @@ function SemanticSearchInput(props: ObjectInputProps) {
|
|
|
163
134
|
[onChange, onPathFocus, schemaType.name],
|
|
164
135
|
)
|
|
165
136
|
|
|
166
|
-
const
|
|
167
|
-
() =>
|
|
168
|
-
[
|
|
137
|
+
const filterResult = useCallback(
|
|
138
|
+
(r: QueryResult) => r.value.documentId !== publicId(docRef.current._id),
|
|
139
|
+
[docRef],
|
|
169
140
|
)
|
|
170
141
|
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
setQuery(newQuery)
|
|
176
|
-
},
|
|
177
|
-
[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],
|
|
178
146
|
)
|
|
179
147
|
|
|
180
148
|
return (
|
|
181
|
-
<
|
|
182
|
-
id={id}
|
|
149
|
+
<SemanticSearchAutocomplete
|
|
183
150
|
ref={autocompleteRef}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
openButton={openButtonConfig}
|
|
187
|
-
onFocus={handleFocus}
|
|
151
|
+
typeFilter={typeFilter}
|
|
152
|
+
indexConfig={indexConfig}
|
|
188
153
|
onChange={handleChange}
|
|
189
|
-
|
|
154
|
+
onFocus={handleFocus}
|
|
190
155
|
onBlur={handleBlur}
|
|
156
|
+
getEmptySearchValue={getEmptySearchValue}
|
|
157
|
+
filterResult={filterResult}
|
|
191
158
|
readOnly={readOnly}
|
|
192
|
-
filterOption={NO_FILTER}
|
|
193
|
-
onQueryChange={handleQueryChange}
|
|
194
|
-
options={options}
|
|
195
|
-
renderOption={AutocompleteOption}
|
|
196
159
|
/>
|
|
197
160
|
)
|
|
198
161
|
}
|
|
199
|
-
|
|
200
|
-
function AutocompleteOption(props: Option) {
|
|
201
|
-
const value = props.result.value
|
|
202
|
-
return (
|
|
203
|
-
<Button mode="bleed" padding={1} style={{width: '100%'}}>
|
|
204
|
-
<Flex gap={2} align="center">
|
|
205
|
-
<Box flex={1}>
|
|
206
|
-
<DocumentPreview documentId={value.documentId} schemaTypeName={value.type} />
|
|
207
|
-
</Box>
|
|
208
|
-
<Box padding={2}>
|
|
209
|
-
<Text size={1} muted title={'Relevance'}>
|
|
210
|
-
{Math.floor(props.result.score * 100)}%
|
|
211
|
-
</Text>
|
|
212
|
-
</Box>
|
|
213
|
-
</Flex>
|
|
214
|
-
</Button>
|
|
215
|
-
)
|
|
216
|
-
}
|
|
@@ -1,21 +1,43 @@
|
|
|
1
1
|
import {definePlugin, isObjectInputProps, ObjectInputProps, ReferenceSchemaType} from 'sanity'
|
|
2
2
|
import {SemanticSearchReferenceInput} from './SemanticSearchReferenceInput'
|
|
3
3
|
import {isType} from '../utils/types'
|
|
4
|
+
import {FeatureEnabledProvider} from '../api/isEnabled'
|
|
5
|
+
import {EmbeddingsIndexConfig} from '../schemas/typeDefExtensions'
|
|
4
6
|
|
|
5
|
-
export const embeddingsIndexReferenceInput = definePlugin(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
(props
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
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
|
+
},
|
|
19
|
+
},
|
|
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
|
+
},
|
|
18
40
|
},
|
|
19
|
-
}
|
|
41
|
+
}
|
|
20
42
|
},
|
|
21
|
-
|
|
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
|
}
|