@sanity/assist 4.1.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.
@@ -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
+ }
@@ -0,0 +1,107 @@
1
+ import {useRunInstruction} from '../assistLayout/RunInstructionProvider'
2
+
3
+ export type GetUserInput = (args: {
4
+ /**
5
+ * Dialog title
6
+ */
7
+ title: string
8
+ /**
9
+ * One titled input per array item
10
+ */
11
+ inputs: CustomInput[]
12
+ }) => Promise<CustomInputResult[] | undefined>
13
+
14
+ /**
15
+ *
16
+ */
17
+ export interface CustomInput {
18
+ /**
19
+ * Id for the input
20
+ */
21
+ id: string
22
+ /**
23
+ * Title of the input field
24
+ */
25
+ title: string
26
+ /**
27
+ * Additional info that will be displayed over the input
28
+ */
29
+ description?: string
30
+ }
31
+
32
+ export type CustomInputResult = {
33
+ /**
34
+ * Identifies which custom input the `result`belongs to
35
+ */
36
+ input: CustomInput
37
+
38
+ /**
39
+ * The text provided by the user in the input
40
+ */
41
+ result: string
42
+ }
43
+
44
+ /**
45
+ * `useUserInput` returns a function that can be used to await user input.
46
+ *
47
+ * Useful for custom `fieldActions` to get user input for populating Agent Action requests,.
48
+ *
49
+ * ```ts
50
+ * fieldActions: {
51
+ * useFieldActions: (props) => {
52
+ * const {
53
+ * documentSchemaType,
54
+ * schemaId,
55
+ * getDocumentValue,
56
+ * getConditionalPaths,
57
+ * documentIdForAction,
58
+ * } = props
59
+ * const client = useClient({apiVersion: 'vX'})
60
+ * const getUserInput = useUserInput()
61
+ * return useMemo(() => {
62
+ * return [
63
+ * defineAssistFieldAction({
64
+ * title: 'Log user input',
65
+ * icon: UserIcon,
66
+ * onAction: async () => {
67
+ * const input = await getUserInput({
68
+ * title: 'Topic',
69
+ * inputs: [{id: 'about', title: 'What should the article be about?'}],
70
+ * })
71
+ * if (!input) return // user canceled input
72
+ * await client.agent.action.generate({
73
+ * schemaId,
74
+ * targetDocument: {
75
+ * operation: 'createIfNotExists',
76
+ * _id: documentIdForAction,
77
+ * _type: documentSchemaType.name,
78
+ * initialValues: getDocumentValue(),
79
+ * },
80
+ * instruction: `
81
+ * Create a document about the following topic:
82
+ * $about
83
+ * ---
84
+ * `,
85
+ * instructionParams: {about: input[0].result},
86
+ * conditionalPaths: {paths: getConditionalPaths()},
87
+ * })
88
+ * },
89
+ * }),
90
+ * ]
91
+ * }, [
92
+ * client,
93
+ * documentSchemaType,
94
+ * schemaId,
95
+ * getDocumentValue,
96
+ * getConditionalPaths,
97
+ * documentIdForAction,
98
+ * getUserInput,
99
+ * ])
100
+ * },
101
+ * }
102
+ * ```
103
+ */
104
+ export function useUserInput(): GetUserInput {
105
+ const {getUserInput} = useRunInstruction()
106
+ return getUserInput
107
+ }
@@ -18,13 +18,23 @@ export function isImage(schemaType: SchemaType) {
18
18
  return isType(schemaType, 'image')
19
19
  }
20
20
 
