@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.
@@ -1,17 +0,0 @@
1
- import {useMemo} from 'react'
2
- import {SanityClient, useClient} from 'sanity'
3
-
4
- export function useApiClient(): SanityClient {
5
- const client = useClient({apiVersion: 'vX'})
6
- return useMemo(() => {
7
- const customHost = localStorage.getItem('embeddings-index-host')
8
- if (customHost) {
9
- return client.withConfig({
10
- apiHost: customHost,
11
- useProjectHostname: false,
12
- withCredentials: false,
13
- })
14
- }
15
- return client
16
- }, [client])
17
- }
@@ -1,65 +0,0 @@
1
- import {Card, Text} from '@sanity/ui'
2
- import {createContext, PropsWithChildren, useContext, useEffect, useState} from 'react'
3
- import {useProjectId} from 'sanity'
4
-
5
- import {useApiClient} from './embeddingsApiHooks'
6
-
7
- export type FeatureStatus = 'enabled' | 'disabled' | 'loading' | 'error'
8
- export const FeatureEnabledContext = createContext<FeatureStatus>('loading')
9
-
10
- export function useIsFeatureEnabled() {
11
- const client = useApiClient()
12
- const [status, setStatus] = useState<FeatureStatus>('loading')
13
-
14
- useEffect(() => {
15
- client
16
- .request<{enabled: boolean}>({
17
- method: 'GET',
18
- url: `/embeddings-index/status`,
19
- })
20
- .then((response) => {
21
- setStatus(response.enabled ? 'enabled' : 'disabled')
22
- })
23
- .catch((err) => {
24
- console.error(err)
25
- setStatus('error')
26
- })
27
- }, [client])
28
-
29
- return status
30
- }
31
-
32
- export function FeatureEnabledProvider(props: PropsWithChildren) {
33
- const status = useIsFeatureEnabled()
34
- return (
35
- <FeatureEnabledContext.Provider value={status}>{props.children}</FeatureEnabledContext.Provider>
36
- )
37
- }
38
-
39
- export function useIsFeatureEnabledContext(): FeatureStatus {
40
- return useContext(FeatureEnabledContext)
41
- }
42
-
43
- export function FeatureDisabledNotice(props: {urlSuffix?: string}) {
44
- const projectId = useProjectId()
45
-
46
- return (
47
- <Card tone="primary" border padding={4}>
48
- <Text size={1}>
49
- 💎 Unlock semantic search with the Embeddings Index API — available on Team, Business, and
50
- Enterprise plans.{' '}
51
- <a href={`https://www.sanity.io/manage/project/${projectId}/plan${props.urlSuffix ?? ''}`}>
52
- Upgrade now →
53
- </a>
54
- </Text>
55
- </Card>
56
- )
57
- }
58
-
59
- export function FeatureError() {
60
- return (
61
- <Card padding={4} border tone="critical">
62
- An error occurred. See console for details.
63
- </Card>
64
- )
65
- }
@@ -1,168 +0,0 @@
1
- import {AddIcon, UndoIcon} from '@sanity/icons'
2
- import {Box, Button, Card, Flex, Heading, Spinner, Stack} from '@sanity/ui'
3
- import {useCallback, useEffect, useState} from 'react'
4
-
5
- import {deleteIndex, getIndexes, IndexState, NamedIndex} from '../api/embeddingsApi'
6
- import {useApiClient} from '../api/embeddingsApiHooks'
7
- import {FeatureDisabledNotice, FeatureError, useIsFeatureEnabled} from '../api/isEnabled'
8
- import {EditIndexDialog} from './IndexEditor'
9
- import {IndexInfo} from './IndexInfo'
10
- import {IndexList} from './IndexList'
11
-
12
- export function EmbeddingsIndexTool() {
13
- const featureState = useIsFeatureEnabled()
14
- return (
15
- <Card flex={1}>
16
- <Flex justify="center" flex={1}>
17
- {featureState === 'error' ? (
18
- <Box padding={4}>
19
- <FeatureError />
20
- </Box>
21
- ) : null}
22
-
23
- {featureState === 'disabled' ? (
24
- <Box padding={4}>
25
- <FeatureDisabledNotice urlSuffix="?ref=embeddings-tab" />
26
- </Box>
27
- ) : null}
28
-
29
- {featureState === 'loading' ? (
30
- <Box padding={6}>
31
- <Spinner size={4} />
32
- </Box>
33
- ) : null}
34
-
35
- {featureState === 'enabled' ? (
36
- <Card flex={1} style={{maxWidth: 1200}} padding={5}>
37
- <Indexes />
38
- </Card>
39
- ) : null}
40
- </Flex>
41
- </Card>
42
- )
43
- }
44
-
45
- const NO_INDEXES: IndexState[] = []
46
-
47
- function Indexes() {
48
- const client = useApiClient()
49
- const [indexes, setIndexes] = useState<IndexState[]>(NO_INDEXES)
50
- const [loading, setLoading] = useState(false)
51
- const [error, setError] = useState(false)
52
- const [createIndexOpen, setCreateIndexOpen] = useState(false)
53
- const [selectedIndex, setSelectedIndex] = useState<IndexState | undefined>(undefined)
54
-
55
- const onCreateIndexClose = useCallback(() => setCreateIndexOpen(false), [])
56
-
57
- useEffect(() => {
58
- setSelectedIndex(indexes.find((i) => i.indexName === selectedIndex?.indexName))
59
- }, [indexes, selectedIndex])
60
-
61
- const updateIndexes = useCallback(() => {
62
- setLoading(true)
63
- setError(false)
64
- getIndexes(client)
65
- .then((response: IndexState[]) => {
66
- setLoading(false)
67
- setIndexes(response)
68
- })
69
- .catch((e) => {
70
- // eslint-disable-next-line no-unused-expressions
71
- console.error(e)
72
- setError(true)
73
- })
74
- .finally(() => {
75
- setLoading(false)
76
- })
77
- }, [client])
78
-
79
- const deleteNamedIndex = useCallback(
80
- (index: NamedIndex) => {
81
- if (
82
- // eslint-disable-next-line no-alert
83
- !confirm(`Are you sure you want to delete ${index.indexName} for dataset ${index.dataset}?`)
84
- ) {
85
- return
86
- }
87
- setLoading(true)
88
- setError(false)
89
- deleteIndex(index.indexName, client)
90
- .then(() => {
91
- setTimeout(() => updateIndexes())
92
- })
93
- .catch((e) => {
94
- // eslint-disable-next-line no-unused-expressions
95
- console.error(e)
96
- setError(true)
97
- })
98
- .finally(() => {
99
- setLoading(false)
100
- })
101
- },
102
- [client, updateIndexes],
103
- )
104
-
105
- const onSelectIndex = useCallback(
106
- (index: IndexState) => {
107
- setSelectedIndex(index)
108
- updateIndexes()
109
- },
110
- [setSelectedIndex, updateIndexes],
111
- )
112
-
113
- useEffect(() => {
114
- updateIndexes()
115
- }, [updateIndexes])
116
-
117
- const openCreate = useCallback(() => setCreateIndexOpen(true), [])
118
- const onSubmit = useCallback(
119
- (index: IndexState) => {
120
- setIndexes((current) => [...current, index])
121
- setSelectedIndex(index)
122
- updateIndexes()
123
- },
124
- [updateIndexes],
125
- )
126
- return (
127
- <Stack space={4}>
128
- <Flex gap={2} align="center" style={{height: 30}}>
129
- <Box flex={1}>
130
- <Heading size={1}>Embeddings indexes</Heading>
131
- </Box>
132
- <Box style={{justifySelf: 'flex-end'}}>
133
- <Button
134
- icon={AddIcon}
135
- text={'New index'}
136
- tone="default"
137
- mode="ghost"
138
- onClick={openCreate}
139
- />
140
- </Box>
141
- <Button
142
- size={1}
143
- icon={loading ? <Spinner /> : UndoIcon}
144
- title={'Refresh index list'}
145
- tone="default"
146
- mode="bleed"
147
- onClick={updateIndexes}
148
- disabled={loading}
149
- />
150
- </Flex>
151
- {error ? (
152
- <Card tone="critical" padding={2} border>
153
- An error occurred. See console for details.
154
- </Card>
155
- ) : null}
156
- <IndexList
157
- loading={loading}
158
- indexes={indexes}
159
- selectedIndex={selectedIndex}
160
- onIndexSelected={onSelectIndex}
161
- />
162
- {selectedIndex && (
163
- <IndexInfo selectedIndex={selectedIndex} onDeleteIndex={deleteNamedIndex} />
164
- )}
165
- <EditIndexDialog open={createIndexOpen} onClose={onCreateIndexClose} onSubmit={onSubmit} />
166
- </Stack>
167
- )
168
- }
@@ -1,185 +0,0 @@
1
- import {AddIcon} from '@sanity/icons'
2
- import {Box, Button, Card, Dialog, Spinner, Stack, Text} from '@sanity/ui'
3
- import {FormEvent, useCallback, useEffect, useId, useRef, useState} from 'react'
4
- import {useSchema} from 'sanity'
5
-
6
- import {IndexState, NamedIndex} from '../api/embeddingsApi'
7
- import {useApiClient} from '../api/embeddingsApiHooks'
8
- import {useDefaultIndex} from './hooks'
9
- import {IndexFormInput} from './IndexFormInput'
10
-
11
- export function EditIndexDialog(props: {
12
- open: boolean
13
- onClose: () => void
14
- onSubmit: (index: IndexState) => void
15
- }) {
16
- const {open, onClose, onSubmit} = props
17
- const id = useId()
18
- const ref = useRef<HTMLDivElement>(null)
19
-
20
- useEffect(() => {
21
- if (!open) {
22
- return
23
- }
24
- setTimeout(() => ref.current?.querySelector('input')?.focus())
25
- }, [ref, open])
26
-
27
- const handleSubmit = useCallback(
28
- (index: IndexState) => {
29
- onSubmit(index)
30
- onClose()
31
- },
32
- [onSubmit, onClose],
33
- )
34
-
35
- return open ? (
36
- <Dialog id={id} width={1} ref={ref} onClose={onClose} header="Create embeddings index">
37
- <Stack padding={4} space={5}>
38
- <IndexEditor readOnly={false} onSubmit={handleSubmit} />
39
- </Stack>
40
- </Dialog>
41
- ) : null
42
- }
43
-
44
- export function IndexEditor(props: {
45
- index?: Partial<NamedIndex>
46
- readOnly: boolean
47
- onSubmit?: (index: IndexState) => void
48
- }) {
49
- const {readOnly, index: selectedIndex, onSubmit} = props
50
- const client = useApiClient()
51
- const schema = useSchema()
52
- const defaultIndex = useDefaultIndex(schema, client.config().dataset ?? '')
53
- const [errors, setErrors] = useState<string[] | undefined>()
54
- const [loading, setLoading] = useState<boolean>()
55
- const [index, setIndex] = useState<Partial<NamedIndex>>(() => ({
56
- ...defaultIndex,
57
- ...selectedIndex,
58
- }))
59
-
60
- useEffect(() => setIndex(selectedIndex ?? {...defaultIndex}), [selectedIndex, defaultIndex])
61
-
62
- const handleSubmit = useCallback(
63
- (e: FormEvent) => {
64
- e.preventDefault()
65
- if (readOnly) {
66
- return
67
- }
68
-
69
- const validationErrors: string[] = []
70
-
71
- if (!index.indexName) {
72
- validationErrors.push('Index name is required')
73
- } else if (!index.indexName.match(/^[a-zA-Z0-9-_]+$/g)) {
74
- validationErrors.push('Index name can only contain the letters a-z, numbers - and _')
75
- }
76
-
77
- if (!index.dataset) {
78
- validationErrors.push('Dataset is required')
79
- }
80
-
81
- if (!index.filter) {
82
- validationErrors.push('Filter is required')
83
- }
84
-
85
- if (!index.projection) {
86
- validationErrors.push('Projection is required')
87
- }
88
-
89
- if (validationErrors.length) {
90
- setErrors(validationErrors)
91
- return
92
- }
93
-
94
- const {projectId} = client.config()
95
- setLoading(true)
96
- client
97
- .request({
98
- method: 'POST',
99
- url: `/embeddings-index/${index.dataset}?projectId=${projectId}`,
100
- body: {
101
- indexName: index.indexName,
102
- projection: index.projection,
103
- filter: index.filter,
104
- },
105
- })
106
- .then((response: {index: IndexState}) => {
107
- if (onSubmit) {
108
- onSubmit(response.index)
109
- }
110
- })
111
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
112
- .catch((err: any) => {
113
- console.error(err)
114
- setErrors([err.message])
115
- })
116
- .finally(() => {
117
- setLoading(false)
118
- })
119
- },
120
- [index, readOnly, onSubmit, client],
121
- )
122
-
123
- return (
124
- <form onSubmit={handleSubmit}>
125
- <Stack space={4}>
126
- {errors?.length ? (
127
- <Card tone="critical" border padding={2}>
128
- <Text>
129
- <ul style={{marginLeft: -10}}>
130
- {/* eslint-disable-next-line react/no-array-index-key */}
131
- {errors?.map((error, i) => <li key={`${error}-${i}`}>{error}</li>)}
132
- </ul>
133
- </Text>
134
- </Card>
135
- ) : null}
136
- <IndexFormInput
137
- label="Index name"
138
- placeholder={'Name of index without spaces...'}
139
- index={index}
140
- prop="indexName"
141
- onChange={setIndex}
142
- readOnly={readOnly}
143
- />
144
- <IndexFormInput label="Dataset" index={index} prop="dataset" onChange={setIndex} readOnly />
145
- <IndexFormInput
146
- label="Filter"
147
- description="Must be a valid GROQ filter"
148
- placeholder={defaultIndex.filter}
149
- index={index}
150
- prop="filter"
151
- onChange={setIndex}
152
- readOnly={readOnly}
153
- type="textarea"
154
- />
155
- <IndexFormInput
156
- label="Projection"
157
- description="Must be a valid GROQ projection, starting { and ending with }"
158
- placeholder={defaultIndex.projection}
159
- index={index}
160
- prop="projection"
161
- onChange={setIndex}
162
- readOnly={readOnly}
163
- type="textarea"
164
- />
165
- {onSubmit && (
166
- <Button
167
- type="submit"
168
- text="Create index"
169
- icon={
170
- loading ? (
171
- <Box style={{marginTop: 5}}>
172
- <Spinner />
173
- </Box>
174
- ) : (
175
- AddIcon
176
- )
177
- }
178
- disabled={readOnly || loading}
179
- tone="primary"
180
- />
181
- )}
182
- </Stack>
183
- </form>
184
- )
185
- }
@@ -1,86 +0,0 @@
1
- import {Box, Label, Stack, Text, TextArea, TextInput} from '@sanity/ui'
2
- import {Dispatch, FormEvent, SetStateAction, useCallback, useId} from 'react'
3
-
4
- import {NamedIndex} from '../api/embeddingsApi'
5
-
6
- export interface IndexFormInputProps {
7
- index: Partial<NamedIndex>
8
- prop: keyof NamedIndex
9
- label: string
10
- description?: string
11
- onChange: Dispatch<SetStateAction<Partial<NamedIndex>>>
12
- readOnly: boolean
13
- placeholder?: string
14
- type?: 'text' | 'textarea'
15
- }
16
-
17
- export function IndexFormInput(props: IndexFormInputProps) {
18
- const {label, description, index, prop, onChange, readOnly, placeholder, type} = props
19
- const handleChange = useCallback(
20
- (propValue: string) => onChange((current) => ({...current, [prop]: propValue})),
21
- [onChange, prop],
22
- )
23
- return (
24
- <FormInput
25
- label={label}
26
- description={description}
27
- onChange={handleChange}
28
- value={index[prop] ?? ''}
29
- readOnly={readOnly}
30
- placeholder={placeholder}
31
- type={type}
32
- />
33
- )
34
- }
35
-
36
- interface FormInputProps {
37
- label: string
38
- description?: string
39
- onChange: (value: string) => void
40
- value: string
41
- readOnly: boolean
42
- placeholder?: string
43
- type?: 'text' | 'textarea'
44
- }
45
-
46
- function FormInput(props: FormInputProps) {
47
- const {label, description, onChange, value, readOnly, placeholder, type = 'text'} = props
48
- const id = useId()
49
- const handleChange = useCallback(
50
- (e: FormEvent<HTMLInputElement | HTMLTextAreaElement>) => onChange(e.currentTarget.value),
51
- [onChange],
52
- )
53
- return (
54
- <Stack space={3}>
55
- <Label muted htmlFor={id}>
56
- <label htmlFor={id}>{label}</label>
57
- </Label>
58
- {description && (
59
- <Box>
60
- <Text size={1} muted>
61
- {description}
62
- </Text>
63
- </Box>
64
- )}
65
- {type === 'text' ? (
66
- <TextInput
67
- id={id}
68
- value={value}
69
- onChange={handleChange}
70
- readOnly={readOnly}
71
- placeholder={placeholder}
72
- />
73
- ) : (
74
- <TextArea
75
- id={id}
76
- value={value}
77
- rows={3}
78
- onChange={handleChange}
79
- readOnly={readOnly}
80
- placeholder={placeholder}
81
- style={{resize: 'vertical'}}
82
- />
83
- )}
84
- </Stack>
85
- )
86
- }
@@ -1,117 +0,0 @@
1
- import {EllipsisVerticalIcon, TrashIcon} from '@sanity/icons'
2
- import {
3
- Box,
4
- Button,
5
- Flex,
6
- Heading,
7
- Label,
8
- Menu,
9
- MenuButton,
10
- MenuItem,
11
- Stack,
12
- Text,
13
- } from '@sanity/ui'
14
- import {useCallback} from 'react'
15
-
16
- import {IndexState} from '../api/embeddingsApi'
17
- import {IndexEditor} from './IndexEditor'
18
- import {QueryIndex} from './QueryIndex'
19
-
20
- export interface IndexInfoProps {
21
- selectedIndex: IndexState
22
- onDeleteIndex: (index: IndexState) => void
23
- }
24
-
25
- export function IndexInfo({selectedIndex, onDeleteIndex}: IndexInfoProps) {
26
- const handleDelete = useCallback(
27
- () => onDeleteIndex(selectedIndex),
28
- [selectedIndex, onDeleteIndex],
29
- )
30
- return (
31
- <Stack space={4} flex={1}>
32
- <Flex align="center" flex={1} gap={2}>
33
- <Box flex={1}>
34
- <Heading>Index: {selectedIndex?.indexName ?? 'Untitled'}</Heading>
35
- </Box>
36
- <Box>
37
- <MenuButton
38
- button={
39
- <Button
40
- title="Open index actions"
41
- icon={EllipsisVerticalIcon}
42
- padding={2}
43
- mode="ghost"
44
- />
45
- }
46
- id={`button-${selectedIndex.indexName}`}
47
- menu={
48
- <Menu>
49
- <MenuItem
50
- text="Delete index"
51
- icon={TrashIcon}
52
- tone="critical"
53
- onClick={handleDelete}
54
- />
55
- </Menu>
56
- }
57
- popover={{placement: 'right'}}
58
- />
59
- </Box>
60
- </Flex>
61
-
62
- <Flex gap={6}>
63
- <Stack space={4} flex={1} style={{maxWidth: 600}}>
64
- <Box>
65
- <IndexEditor index={selectedIndex} readOnly />
66
- </Box>
67
- <IndexStatus selectedIndex={selectedIndex} />
68
- </Stack>
69
-
70
- <Stack space={3} flex={1}>
71
- <Label muted>Query index</Label>
72
- <QueryIndex indexName={selectedIndex.indexName} key={selectedIndex.indexName} />
73
- </Stack>
74
- </Flex>
75
- </Stack>
76
- )
77
- }
78
-
79
- function IndexStatus({selectedIndex}: {selectedIndex: IndexState}) {
80
- return (
81
- <Stack space={4} flex={1}>
82
- <Flex gap={2} align="center">
83
- <Box flex={1}>
84
- <Label size={1} muted>
85
- Status
86
- </Label>
87
- </Box>
88
- <Stack space={2}>
89
- <Text>{selectedIndex.status}</Text>
90
- </Stack>
91
- </Flex>
92
- <Flex gap={5} align="center">
93
- <Box flex={1}>
94
- <Label size={1} muted>
95
- Indexing progress
96
- </Label>
97
- </Box>
98
- <Stack space={2}>
99
- <Text>
100
- {selectedIndex.startDocumentCount - selectedIndex.remainingDocumentCount} /{' '}
101
- {selectedIndex.startDocumentCount}
102
- </Text>
103
- </Stack>
104
- </Flex>
105
- <Flex gap={5} align="center">
106
- <Box flex={1}>
107
- <Label size={1} muted>
108
- Failed documents
109
- </Label>
110
- </Box>
111
- <Stack space={2}>
112
- <Text>{selectedIndex.failedDocumentCount}</Text>
113
- </Stack>
114
- </Flex>
115
- </Stack>
116
- )
117
- }