@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.
- package/LICENSE +1 -1
- package/README.md +32 -35
- package/dist/index.d.ts +49 -60
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +765 -2198
- package/dist/index.js.map +1 -1
- package/package.json +36 -69
- package/dist/index.d.mts +0 -74
- package/dist/index.mjs +0 -2353
- package/dist/index.mjs.map +0 -1
- package/sanity.json +0 -8
- package/src/api/embeddingsApi.ts +0 -68
- package/src/api/embeddingsApiHooks.ts +0 -17
- package/src/api/isEnabled.tsx +0 -65
- package/src/embeddingsIndexDashboard/EmbeddingsIndexTool.tsx +0 -168
- package/src/embeddingsIndexDashboard/IndexEditor.tsx +0 -185
- package/src/embeddingsIndexDashboard/IndexFormInput.tsx +0 -86
- package/src/embeddingsIndexDashboard/IndexInfo.tsx +0 -117
- package/src/embeddingsIndexDashboard/IndexList.tsx +0 -87
- package/src/embeddingsIndexDashboard/QueryIndex.tsx +0 -32
- package/src/embeddingsIndexDashboard/dashboardPlugin.ts +0 -16
- package/src/embeddingsIndexDashboard/hooks.ts +0 -37
- package/src/index.ts +0 -10
- package/src/preview/DocumentPreview.tsx +0 -139
- package/src/referenceInput/SemanticSearchAutocomplete.tsx +0 -213
- package/src/referenceInput/SemanticSearchReferenceInput.tsx +0 -169
- package/src/referenceInput/referencePlugin.tsx +0 -44
- package/src/referenceInput/types.ts +0 -0
- package/src/schemas/typeDefExtensions.ts +0 -27
- package/src/utils/id.ts +0 -3
- package/src/utils/types.ts +0 -11
- package/v2-incompatible.js +0 -11
|
@@ -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
|
-
}
|