@sanity/embeddings-index-ui 1.0.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/LICENSE +21 -0
- package/README.md +148 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.esm.js +2792 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +2807 -0
- package/dist/index.js.map +1 -0
- package/package.json +93 -0
- package/sanity.json +8 -0
- package/src/api/embeddingsApi.ts +68 -0
- package/src/api/embeddingsApiHooks.ts +12 -0
- package/src/embeddingsIndexDashboard/EmbeddingsIndexTool.tsx +150 -0
- package/src/embeddingsIndexDashboard/IndexEditor.tsx +181 -0
- package/src/embeddingsIndexDashboard/IndexFormInput.tsx +75 -0
- package/src/embeddingsIndexDashboard/IndexInfo.tsx +116 -0
- package/src/embeddingsIndexDashboard/IndexList.tsx +83 -0
- package/src/embeddingsIndexDashboard/QueryIndex.tsx +107 -0
- package/src/embeddingsIndexDashboard/dashboardPlugin.ts +15 -0
- package/src/embeddingsIndexDashboard/hooks.ts +36 -0
- package/src/index.ts +9 -0
- package/src/preview/DocumentPreview.tsx +135 -0
- package/src/referenceInput/SemanticSearchReferenceInput.tsx +227 -0
- package/src/referenceInput/referencePlugin.tsx +21 -0
- package/src/referenceInput/types.ts +0 -0
- package/src/schemas/typeDefExtensions.ts +19 -0
- package/src/utils/id.ts +3 -0
- package/src/utils/types.ts +11 -0
- package/v2-incompatible.js +11 -0
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sanity/embeddings-index-ui",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Various Sanity Studio plugins for integrating with the embeddings index API",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sanity",
|
|
7
|
+
"sanity-plugin"
|
|
8
|
+
],
|
|
9
|
+
"homepage": "https://github.com/sanity-io/embeddings-index-ui#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/sanity-io/embeddings-index-ui/issues"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+ssh://git@github.com/sanity-io/embeddings-index-ui.git"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Sanity <hello@sanity.io>",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"source": "./src/index.ts",
|
|
23
|
+
"require": "./dist/index.js",
|
|
24
|
+
"import": "./dist/index.esm.js",
|
|
25
|
+
"default": "./dist/index.esm.js"
|
|
26
|
+
},
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"main": "./dist/index.js",
|
|
30
|
+
"module": "./dist/index.esm.js",
|
|
31
|
+
"source": "./src/index.ts",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"sanity.json",
|
|
36
|
+
"src",
|
|
37
|
+
"v2-incompatible.js"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "run-s clean && plugin-kit verify-package --silent && pkg-utils build --strict && pkg-utils --strict",
|
|
41
|
+
"clean": "rimraf dist",
|
|
42
|
+
"format": "prettier --write --cache --ignore-unknown .",
|
|
43
|
+
"link-watch": "plugin-kit link-watch",
|
|
44
|
+
"lint": "eslint .",
|
|
45
|
+
"prepublishOnly": "run-s build",
|
|
46
|
+
"watch": "pkg-utils watch --strict",
|
|
47
|
+
"prepare": "husky install"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@sanity/icons": "^2.4.1",
|
|
51
|
+
"@sanity/incompatible-plugin": "^1.0.4",
|
|
52
|
+
"@sanity/ui": "^1.7.11"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@commitlint/cli": "^17.7.1",
|
|
56
|
+
"@commitlint/config-conventional": "^17.7.0",
|
|
57
|
+
"@sanity/pkg-utils": "^2.4.8",
|
|
58
|
+
"@sanity/plugin-kit": "^3.1.10",
|
|
59
|
+
"@sanity/semantic-release-preset": "^4.1.4",
|
|
60
|
+
"@types/react": "^18.2.21",
|
|
61
|
+
"@typescript-eslint/eslint-plugin": "^6.5.0",
|
|
62
|
+
"@typescript-eslint/parser": "^6.5.0",
|
|
63
|
+
"eslint": "^8.48.0",
|
|
64
|
+
"eslint-config-prettier": "^9.0.0",
|
|
65
|
+
"eslint-config-sanity": "^6.0.0",
|
|
66
|
+
"eslint-plugin-prettier": "^5.0.0",
|
|
67
|
+
"eslint-plugin-react": "^7.33.2",
|
|
68
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
69
|
+
"husky": "^8.0.3",
|
|
70
|
+
"lint-staged": "^14.0.1",
|
|
71
|
+
"npm-run-all": "^4.1.5",
|
|
72
|
+
"prettier": "^3.0.3",
|
|
73
|
+
"prettier-plugin-packagejson": "^2.4.5",
|
|
74
|
+
"react": "^18.2.0",
|
|
75
|
+
"react-dom": "^18.2.0",
|
|
76
|
+
"react-is": "^18.2.0",
|
|
77
|
+
"rimraf": "^5.0.1",
|
|
78
|
+
"sanity": "^3.16.1",
|
|
79
|
+
"semantic-release": "^21.1.1",
|
|
80
|
+
"styled-components": "^5.3.11",
|
|
81
|
+
"typescript": "^5.2.2"
|
|
82
|
+
},
|
|
83
|
+
"peerDependencies": {
|
|
84
|
+
"react": "^18",
|
|
85
|
+
"sanity": "^3.14"
|
|
86
|
+
},
|
|
87
|
+
"engines": {
|
|
88
|
+
"node": ">=14"
|
|
89
|
+
},
|
|
90
|
+
"publishConfig": {
|
|
91
|
+
"access": "public"
|
|
92
|
+
}
|
|
93
|
+
}
|
package/sanity.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {SanityClient} from 'sanity'
|
|
2
|
+
|
|
3
|
+
export interface NamedIndex {
|
|
4
|
+
indexName: string
|
|
5
|
+
dataset: string
|
|
6
|
+
project: string
|
|
7
|
+
projection: string
|
|
8
|
+
filter: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface IndexState extends NamedIndex {
|
|
12
|
+
status: string
|
|
13
|
+
startDocumentCount: number
|
|
14
|
+
remainingDocumentCount: number
|
|
15
|
+
failedDocumentCount: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface QueryResult {
|
|
19
|
+
score: number
|
|
20
|
+
value: {
|
|
21
|
+
documentId: string
|
|
22
|
+
type?: string
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface QueryConfig {
|
|
27
|
+
query: string
|
|
28
|
+
indexName: string
|
|
29
|
+
maxResults?: number
|
|
30
|
+
filter?: {
|
|
31
|
+
type?: string | string[]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function queryIndex(queryConfig: QueryConfig, client: SanityClient): Promise<QueryResult[]> {
|
|
36
|
+
const {query, indexName, maxResults, filter} = queryConfig
|
|
37
|
+
const projectId = client.config().projectId
|
|
38
|
+
const dataset = client.config().dataset
|
|
39
|
+
const queryString = query?.trim()
|
|
40
|
+
|
|
41
|
+
return client.request<QueryResult[]>({
|
|
42
|
+
method: 'POST',
|
|
43
|
+
url: `/embeddings-index/query/${dataset}/${indexName}?projectId=${projectId}`,
|
|
44
|
+
body: {
|
|
45
|
+
query: queryString,
|
|
46
|
+
maxResults,
|
|
47
|
+
filter,
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getIndexes(client: SanityClient): Promise<IndexState[]> {
|
|
53
|
+
const projectId = client.config().projectId
|
|
54
|
+
const dataset = client.config().dataset
|
|
55
|
+
return client.request<IndexState[]>({
|
|
56
|
+
method: 'GET',
|
|
57
|
+
url: `/embeddings-index/${dataset}?projectId=${projectId}`,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function deleteIndex(indexName: string, client: SanityClient): Promise<IndexState> {
|
|
62
|
+
const projectId = client.config().projectId
|
|
63
|
+
const dataset = client.config().dataset
|
|
64
|
+
return client.request<IndexState>({
|
|
65
|
+
method: 'DELETE',
|
|
66
|
+
url: `/embeddings-index/${dataset}/${indexName}?projectId=${projectId}`,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {SanityClient, useClient} from 'sanity'
|
|
2
|
+
import {useMemo} from 'react'
|
|
3
|
+
|
|
4
|
+
export function useApiClient(
|
|
5
|
+
customApiClient?: (defaultClient: SanityClient) => SanityClient,
|
|
6
|
+
): SanityClient {
|
|
7
|
+
const client = useClient({apiVersion: 'vX'})
|
|
8
|
+
return useMemo(
|
|
9
|
+
() => (customApiClient ? customApiClient(client) : client),
|
|
10
|
+
[client, customApiClient],
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {Box, Button, Card, Flex, Heading, Spinner, Stack} from '@sanity/ui'
|
|
2
|
+
import {useClient} from 'sanity'
|
|
3
|
+
import {useCallback, useEffect, useMemo, useState} from 'react'
|
|
4
|
+
import {AddIcon, UndoIcon} from '@sanity/icons'
|
|
5
|
+
import {deleteIndex, getIndexes, IndexState, NamedIndex} from '../api/embeddingsApi'
|
|
6
|
+
import {EditIndexDialog} from './IndexEditor'
|
|
7
|
+
import {IndexList} from './IndexList'
|
|
8
|
+
import {IndexInfo} from './IndexInfo'
|
|
9
|
+
|
|
10
|
+
function useApiClient() {
|
|
11
|
+
const client = useClient({apiVersion: 'vX'})
|
|
12
|
+
return useMemo(() => client, [client])
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function EmbeddingsIndexTool() {
|
|
16
|
+
return (
|
|
17
|
+
<Card>
|
|
18
|
+
<Flex justify="center" flex={1}>
|
|
19
|
+
<Card flex={1} style={{maxWidth: 1200}} padding={5}>
|
|
20
|
+
<Indexes />
|
|
21
|
+
</Card>
|
|
22
|
+
</Flex>
|
|
23
|
+
</Card>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const NO_INDEXES: IndexState[] = []
|
|
28
|
+
|
|
29
|
+
function Indexes() {
|
|
30
|
+
const client = useApiClient()
|
|
31
|
+
const [indexes, setIndexes] = useState<IndexState[]>(NO_INDEXES)
|
|
32
|
+
const [loading, setLoading] = useState(false)
|
|
33
|
+
const [error, setError] = useState(false)
|
|
34
|
+
const [createIndexOpen, setCreateIndexOpen] = useState(false)
|
|
35
|
+
const [selectedIndex, setSelectedIndex] = useState<IndexState | undefined>(undefined)
|
|
36
|
+
|
|
37
|
+
const onCreateIndexClose = useCallback(() => setCreateIndexOpen(false), [])
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
setSelectedIndex(indexes.find((i) => i.indexName === selectedIndex?.indexName))
|
|
41
|
+
}, [indexes, selectedIndex])
|
|
42
|
+
|
|
43
|
+
const updateIndexes = useCallback(() => {
|
|
44
|
+
setLoading(true)
|
|
45
|
+
setError(false)
|
|
46
|
+
getIndexes(client)
|
|
47
|
+
.then((response: IndexState[]) => {
|
|
48
|
+
setLoading(false)
|
|
49
|
+
setIndexes(response)
|
|
50
|
+
})
|
|
51
|
+
.catch((e) => {
|
|
52
|
+
// eslint-disable-next-line no-unused-expressions
|
|
53
|
+
console.error(e)
|
|
54
|
+
setError(true)
|
|
55
|
+
})
|
|
56
|
+
.finally(() => {
|
|
57
|
+
setLoading(false)
|
|
58
|
+
})
|
|
59
|
+
}, [client])
|
|
60
|
+
|
|
61
|
+
const deleteNamedIndex = useCallback(
|
|
62
|
+
(index: NamedIndex) => {
|
|
63
|
+
if (
|
|
64
|
+
// eslint-disable-next-line no-alert
|
|
65
|
+
!confirm(`Are you sure you want to delete ${index.indexName} for dataset ${index.dataset}?`)
|
|
66
|
+
) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
setLoading(true)
|
|
70
|
+
setError(false)
|
|
71
|
+
deleteIndex(index.indexName, client)
|
|
72
|
+
.then(() => {
|
|
73
|
+
setTimeout(() => updateIndexes())
|
|
74
|
+
})
|
|
75
|
+
.catch((e) => {
|
|
76
|
+
// eslint-disable-next-line no-unused-expressions
|
|
77
|
+
console.error(e)
|
|
78
|
+
setError(true)
|
|
79
|
+
})
|
|
80
|
+
.finally(() => {
|
|
81
|
+
setLoading(false)
|
|
82
|
+
})
|
|
83
|
+
},
|
|
84
|
+
[client, updateIndexes],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const onSelectIndex = useCallback(
|
|
88
|
+
(index: IndexState) => {
|
|
89
|
+
setSelectedIndex(index)
|
|
90
|
+
updateIndexes()
|
|
91
|
+
},
|
|
92
|
+
[setSelectedIndex, updateIndexes],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
updateIndexes()
|
|
97
|
+
}, [updateIndexes])
|
|
98
|
+
|
|
99
|
+
const openCreate = useCallback(() => setCreateIndexOpen(true), [])
|
|
100
|
+
const onSubmit = useCallback(
|
|
101
|
+
(index: IndexState) => {
|
|
102
|
+
setIndexes((current) => [...current, index])
|
|
103
|
+
setSelectedIndex(index)
|
|
104
|
+
updateIndexes()
|
|
105
|
+
},
|
|
106
|
+
[updateIndexes],
|
|
107
|
+
)
|
|
108
|
+
return (
|
|
109
|
+
<Stack space={4}>
|
|
110
|
+
<Flex gap={2} align="center" style={{height: 30}}>
|
|
111
|
+
<Box flex={1}>
|
|
112
|
+
<Heading size={1}>Embeddings indexes</Heading>
|
|
113
|
+
</Box>
|
|
114
|
+
<Box style={{justifySelf: 'flex-end'}}>
|
|
115
|
+
<Button
|
|
116
|
+
icon={AddIcon}
|
|
117
|
+
text={'New index'}
|
|
118
|
+
tone="default"
|
|
119
|
+
mode="ghost"
|
|
120
|
+
onClick={openCreate}
|
|
121
|
+
/>
|
|
122
|
+
</Box>
|
|
123
|
+
<Button
|
|
124
|
+
size={1}
|
|
125
|
+
icon={loading ? <Spinner /> : UndoIcon}
|
|
126
|
+
title={'Refresh index list'}
|
|
127
|
+
tone="default"
|
|
128
|
+
mode="bleed"
|
|
129
|
+
onClick={updateIndexes}
|
|
130
|
+
disabled={loading}
|
|
131
|
+
/>
|
|
132
|
+
</Flex>
|
|
133
|
+
{error ? (
|
|
134
|
+
<Card tone="critical" padding={2} border>
|
|
135
|
+
An error occurred. See console for details.
|
|
136
|
+
</Card>
|
|
137
|
+
) : null}
|
|
138
|
+
<IndexList
|
|
139
|
+
loading={loading}
|
|
140
|
+
indexes={indexes}
|
|
141
|
+
selectedIndex={selectedIndex}
|
|
142
|
+
onIndexSelected={onSelectIndex}
|
|
143
|
+
/>
|
|
144
|
+
{selectedIndex && (
|
|
145
|
+
<IndexInfo selectedIndex={selectedIndex} onDeleteIndex={deleteNamedIndex} />
|
|
146
|
+
)}
|
|
147
|
+
<EditIndexDialog open={createIndexOpen} onClose={onCreateIndexClose} onSubmit={onSubmit} />
|
|
148
|
+
</Stack>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {IndexState, NamedIndex} from '../api/embeddingsApi'
|
|
2
|
+
import {useSchema} from 'sanity'
|
|
3
|
+
import {FormEvent, useCallback, useEffect, useId, useRef, useState} from 'react'
|
|
4
|
+
import {Box, Button, Card, Dialog, Spinner, Stack, Text} from '@sanity/ui'
|
|
5
|
+
import {AddIcon} from '@sanity/icons'
|
|
6
|
+
import {useApiClient} from '../api/embeddingsApiHooks'
|
|
7
|
+
import {useDefaultIndex} from './hooks'
|
|
8
|
+
import {IndexFormInput} from './IndexFormInput'
|
|
9
|
+
|
|
10
|
+
export function EditIndexDialog(props: {
|
|
11
|
+
open: boolean
|
|
12
|
+
onClose: () => void
|
|
13
|
+
onSubmit: (index: IndexState) => void
|
|
14
|
+
}) {
|
|
15
|
+
const {open, onClose, onSubmit} = props
|
|
16
|
+
const id = useId()
|
|
17
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!open) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
setTimeout(() => ref.current?.querySelector('input')?.focus())
|
|
24
|
+
}, [ref, open])
|
|
25
|
+
|
|
26
|
+
const handleSubmit = useCallback(
|
|
27
|
+
(index: IndexState) => {
|
|
28
|
+
onSubmit(index)
|
|
29
|
+
onClose()
|
|
30
|
+
},
|
|
31
|
+
[onSubmit, onClose],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return open ? (
|
|
35
|
+
<Dialog id={id} width={1} ref={ref} onClose={onClose} header="Create embeddings index">
|
|
36
|
+
<Stack padding={4} space={5}>
|
|
37
|
+
<IndexEditor readOnly={false} onSubmit={handleSubmit} />
|
|
38
|
+
</Stack>
|
|
39
|
+
</Dialog>
|
|
40
|
+
) : null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function IndexEditor(props: {
|
|
44
|
+
index?: Partial<NamedIndex>
|
|
45
|
+
readOnly: boolean
|
|
46
|
+
onSubmit?: (index: IndexState) => void
|
|
47
|
+
}) {
|
|
48
|
+
const {readOnly, index: selectedIndex, onSubmit} = props
|
|
49
|
+
const client = useApiClient()
|
|
50
|
+
const schema = useSchema()
|
|
51
|
+
const defaultIndex = useDefaultIndex(schema, client.config().dataset ?? '')
|
|
52
|
+
const [errors, setErrors] = useState<string[] | undefined>()
|
|
53
|
+
const [loading, setLoading] = useState<boolean>()
|
|
54
|
+
const [index, setIndex] = useState<Partial<NamedIndex>>(() => ({
|
|
55
|
+
...defaultIndex,
|
|
56
|
+
...selectedIndex,
|
|
57
|
+
}))
|
|
58
|
+
|
|
59
|
+
useEffect(() => setIndex(selectedIndex ?? {...defaultIndex}), [selectedIndex, defaultIndex])
|
|
60
|
+
|
|
61
|
+
const handleSubmit = useCallback(
|
|
62
|
+
(e: FormEvent) => {
|
|
63
|
+
e.preventDefault()
|
|
64
|
+
if (readOnly) {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const validationErrors: string[] = []
|
|
69
|
+
|
|
70
|
+
if (!index.indexName) {
|
|
71
|
+
validationErrors.push('Index name is required')
|
|
72
|
+
} else if (!index.indexName.match(/^[a-zA-Z0-9-_]+$/g)) {
|
|
73
|
+
validationErrors.push('Index name can only contain the letters a-z, numbers - and _')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!index.dataset) {
|
|
77
|
+
validationErrors.push('Dataset is required')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!index.filter) {
|
|
81
|
+
validationErrors.push('Filter is required')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!index.projection) {
|
|
85
|
+
validationErrors.push('Projection is required')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (validationErrors.length) {
|
|
89
|
+
setErrors(validationErrors)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const {projectId} = client.config()
|
|
94
|
+
setLoading(true)
|
|
95
|
+
client
|
|
96
|
+
.request({
|
|
97
|
+
method: 'POST',
|
|
98
|
+
url: `/embeddings-index/${index.dataset}?projectId=${projectId}`,
|
|
99
|
+
body: {
|
|
100
|
+
indexName: index.indexName,
|
|
101
|
+
projection: index.projection,
|
|
102
|
+
filter: index.filter,
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
.then((response: {index: IndexState}) => {
|
|
106
|
+
if (onSubmit) {
|
|
107
|
+
onSubmit(response.index)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
.catch((err: any) => {
|
|
111
|
+
console.error(err)
|
|
112
|
+
setErrors([err.message])
|
|
113
|
+
})
|
|
114
|
+
.finally(() => {
|
|
115
|
+
setLoading(false)
|
|
116
|
+
})
|
|
117
|
+
},
|
|
118
|
+
[index, readOnly, onSubmit, client],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<form onSubmit={handleSubmit}>
|
|
123
|
+
<Stack space={4}>
|
|
124
|
+
{errors?.length ? (
|
|
125
|
+
<Card tone="critical" border padding={2}>
|
|
126
|
+
<Text>
|
|
127
|
+
<ul style={{marginLeft: -10}}>
|
|
128
|
+
{/* eslint-disable-next-line react/no-array-index-key */}
|
|
129
|
+
{errors?.map((error, i) => <li key={`${error}-${i}`}>{error}</li>)}
|
|
130
|
+
</ul>
|
|
131
|
+
</Text>
|
|
132
|
+
</Card>
|
|
133
|
+
) : null}
|
|
134
|
+
<IndexFormInput
|
|
135
|
+
label="Index name"
|
|
136
|
+
placeholder={'Name of index without spaces...'}
|
|
137
|
+
index={index}
|
|
138
|
+
prop="indexName"
|
|
139
|
+
onChange={setIndex}
|
|
140
|
+
readOnly={readOnly}
|
|
141
|
+
/>
|
|
142
|
+
<IndexFormInput label="Dataset" index={index} prop="dataset" onChange={setIndex} readOnly />
|
|
143
|
+
<IndexFormInput
|
|
144
|
+
label="Projection"
|
|
145
|
+
placeholder={defaultIndex.projection}
|
|
146
|
+
index={index}
|
|
147
|
+
prop="projection"
|
|
148
|
+
onChange={setIndex}
|
|
149
|
+
readOnly={readOnly}
|
|
150
|
+
type="textarea"
|
|
151
|
+
/>
|
|
152
|
+
<IndexFormInput
|
|
153
|
+
label="Filter"
|
|
154
|
+
placeholder={defaultIndex.filter}
|
|
155
|
+
index={index}
|
|
156
|
+
prop="filter"
|
|
157
|
+
onChange={setIndex}
|
|
158
|
+
readOnly={readOnly}
|
|
159
|
+
type="textarea"
|
|
160
|
+
/>
|
|
161
|
+
{onSubmit && (
|
|
162
|
+
<Button
|
|
163
|
+
type="submit"
|
|
164
|
+
text="Create index"
|
|
165
|
+
icon={
|
|
166
|
+
loading ? (
|
|
167
|
+
<Box style={{marginTop: 5}}>
|
|
168
|
+
<Spinner />
|
|
169
|
+
</Box>
|
|
170
|
+
) : (
|
|
171
|
+
AddIcon
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
disabled={readOnly || loading}
|
|
175
|
+
tone="primary"
|
|
176
|
+
/>
|
|
177
|
+
)}
|
|
178
|
+
</Stack>
|
|
179
|
+
</form>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {NamedIndex} from '../api/embeddingsApi'
|
|
2
|
+
import {Dispatch, FormEvent, SetStateAction, useCallback, useId} from 'react'
|
|
3
|
+
import {Label, Stack, TextArea, TextInput} from '@sanity/ui'
|
|
4
|
+
|
|
5
|
+
export interface IndexFormInputProps {
|
|
6
|
+
index: Partial<NamedIndex>
|
|
7
|
+
prop: keyof NamedIndex
|
|
8
|
+
label: string
|
|
9
|
+
onChange: Dispatch<SetStateAction<Partial<NamedIndex>>>
|
|
10
|
+
readOnly: boolean
|
|
11
|
+
placeholder?: string
|
|
12
|
+
type?: 'text' | 'textarea'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function IndexFormInput(props: IndexFormInputProps) {
|
|
16
|
+
const {label, index, prop, onChange, readOnly, placeholder, type} = props
|
|
17
|
+
const handleChange = useCallback(
|
|
18
|
+
(propValue: string) => onChange((current) => ({...current, [prop]: propValue})),
|
|
19
|
+
[onChange, prop],
|
|
20
|
+
)
|
|
21
|
+
return (
|
|
22
|
+
<FormInput
|
|
23
|
+
label={label}
|
|
24
|
+
onChange={handleChange}
|
|
25
|
+
value={index[prop] ?? ''}
|
|
26
|
+
readOnly={readOnly}
|
|
27
|
+
placeholder={placeholder}
|
|
28
|
+
type={type}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface FormInputProps {
|
|
34
|
+
label: string
|
|
35
|
+
onChange: (value: string) => void
|
|
36
|
+
value: string
|
|
37
|
+
readOnly: boolean
|
|
38
|
+
placeholder?: string
|
|
39
|
+
type?: 'text' | 'textarea'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function FormInput(props: FormInputProps) {
|
|
43
|
+
const {label, onChange, value, readOnly, placeholder, type = 'text'} = props
|
|
44
|
+
const id = useId()
|
|
45
|
+
const handleChange = useCallback(
|
|
46
|
+
(e: FormEvent<HTMLInputElement | HTMLTextAreaElement>) => onChange(e.currentTarget.value),
|
|
47
|
+
[onChange],
|
|
48
|
+
)
|
|
49
|
+
return (
|
|
50
|
+
<Stack space={3}>
|
|
51
|
+
<Label muted htmlFor={id}>
|
|
52
|
+
<label htmlFor={id}>{label}</label>
|
|
53
|
+
</Label>
|
|
54
|
+
{type === 'text' ? (
|
|
55
|
+
<TextInput
|
|
56
|
+
id={id}
|
|
57
|
+
value={value}
|
|
58
|
+
onChange={handleChange}
|
|
59
|
+
readOnly={readOnly}
|
|
60
|
+
placeholder={placeholder}
|
|
61
|
+
/>
|
|
62
|
+
) : (
|
|
63
|
+
<TextArea
|
|
64
|
+
id={id}
|
|
65
|
+
value={value}
|
|
66
|
+
rows={3}
|
|
67
|
+
onChange={handleChange}
|
|
68
|
+
readOnly={readOnly}
|
|
69
|
+
placeholder={placeholder}
|
|
70
|
+
style={{resize: 'vertical'}}
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
</Stack>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Box,
|
|
3
|
+
Button,
|
|
4
|
+
Flex,
|
|
5
|
+
Heading,
|
|
6
|
+
Label,
|
|
7
|
+
Menu,
|
|
8
|
+
MenuButton,
|
|
9
|
+
MenuItem,
|
|
10
|
+
Stack,
|
|
11
|
+
Text,
|
|
12
|
+
} from '@sanity/ui'
|
|
13
|
+
import {EllipsisVerticalIcon, TrashIcon} from '@sanity/icons'
|
|
14
|
+
import {IndexEditor} from './IndexEditor'
|
|
15
|
+
import {QueryIndex} from './QueryIndex'
|
|
16
|
+
import {IndexState} from '../api/embeddingsApi'
|
|
17
|
+
import {useCallback} from 'react'
|
|
18
|
+
|
|
19
|
+
export interface IndexInfoProps {
|
|
20
|
+
selectedIndex: IndexState
|
|
21
|
+
onDeleteIndex: (index: IndexState) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function IndexInfo({selectedIndex, onDeleteIndex}: IndexInfoProps) {
|
|
25
|
+
const handleDelete = useCallback(
|
|
26
|
+
() => onDeleteIndex(selectedIndex),
|
|
27
|
+
[selectedIndex, onDeleteIndex],
|
|
28
|
+
)
|
|
29
|
+
return (
|
|
30
|
+
<Stack space={4} flex={1}>
|
|
31
|
+
<Flex align="center" flex={1} gap={2}>
|
|
32
|
+
<Box flex={1}>
|
|
33
|
+
<Heading>Index: {selectedIndex?.indexName ?? 'Untitled'}</Heading>
|
|
34
|
+
</Box>
|
|
35
|
+
<Box>
|
|
36
|
+
<MenuButton
|
|
37
|
+
button={
|
|
38
|
+
<Button
|
|
39
|
+
title="Open index actions"
|
|
40
|
+
icon={EllipsisVerticalIcon}
|
|
41
|
+
padding={2}
|
|
42
|
+
mode="ghost"
|
|
43
|
+
/>
|
|
44
|
+
}
|
|
45
|
+
id={`button-${selectedIndex.indexName}`}
|
|
46
|
+
menu={
|
|
47
|
+
<Menu>
|
|
48
|
+
<MenuItem
|
|
49
|
+
text="Delete index"
|
|
50
|
+
icon={TrashIcon}
|
|
51
|
+
tone="critical"
|
|
52
|
+
onClick={handleDelete}
|
|
53
|
+
/>
|
|
54
|
+
</Menu>
|
|
55
|
+
}
|
|
56
|
+
popover={{placement: 'right'}}
|
|
57
|
+
/>
|
|
58
|
+
</Box>
|
|
59
|
+
</Flex>
|
|
60
|
+
|
|
61
|
+
<Flex gap={6}>
|
|
62
|
+
<Stack space={4} flex={1} style={{maxWidth: 600}}>
|
|
63
|
+
<Box>
|
|
64
|
+
<IndexEditor index={selectedIndex} readOnly />
|
|
65
|
+
</Box>
|
|
66
|
+
<IndexStatus selectedIndex={selectedIndex} />
|
|
67
|
+
</Stack>
|
|
68
|
+
|
|
69
|
+
<Stack space={3} flex={1}>
|
|
70
|
+
<Label muted>Query index</Label>
|
|
71
|
+
<QueryIndex indexName={selectedIndex.indexName} key={selectedIndex.indexName} />
|
|
72
|
+
</Stack>
|
|
73
|
+
</Flex>
|
|
74
|
+
</Stack>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function IndexStatus({selectedIndex}: {selectedIndex: IndexState}) {
|
|
79
|
+
return (
|
|
80
|
+
<Stack space={4} flex={1}>
|
|
81
|
+
<Flex gap={2} align="center">
|
|
82
|
+
<Box flex={1}>
|
|
83
|
+
<Label size={1} muted>
|
|
84
|
+
Status
|
|
85
|
+
</Label>
|
|
86
|
+
</Box>
|
|
87
|
+
<Stack space={2}>
|
|
88
|
+
<Text>{selectedIndex.status}</Text>
|
|
89
|
+
</Stack>
|
|
90
|
+
</Flex>
|
|
91
|
+
<Flex gap={5} align="center">
|
|
92
|
+
<Box flex={1}>
|
|
93
|
+
<Label size={1} muted>
|
|
94
|
+
Indexing progress
|
|
95
|
+
</Label>
|
|
96
|
+
</Box>
|
|
97
|
+
<Stack space={2}>
|
|
98
|
+
<Text>
|
|
99
|
+
{selectedIndex.startDocumentCount - selectedIndex.remainingDocumentCount} /{' '}
|
|
100
|
+
{selectedIndex.startDocumentCount}
|
|
101
|
+
</Text>
|
|
102
|
+
</Stack>
|
|
103
|
+
</Flex>
|
|
104
|
+
<Flex gap={5} align="center">
|
|
105
|
+
<Box flex={1}>
|
|
106
|
+
<Label size={1} muted>
|
|
107
|
+
Failed documents
|
|
108
|
+
</Label>
|
|
109
|
+
</Box>
|
|
110
|
+
<Stack space={2}>
|
|
111
|
+
<Text>{selectedIndex.failedDocumentCount}</Text>
|
|
112
|
+
</Stack>
|
|
113
|
+
</Flex>
|
|
114
|
+
</Stack>
|
|
115
|
+
)
|
|
116
|
+
}
|