@sanity/assist 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 +205 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.esm.js +2341 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +2341 -0
- package/dist/index.js.map +1 -0
- package/package.json +98 -0
- package/sanity.json +8 -0
- package/src/_lib/connector/ConnectFromRegion.tsx +24 -0
- package/src/_lib/connector/ConnectToRegion.tsx +22 -0
- package/src/_lib/connector/ConnectorRegion.tsx +23 -0
- package/src/_lib/connector/ConnectorsProvider.tsx +19 -0
- package/src/_lib/connector/ConnectorsStore.ts +122 -0
- package/src/_lib/connector/ConnectorsStoreContext.ts +4 -0
- package/src/_lib/connector/helpers.ts +5 -0
- package/src/_lib/connector/index.ts +9 -0
- package/src/_lib/connector/mapConnectorToLine.ts +83 -0
- package/src/_lib/connector/types.ts +56 -0
- package/src/_lib/connector/useConnectorsStore.ts +13 -0
- package/src/_lib/connector/useRegionRects.ts +141 -0
- package/src/_lib/fixedListenQuery.ts +101 -0
- package/src/_lib/form/DocumentForm.tsx +197 -0
- package/src/_lib/form/helpers.ts +31 -0
- package/src/_lib/form/index.ts +1 -0
- package/src/_lib/randomKey.ts +29 -0
- package/src/_lib/useListeningQuery.ts +61 -0
- package/src/_lib/usePrevious.ts +9 -0
- package/src/assistConnectors/AssistConnectorsOverlay.tsx +132 -0
- package/src/assistConnectors/ConnectorPath.tsx +62 -0
- package/src/assistConnectors/draw/arrowPath.ts +9 -0
- package/src/assistConnectors/draw/connectorPath.ts +142 -0
- package/src/assistConnectors/index.ts +1 -0
- package/src/assistDocument/AssistDocumentContext.tsx +31 -0
- package/src/assistDocument/AssistDocumentContextProvider.tsx +17 -0
- package/src/assistDocument/AssistDocumentInput.tsx +46 -0
- package/src/assistDocument/RequestRunInstructionProvider.tsx +50 -0
- package/src/assistDocument/components/AssistDocumentForm.tsx +188 -0
- package/src/assistDocument/components/FieldRefPreview.tsx +27 -0
- package/src/assistDocument/components/InstructionsArrayField.tsx +8 -0
- package/src/assistDocument/components/InstructionsArrayInput.tsx +26 -0
- package/src/assistDocument/components/SelectedFieldContext.tsx +10 -0
- package/src/assistDocument/components/generic/HiddenFieldTitle.tsx +5 -0
- package/src/assistDocument/components/helpers.ts +21 -0
- package/src/assistDocument/components/instruction/BackToInstructionsLink.tsx +31 -0
- package/src/assistDocument/components/instruction/FieldRefInput.tsx +33 -0
- package/src/assistDocument/components/instruction/InstructionInput.tsx +87 -0
- package/src/assistDocument/components/instruction/PromptInput.tsx +52 -0
- package/src/assistDocument/components/instruction/appearance/IconInput.tsx +46 -0
- package/src/assistDocument/components/instruction/appearance/InstructionVisibility.tsx +37 -0
- package/src/assistDocument/hooks/useAssistDocumentContextValue.tsx +68 -0
- package/src/assistDocument/hooks/useDocumentState.ts +6 -0
- package/src/assistDocument/hooks/useInstructionToaster.tsx +74 -0
- package/src/assistDocument/hooks/useStudioAssistDocument.ts +119 -0
- package/src/assistDocument/index.ts +1 -0
- package/src/assistFormComponents/AssistField.tsx +51 -0
- package/src/assistFormComponents/AssistFormBlock.tsx +31 -0
- package/src/assistFormComponents/AssistInlineFormBlock.tsx +14 -0
- package/src/assistFormComponents/AssistItem.tsx +20 -0
- package/src/assistFormComponents/validation/listItem.tsx +63 -0
- package/src/assistFormComponents/validation/validationList.tsx +89 -0
- package/src/assistInspector/AssistInspector.tsx +379 -0
- package/src/assistInspector/FieldAutocomplete.tsx +119 -0
- package/src/assistInspector/InstructionTaskHistoryButton.tsx +261 -0
- package/src/assistInspector/constants.ts +1 -0
- package/src/assistInspector/helpers.ts +125 -0
- package/src/assistInspector/index.ts +26 -0
- package/src/assistLayout/AiAssistanceConfigContext.tsx +81 -0
- package/src/assistLayout/AlphaMigration.tsx +311 -0
- package/src/assistLayout/AssistLayout.tsx +38 -0
- package/src/assistLayout/RunInstructionProvider.tsx +222 -0
- package/src/components/AssistFeatureBadge.tsx +9 -0
- package/src/components/Delay.tsx +25 -0
- package/src/components/HideReferenceChangedBannerInput.tsx +25 -0
- package/src/components/SafeValueInput.tsx +73 -0
- package/src/components/TimeAgo.tsx +18 -0
- package/src/constants.ts +20 -0
- package/src/fieldActions/PrivateIcon.tsx +20 -0
- package/src/fieldActions/assistFieldActions.tsx +230 -0
- package/src/globals.d.ts +4 -0
- package/src/helpers/assistSupported.ts +44 -0
- package/src/helpers/ids.ts +19 -0
- package/src/helpers/misc.ts +16 -0
- package/src/helpers/typeUtils.ts +15 -0
- package/src/helpers/useAssistSupported.ts +10 -0
- package/src/index.ts +6 -0
- package/src/legacy-types.ts +72 -0
- package/src/onboarding/FieldActionsOnboarding.tsx +90 -0
- package/src/onboarding/FirstAssistedPathProvider.tsx +29 -0
- package/src/onboarding/InspectorOnboarding.tsx +46 -0
- package/src/onboarding/onboardingStore.ts +33 -0
- package/src/plugin.tsx +80 -0
- package/src/presence/AiFieldPresence.tsx +28 -0
- package/src/presence/AssistAvatar.tsx +96 -0
- package/src/presence/AssistDocumentPresence.tsx +58 -0
- package/src/presence/useAssistPresence.ts +61 -0
- package/src/schemas/assistDocumentSchema.tsx +450 -0
- package/src/schemas/contextDocumentSchema.tsx +56 -0
- package/src/schemas/index.ts +25 -0
- package/src/schemas/serialize/SchemTypeTool.tsx +102 -0
- package/src/schemas/serialize/schemaUtils.ts +37 -0
- package/src/schemas/serialize/serializeSchema.test.ts +382 -0
- package/src/schemas/serialize/serializeSchema.ts +162 -0
- package/src/schemas/serializedSchemaTypeSchema.ts +59 -0
- package/src/schemas/typeDefExtensions.ts +30 -0
- package/src/types.ts +167 -0
- package/src/useApiClient.ts +140 -0
- package/src/vite.config.ts +9 -0
- package/v2-incompatible.js +11 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import {AssistTasksStatus, InstructionTask, StudioInstruction, TaskEndedReason} from '../types'
|
|
2
|
+
import {createElement, ForwardedRef, forwardRef, useCallback, useMemo, useState} from 'react'
|
|
3
|
+
import {
|
|
4
|
+
Box,
|
|
5
|
+
Button,
|
|
6
|
+
Card,
|
|
7
|
+
Flex,
|
|
8
|
+
Popover,
|
|
9
|
+
Spinner,
|
|
10
|
+
Stack,
|
|
11
|
+
Text,
|
|
12
|
+
useClickOutside,
|
|
13
|
+
useGlobalKeyDown,
|
|
14
|
+
useLayer,
|
|
15
|
+
} from '@sanity/ui'
|
|
16
|
+
import {
|
|
17
|
+
CheckmarkCircleIcon,
|
|
18
|
+
ClockIcon,
|
|
19
|
+
CloseCircleIcon,
|
|
20
|
+
ErrorOutlineIcon,
|
|
21
|
+
SyncIcon,
|
|
22
|
+
} from '@sanity/icons'
|
|
23
|
+
import {TimeAgo} from '../components/TimeAgo'
|
|
24
|
+
import {StatusButton, StatusButtonProps, typed, useClient} from 'sanity'
|
|
25
|
+
import {getInstructionTitle} from '../helpers/misc'
|
|
26
|
+
import styled, {keyframes} from 'styled-components'
|
|
27
|
+
import {assistTasksStatusId} from '../helpers/ids'
|
|
28
|
+
import {maxHistoryVisibilityMs, pluginTitle} from '../constants'
|
|
29
|
+
|
|
30
|
+
export interface InstructionTaskHistoryButtonProps {
|
|
31
|
+
documentId?: string
|
|
32
|
+
tasks: InstructionTask[] | undefined
|
|
33
|
+
instructions: StudioInstruction[] | undefined
|
|
34
|
+
showTitles: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface CancelableInstructionTask extends InstructionTask {
|
|
38
|
+
cancel: () => void
|
|
39
|
+
title?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const rotate = keyframes`
|
|
43
|
+
0% {
|
|
44
|
+
transform: rotate(0);
|
|
45
|
+
}
|
|
46
|
+
100% {
|
|
47
|
+
transform: rotate(360deg);
|
|
48
|
+
}
|
|
49
|
+
`
|
|
50
|
+
|
|
51
|
+
const SyncSpinningIcon = styled(SyncIcon)`
|
|
52
|
+
animation: ${rotate} 1s linear infinite;
|
|
53
|
+
`
|
|
54
|
+
|
|
55
|
+
const TASK_CONFIG = {
|
|
56
|
+
aborted: {
|
|
57
|
+
title: 'Canceled',
|
|
58
|
+
icon: CloseCircleIcon,
|
|
59
|
+
tone: 'transparent',
|
|
60
|
+
},
|
|
61
|
+
error: {
|
|
62
|
+
title: 'Error',
|
|
63
|
+
icon: ErrorOutlineIcon,
|
|
64
|
+
tone: 'critical',
|
|
65
|
+
},
|
|
66
|
+
success: {
|
|
67
|
+
title: 'Completed',
|
|
68
|
+
icon: CheckmarkCircleIcon,
|
|
69
|
+
tone: 'positive',
|
|
70
|
+
},
|
|
71
|
+
timeout: {
|
|
72
|
+
title: 'Timeout',
|
|
73
|
+
icon: ClockIcon,
|
|
74
|
+
tone: 'caution',
|
|
75
|
+
},
|
|
76
|
+
} as const
|
|
77
|
+
|
|
78
|
+
export function InstructionTaskHistoryButton(props: InstructionTaskHistoryButtonProps) {
|
|
79
|
+
const {tasks, instructions, documentId, showTitles} = props
|
|
80
|
+
|
|
81
|
+
const client = useClient({apiVersion: '2023-01-01'})
|
|
82
|
+
const cancelRun = useCallback(
|
|
83
|
+
(taskKey: string) => {
|
|
84
|
+
if (!documentId) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
const statusDocId = assistTasksStatusId(documentId)
|
|
88
|
+
const basePath = `${typed<keyof AssistTasksStatus>('tasks')}[_key=="${taskKey}"]`
|
|
89
|
+
client
|
|
90
|
+
.patch(statusDocId)
|
|
91
|
+
.set({
|
|
92
|
+
[`${basePath}.${typed<keyof InstructionTask>('ended')}`]: new Date().toISOString(),
|
|
93
|
+
[`${basePath}.${typed<keyof InstructionTask>('reason')}`]:
|
|
94
|
+
typed<TaskEndedReason>('aborted'),
|
|
95
|
+
})
|
|
96
|
+
.commit()
|
|
97
|
+
.catch(console.error)
|
|
98
|
+
},
|
|
99
|
+
[client, documentId]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const titledTasks = useMemo(() => {
|
|
103
|
+
const t =
|
|
104
|
+
tasks
|
|
105
|
+
?.filter(
|
|
106
|
+
(task) =>
|
|
107
|
+
task.started &&
|
|
108
|
+
new Date().getTime() - new Date(task.started).getTime() < maxHistoryVisibilityMs
|
|
109
|
+
)
|
|
110
|
+
.map((task): CancelableInstructionTask => {
|
|
111
|
+
const instruction = instructions?.find((i) => i._key === task.instructionKey)
|
|
112
|
+
return {
|
|
113
|
+
...task,
|
|
114
|
+
title: showTitles ? getInstructionTitle(instruction) : undefined,
|
|
115
|
+
cancel: () => cancelRun(task._key),
|
|
116
|
+
}
|
|
117
|
+
}) ?? []
|
|
118
|
+
t.sort((a, b) => new Date(b.started ?? '').getTime() - new Date(a.started ?? '').getTime())
|
|
119
|
+
return t
|
|
120
|
+
}, [tasks, instructions, cancelRun, showTitles])
|
|
121
|
+
|
|
122
|
+
// const id = useId()
|
|
123
|
+
|
|
124
|
+
const isRunning = useMemo(() => titledTasks.some((r) => !r.ended), [titledTasks])
|
|
125
|
+
const hasErrors = useMemo(
|
|
126
|
+
() => titledTasks.some((r) => r.reason === 'error' || r.reason === 'timeout'),
|
|
127
|
+
[titledTasks]
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const [open, setOpen] = useState(false)
|
|
131
|
+
|
|
132
|
+
const toggleOpen = useCallback(() => setOpen((v) => !v), [])
|
|
133
|
+
|
|
134
|
+
const [button, setButton] = useState<HTMLButtonElement | null>(null)
|
|
135
|
+
const [popover, setPopover] = useState<HTMLDivElement | null>(null)
|
|
136
|
+
|
|
137
|
+
const handleClickOutside = useCallback(() => {
|
|
138
|
+
setOpen(false)
|
|
139
|
+
}, [])
|
|
140
|
+
|
|
141
|
+
useClickOutside(handleClickOutside, [button, popover])
|
|
142
|
+
|
|
143
|
+
const handleEscape = useCallback(() => {
|
|
144
|
+
setOpen(false)
|
|
145
|
+
button?.focus()
|
|
146
|
+
}, [button])
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<Popover
|
|
150
|
+
constrainSize
|
|
151
|
+
content={<TaskList onEscape={handleEscape} tasks={titledTasks} />}
|
|
152
|
+
open={open && !!titledTasks?.length}
|
|
153
|
+
placement="top"
|
|
154
|
+
portal
|
|
155
|
+
ref={setPopover}
|
|
156
|
+
width={0}
|
|
157
|
+
>
|
|
158
|
+
<TaskStatusButton
|
|
159
|
+
disabled={!titledTasks?.length}
|
|
160
|
+
hasErrors={hasErrors}
|
|
161
|
+
isRunning={isRunning}
|
|
162
|
+
onClick={toggleOpen}
|
|
163
|
+
ref={setButton}
|
|
164
|
+
selected={open}
|
|
165
|
+
/>
|
|
166
|
+
</Popover>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const TASK_STATUS_BUTTON_TOOLTIP_PROPS: StatusButtonProps['tooltip'] = {
|
|
171
|
+
placement: 'top',
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const TaskStatusButton = forwardRef(function TaskStatusButton(
|
|
175
|
+
props: {
|
|
176
|
+
disabled: boolean
|
|
177
|
+
hasErrors: boolean
|
|
178
|
+
isRunning: boolean
|
|
179
|
+
onClick: () => void
|
|
180
|
+
selected: boolean
|
|
181
|
+
},
|
|
182
|
+
ref: ForwardedRef<HTMLButtonElement>
|
|
183
|
+
) {
|
|
184
|
+
const {disabled, hasErrors, isRunning, onClick, selected} = props
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<StatusButton
|
|
188
|
+
label={`${pluginTitle} status`}
|
|
189
|
+
icon={isRunning ? SyncSpinningIcon : hasErrors ? ErrorOutlineIcon : CheckmarkCircleIcon}
|
|
190
|
+
mode="bleed"
|
|
191
|
+
onClick={onClick}
|
|
192
|
+
tone={hasErrors ? 'critical' : undefined}
|
|
193
|
+
fontSize={1}
|
|
194
|
+
disabled={disabled}
|
|
195
|
+
ref={ref}
|
|
196
|
+
selected={selected}
|
|
197
|
+
tooltip={TASK_STATUS_BUTTON_TOOLTIP_PROPS}
|
|
198
|
+
/>
|
|
199
|
+
)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
function TaskList(props: {onEscape: () => void; tasks: CancelableInstructionTask[]}) {
|
|
203
|
+
const {onEscape, tasks} = props
|
|
204
|
+
|
|
205
|
+
const {isTopLayer} = useLayer()
|
|
206
|
+
|
|
207
|
+
useGlobalKeyDown(
|
|
208
|
+
useCallback(
|
|
209
|
+
(event) => {
|
|
210
|
+
if (isTopLayer && event.key === 'Escape') {
|
|
211
|
+
onEscape()
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
[isTopLayer, onEscape]
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<Stack padding={1} space={1}>
|
|
220
|
+
{tasks.map((task) => (
|
|
221
|
+
<TaskItem key={task._key} task={task} />
|
|
222
|
+
))}
|
|
223
|
+
</Stack>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function TaskItem(props: {task: CancelableInstructionTask}) {
|
|
228
|
+
const {task} = props
|
|
229
|
+
|
|
230
|
+
const taskType = task.reason && TASK_CONFIG[task.reason]
|
|
231
|
+
return (
|
|
232
|
+
<Card radius={2} tone={taskType && taskType?.tone}>
|
|
233
|
+
<Flex align="center" gap={1}>
|
|
234
|
+
<Flex align="flex-start" flex={1} gap={3} padding={3}>
|
|
235
|
+
<Box flex="none">
|
|
236
|
+
<Text size={1}>
|
|
237
|
+
{taskType && createElement(taskType.icon)}
|
|
238
|
+
{!task.reason && <Spinner />}
|
|
239
|
+
</Text>
|
|
240
|
+
</Box>
|
|
241
|
+
<Stack flex={1} space={2}>
|
|
242
|
+
<Text size={1} weight="medium">
|
|
243
|
+
{taskType ? taskType.title : 'Running'}
|
|
244
|
+
{task.title && <>: {task.title}</>}
|
|
245
|
+
</Text>
|
|
246
|
+
{task.message ? <Text size={1}>{task.message}</Text> : null}
|
|
247
|
+
<Text muted size={1}>
|
|
248
|
+
<TimeAgo date={task.ended ?? task.started} />
|
|
249
|
+
</Text>
|
|
250
|
+
</Stack>
|
|
251
|
+
</Flex>
|
|
252
|
+
|
|
253
|
+
{!task.ended && (
|
|
254
|
+
<Box flex="none" padding={1}>
|
|
255
|
+
<Button fontSize={1} mode="bleed" onClick={task.cancel} text="Cancel" />
|
|
256
|
+
</Box>
|
|
257
|
+
)}
|
|
258
|
+
</Flex>
|
|
259
|
+
</Card>
|
|
260
|
+
)
|
|
261
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const aiInspectorId = 'ai-assistance'
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BlockContentIcon,
|
|
3
|
+
BlockquoteIcon,
|
|
4
|
+
DocumentIcon,
|
|
5
|
+
ImageIcon,
|
|
6
|
+
LinkIcon,
|
|
7
|
+
OlistIcon,
|
|
8
|
+
StringIcon,
|
|
9
|
+
} from '@sanity/icons'
|
|
10
|
+
import {isObjectSchemaType, ObjectSchemaType, Path, pathToString, SchemaType} from 'sanity'
|
|
11
|
+
import {ComponentType, useContext, useMemo} from 'react'
|
|
12
|
+
import {AssistInspectorRouteParams, documentRootKey, fieldPathParam} from '../types'
|
|
13
|
+
import {usePaneRouter} from 'sanity/desk'
|
|
14
|
+
import {isAssistSupported} from '../helpers/assistSupported'
|
|
15
|
+
import {isPortableTextArray, isType} from '../helpers/typeUtils'
|
|
16
|
+
import {SelectedFieldContext} from '../assistDocument/components/SelectedFieldContext'
|
|
17
|
+
|
|
18
|
+
export interface FieldRef {
|
|
19
|
+
key: string
|
|
20
|
+
path: Path
|
|
21
|
+
title: string
|
|
22
|
+
schemaType: SchemaType
|
|
23
|
+
icon: ComponentType
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getTypeIcon(schemaType: SchemaType) {
|
|
27
|
+
let t: SchemaType | undefined = schemaType
|
|
28
|
+
|
|
29
|
+
while (t) {
|
|
30
|
+
if (t.icon) return t.icon
|
|
31
|
+
t = t.type
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (isType(schemaType, 'slug')) return LinkIcon
|
|
35
|
+
if (isType(schemaType, 'image')) return ImageIcon
|
|
36
|
+
if (schemaType.jsonType === 'array' && isPortableTextArray(schemaType)) return BlockContentIcon
|
|
37
|
+
|
|
38
|
+
if (schemaType.jsonType === 'array') return OlistIcon
|
|
39
|
+
if (schemaType.jsonType === 'object') return BlockquoteIcon
|
|
40
|
+
if (schemaType.jsonType === 'string') return StringIcon
|
|
41
|
+
|
|
42
|
+
return DocumentIcon
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getFieldRefsWithDocument(schemaType: ObjectSchemaType): FieldRef[] {
|
|
46
|
+
const fields = getFieldRefs(schemaType)
|
|
47
|
+
return [
|
|
48
|
+
{
|
|
49
|
+
key: documentRootKey,
|
|
50
|
+
icon: schemaType.icon ?? DocumentIcon,
|
|
51
|
+
title: `The entire document`,
|
|
52
|
+
path: [],
|
|
53
|
+
schemaType: schemaType,
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
...fields,
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getFieldRefs(schemaType: ObjectSchemaType, parent?: FieldRef): FieldRef[] {
|
|
61
|
+
return schemaType.fields
|
|
62
|
+
.filter((f) => !f.name.startsWith('_'))
|
|
63
|
+
.flatMap((field) => {
|
|
64
|
+
const path: Path = parent ? [...parent.path, field.name] : [field.name]
|
|
65
|
+
const title = field.type.title ?? field.name
|
|
66
|
+
const fieldRef: FieldRef = {
|
|
67
|
+
key: pathToString(path),
|
|
68
|
+
path,
|
|
69
|
+
title: parent ? [parent.title, title].join(' / ') : title,
|
|
70
|
+
schemaType: field.type,
|
|
71
|
+
icon: getTypeIcon(field.type),
|
|
72
|
+
}
|
|
73
|
+
const fields = field.type.jsonType === 'object' ? getFieldRefs(field.type, fieldRef) : []
|
|
74
|
+
|
|
75
|
+
if (!isAssistSupported(field.type)) {
|
|
76
|
+
return fields
|
|
77
|
+
}
|
|
78
|
+
return [fieldRef, ...fields]
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function useSelectedField(
|
|
83
|
+
documentSchemaType?: SchemaType,
|
|
84
|
+
path?: string
|
|
85
|
+
): FieldRef | undefined {
|
|
86
|
+
const selectableFields = useMemo(
|
|
87
|
+
() =>
|
|
88
|
+
documentSchemaType && isObjectSchemaType(documentSchemaType)
|
|
89
|
+
? getFieldRefsWithDocument(documentSchemaType)
|
|
90
|
+
: [],
|
|
91
|
+
[documentSchemaType]
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return useMemo(() => {
|
|
95
|
+
return path ? selectableFields?.find((f) => f.key === path) : undefined
|
|
96
|
+
}, [selectableFields, path])
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function useSelectedFieldTitle() {
|
|
100
|
+
const {params} = useAiPaneRouter()
|
|
101
|
+
const {documentSchema} = useContext(SelectedFieldContext) ?? {}
|
|
102
|
+
const selectedField = useSelectedField(documentSchema, params[fieldPathParam])
|
|
103
|
+
return getFieldTitle(selectedField)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function getFieldTitle(field?: FieldRef) {
|
|
107
|
+
const schemaType = field?.schemaType
|
|
108
|
+
return schemaType?.title ?? schemaType?.name ?? 'Untitled'
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function useAiPaneRouter() {
|
|
112
|
+
const paneRouter = usePaneRouter()
|
|
113
|
+
|
|
114
|
+
return useMemo(
|
|
115
|
+
() =>
|
|
116
|
+
({...paneRouter, params: paneRouter.params ?? {}} as Omit<
|
|
117
|
+
typeof paneRouter,
|
|
118
|
+
'setParams' | 'params'
|
|
119
|
+
> & {
|
|
120
|
+
params: AssistInspectorRouteParams
|
|
121
|
+
setParams: (p: Record<keyof AssistInspectorRouteParams, string | undefined>) => void
|
|
122
|
+
}),
|
|
123
|
+
[paneRouter]
|
|
124
|
+
)
|
|
125
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {SparklesIcon} from '@sanity/icons'
|
|
2
|
+
import {DocumentInspector, typed} from 'sanity'
|
|
3
|
+
import {aiInspectorId} from './constants'
|
|
4
|
+
import {AssistInspectorWrapper} from './AssistInspector'
|
|
5
|
+
import {AssistInspectorRouteParams, fieldPathParam, instructionParam} from '../types'
|
|
6
|
+
import {pluginTitle} from '../constants'
|
|
7
|
+
|
|
8
|
+
export const assistInspector: DocumentInspector = {
|
|
9
|
+
name: aiInspectorId,
|
|
10
|
+
useMenuItem: () => ({
|
|
11
|
+
icon: SparklesIcon,
|
|
12
|
+
title: pluginTitle,
|
|
13
|
+
hidden: true,
|
|
14
|
+
showAsAction: false,
|
|
15
|
+
}),
|
|
16
|
+
component: AssistInspectorWrapper,
|
|
17
|
+
onClose({params}) {
|
|
18
|
+
return {
|
|
19
|
+
params: typed<AssistInspectorRouteParams>({
|
|
20
|
+
...params,
|
|
21
|
+
[fieldPathParam]: undefined,
|
|
22
|
+
[instructionParam]: undefined,
|
|
23
|
+
}) as typeof params,
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
ReactNode,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react'
|
|
10
|
+
import {AssistPluginConfig} from '../plugin'
|
|
11
|
+
import {InstructStatus, useApiClient, useGetInstructStatus, useInitInstruct} from '../useApiClient'
|
|
12
|
+
|
|
13
|
+
export interface AiAssistanceConfigContextValue {
|
|
14
|
+
config: AssistPluginConfig
|
|
15
|
+
status?: InstructStatus
|
|
16
|
+
statusLoading: boolean
|
|
17
|
+
initLoading: boolean
|
|
18
|
+
init: () => void
|
|
19
|
+
error?: Error
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const AiAssistanceConfigContext = createContext<AiAssistanceConfigContextValue>({} as any)
|
|
23
|
+
|
|
24
|
+
export function useAiAssistanceConfig() {
|
|
25
|
+
const context = useContext(AiAssistanceConfigContext)
|
|
26
|
+
if (!context) {
|
|
27
|
+
throw new Error('Missing AiAssistanceConfigContext')
|
|
28
|
+
}
|
|
29
|
+
return context
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function AiAssistanceConfigProvider(props: {
|
|
33
|
+
children?: ReactNode
|
|
34
|
+
config: AssistPluginConfig
|
|
35
|
+
}) {
|
|
36
|
+
const [status, setStatus] = useState<InstructStatus | undefined>()
|
|
37
|
+
const [error, setError] = useState<Error | undefined>()
|
|
38
|
+
|
|
39
|
+
const apiClient = useApiClient(props.config?.__customApiClient)
|
|
40
|
+
const {getInstructStatus, loading: statusLoading} = useGetInstructStatus(apiClient)
|
|
41
|
+
const {initInstruct, loading: initLoading} = useInitInstruct(apiClient)
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
getInstructStatus()
|
|
45
|
+
.then((s) => setStatus(s))
|
|
46
|
+
.catch((e) => {
|
|
47
|
+
console.error(e)
|
|
48
|
+
setError(e as Error)
|
|
49
|
+
})
|
|
50
|
+
}, [getInstructStatus])
|
|
51
|
+
|
|
52
|
+
const init = useCallback(async () => {
|
|
53
|
+
setError(undefined)
|
|
54
|
+
try {
|
|
55
|
+
await initInstruct()
|
|
56
|
+
const status = await getInstructStatus()
|
|
57
|
+
setStatus(status)
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.error('Failed to init ai assistance', e)
|
|
60
|
+
setError(e as Error)
|
|
61
|
+
}
|
|
62
|
+
}, [initInstruct, getInstructStatus, setStatus])
|
|
63
|
+
|
|
64
|
+
const {config, children} = props
|
|
65
|
+
const context = useMemo<AiAssistanceConfigContextValue>(() => {
|
|
66
|
+
return {
|
|
67
|
+
config,
|
|
68
|
+
status,
|
|
69
|
+
statusLoading,
|
|
70
|
+
init,
|
|
71
|
+
initLoading,
|
|
72
|
+
error,
|
|
73
|
+
}
|
|
74
|
+
}, [config, status, init, statusLoading, initLoading, error])
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<AiAssistanceConfigContext.Provider value={context}>
|
|
78
|
+
{children}
|
|
79
|
+
</AiAssistanceConfigContext.Provider>
|
|
80
|
+
)
|
|
81
|
+
}
|