21
- export function getDescriptionFieldOption(schemaType: SchemaType | undefined): string | undefined {
21
+ export function getDescriptionFieldOption(
22
+ schemaType: SchemaType | undefined,
23
+ ): {path: string; updateOnImageChange: boolean} | undefined {
22
24
  if (!schemaType) {
23
25
  return undefined
24
26
  }
25
27
  const descriptionField = (schemaType.options as ImageOptions)?.aiAssist?.imageDescriptionField
26
- if (descriptionField) {
27
- return descriptionField
28
+ if (typeof descriptionField === 'string') {
29
+ return {
30
+ path: descriptionField,
31
+ updateOnImageChange: true,
32
+ }
33
+ } else if (descriptionField) {
34
+ return {
35
+ path: descriptionField.path,
36
+ updateOnImageChange: descriptionField.updateOnImageChange ?? true,
37
+ }
28
38
  }
29
39
  return getDescriptionFieldOption(schemaType.type)
30
40
  }
package/src/index.ts CHANGED
@@ -5,3 +5,20 @@ export {defaultLanguageOutputs} from './translate/paths'
5
5
  export * from './translate/types'
6
6
  export {contextDocumentTypeName} from './types'
7
7
  export * from './assistTypes'
8
+
9
+ export {
10
+ type AssistFieldActionProps,
11
+ type AssistFieldActionGroup,
12
+ type AssistFieldActionItem,
13
+ type AssistFieldActionNode,
14
+ defineAssistFieldAction,
15
+ defineFieldActionDivider,
16
+ defineAssistFieldActionGroup,
17
+ } from './fieldActions/customFieldActions'
18
+
19
+ export {
20
+ type GetUserInput,
21
+ type CustomInput,
22
+ type CustomInputResult,
23
+ useUserInput,
24
+ } from './fieldActions/useUserInput'
package/src/plugin.tsx CHANGED
@@ -21,6 +21,7 @@ import {createAssistDocumentPresence} from './presence/AssistDocumentPresence'
21
21
  import {schemaTypes} from './schemas'
22
22
  import {TranslationConfig} from './translate/types'
23
23
  import {assistDocumentTypeName, AssistPreset} from './types'
24
+ import {AssistFieldActionNode, AssistFieldActionProps} from './fieldActions/customFieldActions'
24
25
 
25
26
  export interface AssistPluginConfig {
26
27
  translate?: TranslationConfig
@@ -30,6 +31,11 @@ export interface AssistPluginConfig {
30
31
  */
31
32
  assist?: AssistConfig
32
33
 
34
+ fieldActions?: {
35
+ title?: string
36
+ useFieldActions?: (props: AssistFieldActionProps) => AssistFieldActionNode[]
37
+ }
38
+
33
39
  /**
34
40
  * @internal
35
41
  */
@@ -13,9 +13,9 @@ export function createAssistDocumentPresence(documentId: string | undefined) {
13
13
  }
14
14
 
15
15
  function AssistDocumentPresence() {
16
- const {assistDocument} = useAssistDocumentContext()
16
+ const {assistDocument, syntheticTasks} = useAssistDocumentContext()
17
17
  const anyPresence = useMemo(() => {
18
- const anyPresence = assistDocument?.tasks
18
+ const anyPresence = [...(assistDocument?.tasks ?? []), ...(syntheticTasks ?? [])]
19
19
  ?.filter((run) => !run.ended && !run.reason)
20
20
  ?.flatMap((run) => run.presence ?? [])
21
21
  .find((f) => f.started && new Date().getTime() - new Date(f.started).getTime() < 30000)
@@ -36,7 +36,7 @@ function AssistDocumentPresence() {
36
36
  [],
37
37
  )
38
38
  : undefined
39
- }, [assistDocument?.tasks])
39
+ }, [assistDocument?.tasks, syntheticTasks])
40
40
 
41
41
  return (
42
42
  <Card>
@@ -90,7 +90,18 @@ declare module 'sanity' {
90
90
  * })
91
91
  * ```
92
92
  */
93
- imageDescriptionField?: string
93
+ imageDescriptionField?:
94
+ | string
95
+ | {
96
+ path: string
97
+ /**
98
+ * When updateOnImageChange is true (or undefined), whenever the
99
+ * image asset changes, imageDescriptionField will be regenerated.
100
+ *
101
+ * default: true
102
+ * */
103
+ updateOnImageChange?: boolean
104
+ }
94
105
  }
95
106
  }
96
107
  interface NumberOptions extends AssistOptions {}