@sanity/assist 4.2.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +292 -0
- package/dist/index.d.mts +257 -0
- package/dist/index.d.ts +257 -0
- package/dist/index.esm.js +231 -99
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +226 -94
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +231 -99
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -8
- package/src/assistDocument/AssistDocumentContext.tsx +14 -1
- package/src/assistDocument/hooks/useAssistDocumentContextValue.tsx +30 -3
- package/src/assistLayout/RunInstructionProvider.tsx +75 -23
- package/src/fieldActions/assistFieldActions.tsx +42 -2
- package/src/fieldActions/customFieldActions.tsx +304 -0
- package/src/fieldActions/useUserInput.ts +107 -0
- package/src/index.ts +17 -0
- package/src/plugin.tsx +6 -0
- package/src/presence/AssistDocumentPresence.tsx +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/assist",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "You create the instructions; Sanity AI Assist does the rest.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@sanity/icons": "^3.5.2",
|
|
53
53
|
"@sanity/incompatible-plugin": "^1.0.4",
|
|
54
|
-
"@sanity/ui": "^2.
|
|
54
|
+
"@sanity/ui": "^2.15.18",
|
|
55
55
|
"date-fns": "^3.6.0",
|
|
56
56
|
"lodash": "^4.17.21",
|
|
57
57
|
"lodash-es": "^4.17.21",
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
"@commitlint/cli": "^19.2.1",
|
|
64
64
|
"@commitlint/config-conventional": "^19.1.0",
|
|
65
65
|
"@rollup/plugin-image": "^3.0.3",
|
|
66
|
-
"@sanity/pkg-utils": "^6.
|
|
66
|
+
"@sanity/pkg-utils": "^6.13.4",
|
|
67
67
|
"@sanity/plugin-kit": "^3.1.10",
|
|
68
|
-
"@sanity/schema": "^3.
|
|
68
|
+
"@sanity/schema": "^3.91.0",
|
|
69
69
|
"@sanity/semantic-release-preset": "^4.1.7",
|
|
70
70
|
"@types/lodash": "^4.17.0",
|
|
71
71
|
"@types/lodash-es": "^4.17.12",
|
|
@@ -83,11 +83,11 @@
|
|
|
83
83
|
"react": "^18.2.0",
|
|
84
84
|
"react-dom": "^18.2.0",
|
|
85
85
|
"rimraf": "^5.0.5",
|
|
86
|
-
"sanity": "^3.
|
|
86
|
+
"sanity": "^3.91.0",
|
|
87
87
|
"semantic-release": "^23.0.8",
|
|
88
88
|
"styled-components": "^6.1.16",
|
|
89
89
|
"typescript": "^5.7.2",
|
|
90
|
-
"vitest": "^1.4
|
|
90
|
+
"vitest": "^3.1.4"
|
|
91
91
|
},
|
|
92
92
|
"peerDependencies": {
|
|
93
93
|
"@sanity/mutator": "^3.36.4",
|
|
@@ -96,9 +96,10 @@
|
|
|
96
96
|
"styled-components": "^6.1"
|
|
97
97
|
},
|
|
98
98
|
"engines": {
|
|
99
|
-
"node": ">=
|
|
99
|
+
"node": ">=20"
|
|
100
100
|
},
|
|
101
101
|
"publishConfig": {
|
|
102
102
|
"access": "public"
|
|
103
|
-
}
|
|
103
|
+
},
|
|
104
|
+
"browserslist": "extends @sanity/browserslist-config"
|
|
104
105
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {createContext, useContext} from 'react'
|
|
2
2
|
import {DocumentInspector, ObjectSchemaType, PatchEvent} from 'sanity'
|
|
3
3
|
|
|
4
|
-
import {StudioAssistDocument} from '../types'
|
|
4
|
+
import {InstructionTask, StudioAssistDocument} from '../types'
|
|
5
5
|
|
|
6
6
|
export type AssistDocumentContextValue = (
|
|
7
7
|
| {assistDocument: StudioAssistDocument; loading: false}
|
|
@@ -19,6 +19,19 @@ export type AssistDocumentContextValue = (
|
|
|
19
19
|
inspector: DocumentInspector | null
|
|
20
20
|
selectedPath?: string
|
|
21
21
|
documentOnChange: (event: PatchEvent) => void
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Synthetic task is used to display AI presence at the document level for the user who started the action.
|
|
25
|
+
* These are not persisted, so other users will not see them.
|
|
26
|
+
* It is mostly a helper to give _some_ visual feedback to the user while a custom action is running.
|
|
27
|
+
* This also means that reloading the page will remove the icon.
|
|
28
|
+
*
|
|
29
|
+
* Agent Actions add their own "real" tasks, so if a custom action calls an Agent action, _those_ tasks
|
|
30
|
+
* are visible across sessions.
|
|
31
|
+
*/
|
|
32
|
+
syntheticTasks?: InstructionTask[]
|
|
33
|
+
addSyntheticTask: (syntheticTask: InstructionTask) => void
|
|
34
|
+
removeSyntheticTask: (syntheticTask: InstructionTask) => void
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
export const AssistDocumentContext = createContext<AssistDocumentContextValue | undefined>(
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {useMemo} from 'react'
|
|
1
|
+
import {useCallback, useEffect, useMemo, useState} from 'react'
|
|
2
2
|
import {getDraftId, getVersionId, type ObjectSchemaType, useSchema} from 'sanity'
|
|
3
3
|
import {useDocumentPane} from 'sanity/structure'
|
|
4
4
|
|
|
5
5
|
import {useAiPaneRouter} from '../../assistInspector/helpers'
|
|
6
|
-
import {fieldPathParam} from '../../types'
|
|
6
|
+
import {fieldPathParam, InstructionTask} from '../../types'
|
|
7
7
|
import type {AssistDocumentContextValue} from '../AssistDocumentContext'
|
|
8
8
|
import {isDocAssistable} from '../RequestRunInstructionProvider'
|
|
9
9
|
import {useStudioAssistDocument} from './useStudioAssistDocument'
|
|
@@ -51,7 +51,8 @@ export function useAssistDocumentContextValue(documentId: string, documentType:
|
|
|
51
51
|
documentId: assistableDocumentId,
|
|
52
52
|
schemaType: documentSchemaType,
|
|
53
53
|
})
|
|
54
|
-
|
|
54
|
+
const {syntheticTasks, addSyntheticTask, removeSyntheticTask} =
|
|
55
|
+
useSyntheticTasks(assistableDocumentId)
|
|
55
56
|
const value: AssistDocumentContextValue = useMemo(() => {
|
|
56
57
|
const base = {
|
|
57
58
|
assistableDocumentId,
|
|
@@ -63,6 +64,9 @@ export function useAssistDocumentContextValue(documentId: string, documentType:
|
|
|
63
64
|
inspector,
|
|
64
65
|
documentOnChange,
|
|
65
66
|
selectedPath,
|
|
67
|
+
syntheticTasks,
|
|
68
|
+
addSyntheticTask,
|
|
69
|
+
removeSyntheticTask,
|
|
66
70
|
}
|
|
67
71
|
if (!assistDocument) {
|
|
68
72
|
return {...base, loading: true, assistDocument: undefined}
|
|
@@ -83,7 +87,30 @@ export function useAssistDocumentContextValue(documentId: string, documentType:
|
|
|
83
87
|
inspector,
|
|
84
88
|
documentOnChange,
|
|
85
89
|
selectedPath,
|
|
90
|
+
syntheticTasks,
|
|
91
|
+
addSyntheticTask,
|
|
92
|
+
removeSyntheticTask,
|
|
86
93
|
])
|
|
87
94
|
|
|
88
95
|
return value
|
|
89
96
|
}
|
|
97
|
+
|
|
98
|
+
function useSyntheticTasks(assistableDocumentId: string) {
|
|
99
|
+
const [syntheticTasks, setSyntheticTasks] = useState<InstructionTask[]>(() => [])
|
|
100
|
+
const addSyntheticTask = useCallback((task: InstructionTask) => {
|
|
101
|
+
setSyntheticTasks((current) => [...current, task])
|
|
102
|
+
}, [])
|
|
103
|
+
const removeSyntheticTask = useCallback((task: InstructionTask) => {
|
|
104
|
+
setSyntheticTasks((current) => current.filter((t) => task._key !== t._key))
|
|
105
|
+
}, [])
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
setSyntheticTasks([])
|
|
109
|
+
}, [assistableDocumentId])
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
syntheticTasks,
|
|
113
|
+
addSyntheticTask,
|
|
114
|
+
removeSyntheticTask,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import {PlayIcon} from '@sanity/icons'
|
|
2
2
|
import {Button, Dialog, Flex, Stack, Text, TextArea, Tooltip} from '@sanity/ui'
|
|
3
|
+
import {FormFieldHeaderText} from 'sanity'
|
|
4
|
+
|
|
5
|
+
import {getInstructionTitle} from '../helpers/misc'
|
|
6
|
+
import {type UserInputBlock, userInputTypeName} from '../types'
|
|
7
|
+
import {useApiClient, useRunInstructionApi} from '../useApiClient'
|
|
8
|
+
import {useAiAssistanceConfig} from './AiAssistanceConfigContext'
|
|
9
|
+
import type {RunInstructionArgs} from './AssistLayout'
|
|
10
|
+
import {CustomInputResult, GetUserInput} from '../fieldActions/useUserInput'
|
|
3
11
|
import {
|
|
4
12
|
createContext,
|
|
5
13
|
type Dispatch,
|
|
@@ -14,24 +22,19 @@ import {
|
|
|
14
22
|
useRef,
|
|
15
23
|
useState,
|
|
16
24
|
} from 'react'
|
|
17
|
-
import {FormFieldHeaderText} from 'sanity'
|
|
18
|
-
|
|
19
|
-
import {getInstructionTitle} from '../helpers/misc'
|
|
20
|
-
import {type UserInputBlock, userInputTypeName} from '../types'
|
|
21
|
-
import {useApiClient, useRunInstructionApi} from '../useApiClient'
|
|
22
|
-
import {useAiAssistanceConfig} from './AiAssistanceConfigContext'
|
|
23
|
-
import type {RunInstructionArgs} from './AssistLayout'
|
|
24
25
|
|
|
25
26
|
type BlockInputs = Record<string, string>
|
|
26
27
|
const NO_INPUT: BlockInputs = {}
|
|
27
28
|
|
|
28
29
|
export interface RunInstructionContextValue {
|
|
29
30
|
runInstruction: (req: RunInstructionArgs) => void
|
|
31
|
+
getUserInput: GetUserInput
|
|
30
32
|
instructionLoading: boolean
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export const RunInstructionContext = createContext<RunInstructionContextValue>({
|
|
34
36
|
runInstruction: () => {},
|
|
37
|
+
getUserInput: async () => undefined,
|
|
35
38
|
instructionLoading: false,
|
|
36
39
|
})
|
|
37
40
|
|
|
@@ -53,9 +56,34 @@ export function RunInstructionProvider(props: PropsWithChildren<{}>) {
|
|
|
53
56
|
|
|
54
57
|
const [inputs, setInputs] = useState(NO_INPUT)
|
|
55
58
|
const [runRequest, setRunRequest] = useState<
|
|
56
|
-
(RunInstructionArgs & {userInputBlocks: UserInputBlock[]})
|
|
59
|
+
| (RunInstructionArgs & {userInputBlocks: UserInputBlock[]})
|
|
60
|
+
| {dialogTitle: string; userInputBlocks: UserInputBlock[]}
|
|
61
|
+
| undefined
|
|
57
62
|
>()
|
|
58
63
|
|
|
64
|
+
const [resolveUserInput, setResolveUserInput] =
|
|
65
|
+
useState<
|
|
66
|
+
(
|
|
67
|
+
value: CustomInputResult[] | PromiseLike<CustomInputResult[] | undefined> | undefined,
|
|
68
|
+
) => void
|
|
69
|
+
>()
|
|
70
|
+
|
|
71
|
+
const getUserInput: GetUserInput = useCallback(async ({title, inputs}) => {
|
|
72
|
+
const userInputBlocks: UserInputBlock[] = inputs.map((input, i) => ({
|
|
73
|
+
_type: userInputTypeName,
|
|
74
|
+
_key: input.id ?? `${i}`,
|
|
75
|
+
message: input.title,
|
|
76
|
+
description: input.description,
|
|
77
|
+
}))
|
|
78
|
+
if (!userInputBlocks.length) {
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
setRunRequest({dialogTitle: title, userInputBlocks})
|
|
82
|
+
return new Promise<CustomInputResult[] | undefined>((resolve) => {
|
|
83
|
+
setResolveUserInput(() => resolve)
|
|
84
|
+
})
|
|
85
|
+
}, [])
|
|
86
|
+
|
|
59
87
|
const runInstruction = useCallback(
|
|
60
88
|
(req: RunInstructionArgs) => {
|
|
61
89
|
if (loading) {
|
|
@@ -89,23 +117,43 @@ export function RunInstructionProvider(props: PropsWithChildren<{}>) {
|
|
|
89
117
|
const close = useCallback(() => {
|
|
90
118
|
setRunRequest(undefined)
|
|
91
119
|
setInputs(NO_INPUT)
|
|
92
|
-
|
|
120
|
+
if (resolveUserInput) {
|
|
121
|
+
resolveUserInput(undefined)
|
|
122
|
+
}
|
|
123
|
+
setResolveUserInput(undefined)
|
|
124
|
+
}, [resolveUserInput])
|
|
93
125
|
|
|
94
126
|
const runWithInput = useCallback(() => {
|
|
95
127
|
if (runRequest) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
128
|
+
if ('instruction' in runRequest) {
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
130
|
+
const {instruction, userTexts, ...request} = runRequest
|
|
131
|
+
runInstructionRequest({
|
|
132
|
+
...request,
|
|
133
|
+
instructionKey: instruction._key,
|
|
134
|
+
userTexts: Object.entries(inputs).map(([key, value]) => ({
|
|
135
|
+
blockKey: key,
|
|
136
|
+
userInput: value,
|
|
137
|
+
})),
|
|
138
|
+
})
|
|
139
|
+
} else {
|
|
140
|
+
const userInputs = Object.values(inputs).map((input, i) => {
|
|
141
|
+
const userInputBlock = runRequest.userInputBlocks[i]
|
|
142
|
+
return {
|
|
143
|
+
input: {
|
|
144
|
+
id: userInputBlock._key,
|
|
145
|
+
title: userInputBlock.message ?? '',
|
|
146
|
+
description: userInputBlock.description,
|
|
147
|
+
},
|
|
148
|
+
result: input,
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
resolveUserInput?.(userInputs)
|
|
152
|
+
setResolveUserInput(undefined)
|
|
153
|
+
}
|
|
106
154
|
}
|
|
107
155
|
close()
|
|
108
|
-
}, [close, runInstructionRequest, runRequest, inputs])
|
|
156
|
+
}, [close, runInstructionRequest, runRequest, inputs, resolveUserInput])
|
|
109
157
|
|
|
110
158
|
const open = !!runRequest
|
|
111
159
|
|
|
@@ -128,7 +176,7 @@ export function RunInstructionProvider(props: PropsWithChildren<{}>) {
|
|
|
128
176
|
)
|
|
129
177
|
|
|
130
178
|
const contextValue: RunInstructionContextValue = useMemo(
|
|
131
|
-
() => ({runInstruction, instructionLoading: loading}),
|
|
179
|
+
() => ({runInstruction, getUserInput, instructionLoading: loading}),
|
|
132
180
|
[runInstruction, loading],
|
|
133
181
|
)
|
|
134
182
|
|
|
@@ -140,7 +188,11 @@ export function RunInstructionProvider(props: PropsWithChildren<{}>) {
|
|
|
140
188
|
open={open}
|
|
141
189
|
onClose={close}
|
|
142
190
|
width={1}
|
|
143
|
-
header={
|
|
191
|
+
header={
|
|
192
|
+
'dialogTitle' in runRequest
|
|
193
|
+
? runRequest.dialogTitle
|
|
194
|
+
: getInstructionTitle(runRequest?.instruction)
|
|
195
|
+
}
|
|
144
196
|
footer={
|
|
145
197
|
<Flex justify="space-between" padding={2} flex={1}>
|
|
146
198
|
{runDisabled ? (
|
|
@@ -178,7 +230,7 @@ export function RunInstructionProvider(props: PropsWithChildren<{}>) {
|
|
|
178
230
|
)
|
|
179
231
|
}
|
|
180
232
|
|
|
181
|
-
function UserInput(props: {
|
|
233
|
+
export function UserInput(props: {
|
|
182
234
|
block: UserInputBlock
|
|
183
235
|
inputs: BlockInputs
|
|
184
236
|
setInputs: Dispatch<SetStateAction<BlockInputs>>
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type DocumentFieldAction,
|
|
5
5
|
type DocumentFieldActionGroup,
|
|
6
6
|
type DocumentFieldActionItem,
|
|
7
|
+
stringToPath,
|
|
7
8
|
typed,
|
|
8
9
|
useCurrentUser,
|
|
9
10
|
} from 'sanity'
|
|
@@ -24,6 +25,8 @@ import {documentRootKey, fieldPathParam, instructionParam, type StudioInstructio
|
|
|
24
25
|
import {generateCaptionsActions} from './generateCaptionActions'
|
|
25
26
|
import {generateImagActions} from './generateImageActions'
|
|
26
27
|
import {PrivateIcon} from './PrivateIcon'
|
|
28
|
+
import {AgentActionConditionalPath, useCustomFieldActions} from './customFieldActions'
|
|
29
|
+
import {AgentActionPath} from '@sanity/client/stega'
|
|
27
30
|
|
|
28
31
|
function node(node: DocumentFieldActionItem | DocumentFieldActionGroup) {
|
|
29
32
|
return node
|
|
@@ -48,6 +51,7 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
48
51
|
} = useAssistDocumentContext()
|
|
49
52
|
|
|
50
53
|
const {value: docValue, formState} = useDocumentPane()
|
|
54
|
+
const docValueRef = useRef(docValue)
|
|
51
55
|
const formStateRef = useRef(formState)
|
|
52
56
|
formStateRef.current = formState
|
|
53
57
|
|
|
@@ -180,7 +184,35 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
180
184
|
imageGenAction,
|
|
181
185
|
])
|
|
182
186
|
|
|
183
|
-
const
|
|
187
|
+
const getDocumentValue = useCallback(() => {
|
|
188
|
+
return docValueRef.current
|
|
189
|
+
}, [])
|
|
190
|
+
|
|
191
|
+
const getConditionalPaths: () => AgentActionConditionalPath[] = useCallback(() => {
|
|
192
|
+
return (formStateRef.current ? getConditionalMembers(formStateRef.current) : []).flatMap(
|
|
193
|
+
(cm) => {
|
|
194
|
+
const path = stringToPath(cm.path)
|
|
195
|
+
if (path.some((s) => typeof s === 'number')) {
|
|
196
|
+
//agent actions does not support indexed paths
|
|
197
|
+
return []
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
...cm,
|
|
201
|
+
path: path as AgentActionPath,
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
}, [])
|
|
206
|
+
|
|
207
|
+
const customActions = useCustomFieldActions({
|
|
208
|
+
actionType: props.path.length ? 'field' : 'document',
|
|
209
|
+
documentIdForAction: assistableDocumentId,
|
|
210
|
+
schemaType,
|
|
211
|
+
documentSchemaType,
|
|
212
|
+
path: props.path,
|
|
213
|
+
getDocumentValue,
|
|
214
|
+
getConditionalPaths,
|
|
215
|
+
})
|
|
184
216
|
|
|
185
217
|
const manageInstructionsItem = useMemo(
|
|
186
218
|
() =>
|
|
@@ -203,6 +235,7 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
203
235
|
children: [
|
|
204
236
|
runInstructionsGroup,
|
|
205
237
|
translateAction,
|
|
238
|
+
...customActions,
|
|
206
239
|
assistSupported && manageInstructionsItem,
|
|
207
240
|
]
|
|
208
241
|
.filter((c): c is DocumentFieldActionItem | DocumentFieldActionGroup => !!c)
|
|
@@ -219,6 +252,7 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
219
252
|
imageCaptionAction,
|
|
220
253
|
translateAction,
|
|
221
254
|
imageGenAction,
|
|
255
|
+
customActions,
|
|
222
256
|
],
|
|
223
257
|
)
|
|
224
258
|
|
|
@@ -237,7 +271,13 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
237
271
|
)
|
|
238
272
|
|
|
239
273
|
// If there are no instructions, we don't want to render the group
|
|
240
|
-
if (
|
|
274
|
+
if (
|
|
275
|
+
!instructions?.length &&
|
|
276
|
+
!imageCaptionAction &&
|
|
277
|
+
!translateAction &&
|
|
278
|
+
!imageGenAction &&
|
|
279
|
+
!customActions.length
|
|
280
|
+
) {
|
|
241
281
|
return emptyAction
|
|
242
282
|
}
|
|
243
283
|
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DocumentFieldActionDivider,
|
|
3
|
+
DocumentFieldActionGroup,
|
|
4
|
+
DocumentFieldActionItem,
|
|
5
|
+
DocumentFieldActionNode,
|
|
6
|
+
ObjectSchemaType,
|
|
7
|
+
SanityDocumentLike,
|
|
8
|
+
useWorkspaceSchemaId,
|
|
9
|
+
} from 'sanity'
|
|
10
|
+
import {Path, SchemaType} from '@sanity/types'
|
|
11
|
+
import {useMemo} from 'react'
|
|
12
|
+
import {useAiAssistanceConfig} from '../assistLayout/AiAssistanceConfigContext'
|
|
13
|
+
import {ToastParams, useToast} from '@sanity/ui'
|
|
14
|
+
import {AgentActionPath} from '@sanity/client/stega'
|
|
15
|
+
import {useAssistDocumentContext} from '../assistDocument/AssistDocumentContext'
|
|
16
|
+
import {
|
|
17
|
+
documentRootKey,
|
|
18
|
+
fieldPresenceTypeName,
|
|
19
|
+
InstructionTask,
|
|
20
|
+
instructionTaskTypeName,
|
|
21
|
+
} from '../types'
|
|
22
|
+
import {randomKey} from '../_lib/randomKey'
|
|
23
|
+
|
|
24
|
+
export interface AgentActionConditionalPath {
|
|
25
|
+
path: AgentActionPath
|
|
26
|
+
readOnly: boolean
|
|
27
|
+
hidden: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AssistFieldActionProps {
|
|
31
|
+
/**
|
|
32
|
+
* `actionType` will be `document` for action invoked from the top right document action menu, and
|
|
33
|
+
* `field` when invoked from a field action menu.
|
|
34
|
+
*/
|
|
35
|
+
actionType: 'document' | 'field'
|
|
36
|
+
/**
|
|
37
|
+
* This is the id of the current document pane; it contains `drafts.`or `versions. prefix` ect depending on context.
|
|
38
|
+
* Use this for `documentId` when calling any `client.agent.action`.
|
|
39
|
+
*
|
|
40
|
+
* It is generally recommended to call actions from the studio like this:
|
|
41
|
+
* ```ts
|
|
42
|
+
* await client.agent.action.generate({
|
|
43
|
+
* targetDocument: {
|
|
44
|
+
* operation: 'createIfNotExists',
|
|
45
|
+
* _id: props.documentIdForAction,
|
|
46
|
+
* _type: props.documentSchemaType.name,
|
|
47
|
+
* initialValues: props.getDocumentValue()
|
|
48
|
+
* },
|
|
49
|
+
* //...
|
|
50
|
+
* })
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
documentIdForAction: string
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Schema type of the current document.
|
|
57
|
+
* @see documentIdForAction
|
|
58
|
+
*/
|
|
59
|
+
documentSchemaType: ObjectSchemaType
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns the current document value.
|
|
63
|
+
*
|
|
64
|
+
* Prefer passing this function to your hooks instead of passing the document value directly to avoid unnecessary re-renders.
|
|
65
|
+
* @see documentIdForAction
|
|
66
|
+
*/
|
|
67
|
+
getDocumentValue: () => SanityDocumentLike
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Returns the current readOnly and hidden state of all conditional members in the current document form.
|
|
71
|
+
*
|
|
72
|
+
* Intended to be passed to agent actions `conditionalPaths.paths`.
|
|
73
|
+
*/
|
|
74
|
+
getConditionalPaths: () => AgentActionConditionalPath[]
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* `schemaId` for the current workspace.
|
|
78
|
+
*
|
|
79
|
+
* Note: the workspace schema has to be deployed using `sanity schema deploy` or `sanity deploy`.
|
|
80
|
+
*
|
|
81
|
+
* Use this for `schemaId` when calling any `client.agent.action`.
|
|
82
|
+
*
|
|
83
|
+
* It is generally recommended to call actions from the studio like this:
|
|
84
|
+
* ```ts
|
|
85
|
+
* await client.agent.action.generate({
|
|
86
|
+
* targetDocument: {
|
|
87
|
+
* operation: 'createIfNotExists',
|
|
88
|
+
* _id: props.documentIdForAction,
|
|
89
|
+
* _type: props.documentSchemaType.name,
|
|
90
|
+
* initialValues: props.getDocumentValue()
|
|
91
|
+
* },
|
|
92
|
+
* //...
|
|
93
|
+
* })
|
|
94
|
+
*/
|
|
95
|
+
schemaId: string
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* This is the schema type of the field the actions will be attached to (ie, schemaType for `path`)
|
|
99
|
+
*
|
|
100
|
+
* It can be used with agent actions using `target.path`, to scope the action to a specific field.
|
|
101
|
+
*
|
|
102
|
+
* It is generally recommended to call actions from the studio like this:
|
|
103
|
+
* ```ts
|
|
104
|
+
* await client.agent.action.generate({
|
|
105
|
+
* targetDocument: {
|
|
106
|
+
* operation: 'createIfNotExists',
|
|
107
|
+
* _id: props.documentIdForAction,
|
|
108
|
+
* _type: props.documentSchemaType.name,
|
|
109
|
+
* initialValues: props.getDocumentValue()
|
|
110
|
+
* },
|
|
111
|
+
* target: {
|
|
112
|
+
* path: props.path
|
|
113
|
+
* },
|
|
114
|
+
* })
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
path: AgentActionPath
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* This is the schema type of the field the actions will be attached to (ie, schemaType for `path`).
|
|
121
|
+
*
|
|
122
|
+
* Typically useful to dynamically return different actions based on the schema type of the field.
|
|
123
|
+
*
|
|
124
|
+
* ```ts
|
|
125
|
+
* if(isObjectSchemaType(schemaType)) {
|
|
126
|
+
* return [
|
|
127
|
+
* defineAssistFieldAction({
|
|
128
|
+
* title: 'Fill the object fields',
|
|
129
|
+
* icon: RobotIcon,
|
|
130
|
+
* onAction: () => {
|
|
131
|
+
* //...
|
|
132
|
+
* }
|
|
133
|
+
* })
|
|
134
|
+
* ]
|
|
135
|
+
* }
|
|
136
|
+
* return useMemo(() => {
|
|
137
|
+
*
|
|
138
|
+
*
|
|
139
|
+
* }, [])
|
|
140
|
+
*
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
schemaType: SchemaType
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export type AssistFieldActionNode =
|
|
147
|
+
| AssistFieldActionItem
|
|
148
|
+
| AssistFieldActionGroup
|
|
149
|
+
| DocumentFieldActionDivider
|
|
150
|
+
|
|
151
|
+
export type AssistFieldActionItem = Omit<
|
|
152
|
+
DocumentFieldActionItem,
|
|
153
|
+
'renderAsButton' | 'selected' | 'onAction'
|
|
154
|
+
> & {
|
|
155
|
+
onAction: () => void | Promise<void>
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type AssistFieldActionGroup = Omit<
|
|
159
|
+
DocumentFieldActionGroup,
|
|
160
|
+
'renderAsButton' | 'expanded' | 'children'
|
|
161
|
+
> & {
|
|
162
|
+
children: AssistFieldActionNode[]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
type PushToast = (params: ToastParams) => string
|
|
166
|
+
|
|
167
|
+
export function defineAssistFieldAction(
|
|
168
|
+
action: Omit<AssistFieldActionItem, 'type'>,
|
|
169
|
+
): AssistFieldActionItem {
|
|
170
|
+
return {
|
|
171
|
+
...action,
|
|
172
|
+
type: 'action',
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function defineFieldActionDivider(): DocumentFieldActionDivider {
|
|
177
|
+
return {
|
|
178
|
+
type: 'divider',
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function defineAssistFieldActionGroup(
|
|
183
|
+
group: Omit<AssistFieldActionGroup, 'type'>,
|
|
184
|
+
): AssistFieldActionGroup {
|
|
185
|
+
return {
|
|
186
|
+
...group,
|
|
187
|
+
type: 'group',
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function useCustomFieldActions(
|
|
192
|
+
props: Omit<AssistFieldActionProps, 'schemaId' | 'path'> & {path: Path},
|
|
193
|
+
) {
|
|
194
|
+
const {
|
|
195
|
+
config: {fieldActions},
|
|
196
|
+
} = useAiAssistanceConfig()
|
|
197
|
+
const {addSyntheticTask, removeSyntheticTask} = useAssistDocumentContext()
|
|
198
|
+
|
|
199
|
+
const schemaId = useWorkspaceSchemaId()
|
|
200
|
+
const {push: pushToast} = useToast()
|
|
201
|
+
const configActions = fieldActions?.useFieldActions?.({
|
|
202
|
+
...props,
|
|
203
|
+
schemaId,
|
|
204
|
+
path: props.path as AgentActionPath,
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
return useMemo(() => {
|
|
208
|
+
const title = fieldActions?.title
|
|
209
|
+
const customActions = configActions?.map((node) => {
|
|
210
|
+
return createSafeNode({
|
|
211
|
+
node,
|
|
212
|
+
pushToast,
|
|
213
|
+
addSyntheticTask,
|
|
214
|
+
removeSyntheticTask,
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
const onlyGroups =
|
|
218
|
+
customActions?.length && customActions?.every((node) => node.type === 'group')
|
|
219
|
+
const groups = customActions?.length
|
|
220
|
+
? onlyGroups
|
|
221
|
+
? customActions
|
|
222
|
+
: [
|
|
223
|
+
{
|
|
224
|
+
type: 'group',
|
|
225
|
+
title: title || 'Custom actions',
|
|
226
|
+
children: customActions,
|
|
227
|
+
expanded: true,
|
|
228
|
+
} satisfies DocumentFieldActionGroup,
|
|
229
|
+
]
|
|
230
|
+
: []
|
|
231
|
+
return groups ?? []
|
|
232
|
+
}, [configActions, fieldActions, pushToast])
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function createSafeNode(args: {
|
|
236
|
+
node: AssistFieldActionNode
|
|
237
|
+
pushToast: PushToast
|
|
238
|
+
addSyntheticTask: (task: InstructionTask) => void
|
|
239
|
+
removeSyntheticTask: (task: InstructionTask) => void
|
|
240
|
+
}): DocumentFieldActionNode {
|
|
241
|
+
const {node} = args
|
|
242
|
+
switch (node.type) {
|
|
243
|
+
case 'action':
|
|
244
|
+
return createSafeAction({...args, action: node})
|
|
245
|
+
case 'group':
|
|
246
|
+
return {
|
|
247
|
+
...node,
|
|
248
|
+
renderAsButton: false,
|
|
249
|
+
expanded: true,
|
|
250
|
+
children: node.children?.map((child) => createSafeNode({...args, node: child})),
|
|
251
|
+
}
|
|
252
|
+
case 'divider':
|
|
253
|
+
default:
|
|
254
|
+
return node
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function createSafeAction(args: {
|
|
259
|
+
action: AssistFieldActionItem
|
|
260
|
+
pushToast: PushToast
|
|
261
|
+
addSyntheticTask: (task: InstructionTask) => void
|
|
262
|
+
removeSyntheticTask: (task: InstructionTask) => void
|
|
263
|
+
}) {
|
|
264
|
+
const {action, pushToast, addSyntheticTask, removeSyntheticTask} = args
|
|
265
|
+
return {
|
|
266
|
+
...action,
|
|
267
|
+
onAction: () => {
|
|
268
|
+
async function runAction() {
|
|
269
|
+
const task: InstructionTask = {
|
|
270
|
+
_type: instructionTaskTypeName,
|
|
271
|
+
_key: randomKey(12),
|
|
272
|
+
started: new Date().toISOString(),
|
|
273
|
+
presence: [
|
|
274
|
+
{
|
|
275
|
+
_type: fieldPresenceTypeName,
|
|
276
|
+
_key: randomKey(12),
|
|
277
|
+
path: documentRootKey,
|
|
278
|
+
started: new Date().toISOString(),
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
addSyntheticTask(task)
|
|
284
|
+
const actionResult = action.onAction?.()
|
|
285
|
+
if (actionResult instanceof Promise) {
|
|
286
|
+
await actionResult
|
|
287
|
+
}
|
|
288
|
+
} catch (err: any) {
|
|
289
|
+
console.error('Failed to execute action', action, err)
|
|
290
|
+
pushToast({
|
|
291
|
+
title: 'Failed to execute action',
|
|
292
|
+
description: err?.message,
|
|
293
|
+
status: 'error',
|
|
294
|
+
})
|
|
295
|
+
} finally {
|
|
296
|
+
removeSyntheticTask(task)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
runAction()
|
|
300
|
+
},
|
|
301
|
+
renderAsButton: false,
|
|
302
|
+
selected: false,
|
|
303
|
+
}
|
|
304
|
+
}
|