@sanity/assist 4.2.0 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,6 +34,10 @@
34
34
  - [Configure](#configure-field-translations)
35
35
  - [Adding translation actions to fields](#adding-translation-actions-to-fields)
36
36
  - [Translation style guide](#translation-style-guide)
37
+ - [Custom field actions](#custom-field-actions)
38
+ - [useFieldActions](#usefieldaction)
39
+ - [Define helpers](#define-helpers)
40
+ - [useUserInput](#useuserinput)
37
41
  - [License](#license)
38
42
  - [Develop \& test](#develop--test)
39
43
  - [Release new version](#release-new-version)
@@ -940,6 +944,301 @@ assist({
940
944
  })
941
945
  ```
942
946
 
947
+ ## Custom field actions
948
+
949
+ <img width="513" alt="Field action menu with custom actions" src="https://github.com/user-attachments/assets/c613f692-4983-4acc-a8c2-8fb60294682a" />
950
+
951
+ To incorporate [Agent Actions](https://www.sanity.io/docs/agent-actions?utm_source=github.com&utm_medium=organic_social&utm_campaign=ai-assist&utm_content=)
952
+ or other custom actions into the AI Assist document and field action menus, use `fieldActions` plugin config:
953
+
954
+ ```ts
955
+ assist({
956
+ fieldActions: {
957
+ title: 'Custom actions',
958
+ useFieldActions: (props: AssistFieldActionProps) => {
959
+ return useMemo(() => [
960
+ defineAssistFieldAction({
961
+ title: 'Do something',
962
+ icon: ActionIcon,
963
+ onAction: async () => {
964
+ // perform an (async) action
965
+ // errors will be caught and displayed in a toast
966
+ // until the action completes or fails, AI Assist "presence" will show up on the top of the document
967
+ },
968
+ })], [])
969
+ }
970
+ }
971
+ })
972
+ ```
973
+
974
+ ### `useFieldActions`
975
+
976
+ `useFieldActions` is called for the document itself and for all fields within it. It can call React hooks.
977
+ Actions returned by the hook will be added to the corresponding document or field menu.
978
+
979
+ It is recommended to wrap the returned actions in `useMemo`.
980
+
981
+ See TSDocs for [AssistFieldActionProps](./src/fieldActions/customFieldActions.tsx) for details on how each
982
+ prop can be used to parameterize Agent Actions on sanity client.
983
+
984
+ #### Agent Action examples
985
+
986
+ ##### Fix spelling
987
+
988
+ The following example adds a "Fix spelling" action to all fields and the document itself.
989
+
990
+ It will fix spelling mistakes for the field it is invoked for (and all child fields, for arrays and objects),
991
+ by calling `client.agent.action.transform`.
992
+
993
+ ```ts
994
+ assist({
995
+ fieldActions: {
996
+ title: 'Custom actions',
997
+ useFieldActions: (props) => {
998
+ const {
999
+ documentSchemaType,
1000
+ schemaId,
1001
+ getDocumentValue,
1002
+ getConditionalPaths,
1003
+ documentIdForAction,
1004
+ path,
1005
+ } = props
1006
+ const client = useClient({apiVersion: 'vX'})
1007
+ return useMemo(() => {
1008
+ return [
1009
+ defineAssistFieldAction({
1010
+ title: 'Fix spelling',
1011
+ icon: TranslateIcon,
1012
+ onAction: async () => {
1013
+ await client.agent.action.transform({
1014
+ schemaId,
1015
+ documentId: documentIdForAction,
1016
+ instruction: 'Fix any spelling mistakes',
1017
+ instructionParams: {field: {type: 'field', path}},
1018
+ // no need to send path for document actions
1019
+ target: path.length ? {path} : undefined,
1020
+ conditionalPaths: {paths: getConditionalPaths()},
1021
+ })
1022
+ },
1023
+ }),
1024
+ ]
1025
+ }, [
1026
+ client,
1027
+ documentSchemaType,
1028
+ schemaId,
1029
+ getDocumentValue,
1030
+ getConditionalPaths,
1031
+ documentIdForAction,
1032
+ path,
1033
+ ])
1034
+ },
1035
+ },
1036
+ })
1037
+ ```
1038
+
1039
+ ##### Fill field (contextually aware)
1040
+
1041
+ The following example adds a "Fill field" action to all fields in the document by calling `client.agent.action.generate`.
1042
+
1043
+ The action will:
1044
+ - create the document as a draft if it does not exist, respecting initial values (`targetDocument`)
1045
+ - use existing document state to determine what should be put in the the field (`instruction`, `instructionParams`)
1046
+ - pass the current readOnly and hidden state currently use by the document form to the Agent Action, so it respects it (`conditionalPaths`)
1047
+ - output to the field the action started from (`target.path`)
1048
+
1049
+ ```ts
1050
+ assist({
1051
+ fieldActions: {
1052
+ title: 'Custom actions',
1053
+ useFieldActions: (props) => {
1054
+ const {
1055
+ documentSchemaType,
1056
+ actionType,
1057
+ schemaId,
1058
+ getDocumentValue,
1059
+ getConditionalPaths,
1060
+ documentIdForAction,
1061
+ path,
1062
+ schemaType,
1063
+ } = props
1064
+
1065
+ // hook usage has to happen outside onAction, so preassemble state in useFieldActions and pass to useMemo
1066
+ const client = useClient({apiVersion: 'vX'})
1067
+
1068
+ return useMemo(() => {
1069
+ if (actionType === 'document') {
1070
+ // in this case we dont want a document action
1071
+ return []
1072
+ }
1073
+
1074
+ return [
1075
+ defineAssistFieldAction({
1076
+ title: 'Fill field',
1077
+ icon: EditIcon,
1078
+ onAction: async () => {
1079
+ await client.agent.action.generate({
1080
+ schemaId,
1081
+ targetDocument: {
1082
+ operation: 'createIfNotExists',
1083
+ _id: documentIdForAction,
1084
+ _type: documentSchemaType.name,
1085
+ initialValues: getDocumentValue(),
1086
+ },
1087
+ instruction: `
1088
+ We are generating a new value for a document field.
1089
+ The document type is ${documentSchemaType.name}, and the document type title is ${documentSchemaType.title}
1090
+ The document language is: "$lang" (use en-US if unspecified)
1091
+ The document value is:
1092
+ $doc
1093
+ ---
1094
+ We are in the following field:
1095
+ JSON-path: ${pathToString(path)}
1096
+ Title: ${schemaType.title}
1097
+ Value: $field (consider it empty if undefined)
1098
+ ---
1099
+ Generate a new field value. The new value should be relevant to the document type and context.
1100
+ Keep it interesting. Generate using the document language.
1101
+ `,
1102
+ instructionParams: {
1103
+ doc: {type: 'document'},
1104
+ field: {type: 'field', path},
1105
+ lang: {type: 'field', path: ['language']},
1106
+ },
1107
+ target: {
1108
+ path,
1109
+ },
1110
+ conditionalPaths: {
1111
+ paths: getConditionalPaths(),
1112
+ },
1113
+ })
1114
+ },
1115
+ }),
1116
+ ]
1117
+ }, [
1118
+ client,
1119
+ documentSchemaType,
1120
+ schemaId,
1121
+ getDocumentValue,
1122
+ getConditionalPaths,
1123
+ documentIdForAction,
1124
+ actionType,
1125
+ path,
1126
+ schemaType,
1127
+ ])
1128
+ },
1129
+ },
1130
+ })
1131
+ ```
1132
+
1133
+ ### Define helpers
1134
+
1135
+ #### `defineAssistFieldAction`
1136
+
1137
+ Adds a single action that will appear in the document/field action menu.
1138
+
1139
+ `onAction` _cannot_ call hooks. If state from hook is needed, it should be pre-assembled by `useFieldActions`
1140
+
1141
+ ```ts
1142
+ defineAssistFieldAction({
1143
+ title: 'Do something',
1144
+ icon: ActionIcon,
1145
+ onAction: async () => {
1146
+ //perform actions
1147
+ },
1148
+ })
1149
+ ```
1150
+
1151
+ #### `defineAssistFieldActionGroup`
1152
+
1153
+ Adds a group to hold one or more actions (or nested groups).
1154
+
1155
+ By default, any actions returned by `useFieldActions` will be grouped under `title`.
1156
+ ```ts
1157
+ useFieldActions: (props) => {
1158
+ return [
1159
+ defineAssistFieldAction({/* ... */}),
1160
+ defineAssistFieldActionGroup({
1161
+ title: 'More actions',
1162
+ children: [
1163
+ defineAssistFieldAction({/* ... */}),
1164
+ ],
1165
+ })
1166
+ ]
1167
+ }
1168
+ ```
1169
+
1170
+ #### Only groups in `useFieldActions`
1171
+ If `useFieldActions` _only_ returns groups, the default wrapper group will be omitted. This allows full control over each group title.
1172
+
1173
+ #### `defineFieldActionDivider`
1174
+ Adds a divider between actions or groups. Takes no arguments:
1175
+
1176
+ ```ts
1177
+
1178
+ useFieldActions: (props) => {
1179
+ return useMemo(() => [
1180
+ defineAssistFieldAction({/* ... */}),
1181
+ defineFieldActionDivider(),
1182
+ defineAssistFieldAction({/* ... */}),
1183
+ ], [])
1184
+ }
1185
+ ```
1186
+
1187
+ ### `useUserInput`
1188
+
1189
+ <img width="522" alt="user input dialog" src="https://github.com/user-attachments/assets/86966468-9a28-4c0b-99f3-e4b80fdbe691" />
1190
+
1191
+ For certain actions, it is useful to have the user provide additional information or details that can be used
1192
+ as parameters for the action.
1193
+
1194
+ `useUserInput` returns a `getUserInput` function that can be called and awaited to return input from the user.
1195
+
1196
+ The `getUserInput` function takes input configuration and will display an input dialog to the user.
1197
+ When the user completes the dialog, the user inputted text will be available (or undefined if the user closed the dialog).
1198
+
1199
+
1200
+ ```ts
1201
+ ({
1202
+ fieldActions: {
1203
+ title: 'Custom actions',
1204
+ useFieldActions: (props) => {
1205
+ const getUserInput = useUserInput()
1206
+
1207
+ return useMemo(
1208
+ () => [
1209
+ defineAssistFieldAction({
1210
+ title: 'Do something with user input',
1211
+ onAction: async () => {
1212
+ const inputResult = await getUserInput({
1213
+ title: 'What do you want to do?', // dialog title
1214
+ inputs: [
1215
+ {
1216
+ id: 'topic',
1217
+ title: 'Topic',
1218
+ },
1219
+ {
1220
+ id: 'facts',
1221
+ title: 'Facts',
1222
+ description: 'Provide additional facts that will be used by the action',
1223
+ },
1224
+ ],
1225
+ })
1226
+ if (!inputResult) {
1227
+ return // user closed the dialog
1228
+ }
1229
+
1230
+ //use the result from each input
1231
+ //const [{result: topic}, {result: facts}] = inputResult
1232
+ },
1233
+ }),
1234
+ ],
1235
+ [getUserInput],
1236
+ )
1237
+ },
1238
+ },
1239
+ })
1240
+ ```
1241
+
943
1242
  ## Caveats
944
1243
 
945
1244
  Large Language Models (LLMs) are a new technology. Constraints and limitations are still being explored,
package/dist/index.d.mts CHANGED
@@ -1,4 +1,8 @@
1
+ import {AgentActionPath} from '@sanity/client/stega'
1
2
  import {CurrentUser} from 'sanity'
3
+ import {DocumentFieldActionDivider} from 'sanity'
4
+ import {DocumentFieldActionGroup} from 'sanity'
5
+ import {DocumentFieldActionItem} from 'sanity'
2
6
  import {JSX as JSX_2} from 'react/jsx-runtime'
3
7
  import {ObjectSchemaType} from 'sanity'
4
8
  import {Path} from 'sanity'
@@ -8,7 +12,15 @@ import {PortableTextMarkDefinition} from '@portabletext/types'
8
12
  import {PortableTextSpan} from '@portabletext/types'
9
13
  import {SanityClient} from 'sanity'
10
14
  import type {SanityClient as SanityClient_2} from '@sanity/client'
15
+ import {SanityDocumentLike} from 'sanity'
11
16
  import {SchemaType} from 'sanity'
17
+ import {SchemaType as SchemaType_2} from '@sanity/types'
18
+
19
+ declare interface AgentActionConditionalPath {
20
+ path: AgentActionPath
21
+ readOnly: boolean
22
+ hidden: boolean
23
+ }
12
24
 
13
25
  export declare const assist: Plugin_2<void | AssistPluginConfig>
14
26
 
@@ -62,6 +74,135 @@ export declare interface AssistConfig {
62
74
  temperature?: number
63
75
  }
64
76
 
77
+ export declare type AssistFieldActionGroup = Omit<
78
+ DocumentFieldActionGroup,
79
+ 'renderAsButton' | 'expanded' | 'children'
80
+ > & {
81
+ children: AssistFieldActionNode[]
82
+ }
83
+
84
+ export declare type AssistFieldActionItem = Omit<
85
+ DocumentFieldActionItem,
86
+ 'renderAsButton' | 'selected' | 'onAction'
87
+ > & {
88
+ onAction: () => void | Promise<void>
89
+ }
90
+
91
+ export declare type AssistFieldActionNode =
92
+ | AssistFieldActionItem
93
+ | AssistFieldActionGroup
94
+ | DocumentFieldActionDivider
95
+
96
+ export declare interface AssistFieldActionProps {
97
+ /**
98
+ * `actionType` will be `document` for action invoked from the top right document action menu, and
99
+ * `field` when invoked from a field action menu.
100
+ */
101
+ actionType: 'document' | 'field'
102
+ /**
103
+ * This is the id of the current document pane; it contains `drafts.`or `versions. prefix` ect depending on context.
104
+ * Use this for `documentId` when calling any `client.agent.action`.
105
+ *
106
+ * It is generally recommended to call actions from the studio like this:
107
+ * ```ts
108
+ * await client.agent.action.generate({
109
+ * targetDocument: {
110
+ * operation: 'createIfNotExists',
111
+ * _id: props.documentIdForAction,
112
+ * _type: props.documentSchemaType.name,
113
+ * initialValues: props.getDocumentValue()
114
+ * },
115
+ * //...
116
+ * })
117
+ * ```
118
+ */
119
+ documentIdForAction: string
120
+ /**
121
+ * Schema type of the current document.
122
+ * @see documentIdForAction
123
+ */
124
+ documentSchemaType: ObjectSchemaType
125
+ /**
126
+ * Returns the current document value.
127
+ *
128
+ * Prefer passing this function to your hooks instead of passing the document value directly to avoid unnecessary re-renders.
129
+ * @see documentIdForAction
130
+ */
131
+ getDocumentValue: () => SanityDocumentLike
132
+ /**
133
+ * Returns the current readOnly and hidden state of all conditional members in the current document form.
134
+ *
135
+ * Intended to be passed to agent actions `conditionalPaths.paths`.
136
+ */
137
+ getConditionalPaths: () => AgentActionConditionalPath[]
138
+ /**
139
+ * `schemaId` for the current workspace.
140
+ *
141
+ * Note: the workspace schema has to be deployed using `sanity schema deploy` or `sanity deploy`.
142
+ *
143
+ * Use this for `schemaId` when calling any `client.agent.action`.
144
+ *
145
+ * It is generally recommended to call actions from the studio like this:
146
+ * ```ts
147
+ * await client.agent.action.generate({
148
+ * targetDocument: {
149
+ * operation: 'createIfNotExists',
150
+ * _id: props.documentIdForAction,
151
+ * _type: props.documentSchemaType.name,
152
+ * initialValues: props.getDocumentValue()
153
+ * },
154
+ * //...
155
+ * })
156
+ */
157
+ schemaId: string
158
+ /**
159
+ * This is the schema type of the field the actions will be attached to (ie, schemaType for `path`)
160
+ *
161
+ * It can be used with agent actions using `target.path`, to scope the action to a specific field.
162
+ *
163
+ * It is generally recommended to call actions from the studio like this:
164
+ * ```ts
165
+ * await client.agent.action.generate({
166
+ * targetDocument: {
167
+ * operation: 'createIfNotExists',
168
+ * _id: props.documentIdForAction,
169
+ * _type: props.documentSchemaType.name,
170
+ * initialValues: props.getDocumentValue()
171
+ * },
172
+ * target: {
173
+ * path: props.path
174
+ * },
175
+ * })
176
+ * ```
177
+ */
178
+ path: AgentActionPath
179
+ /**
180
+ * This is the schema type of the field the actions will be attached to (ie, schemaType for `path`).
181
+ *
182
+ * Typically useful to dynamically return different actions based on the schema type of the field.
183
+ *
184
+ * ```ts
185
+ * if(isObjectSchemaType(schemaType)) {
186
+ * return [
187
+ * defineAssistFieldAction({
188
+ * title: 'Fill the object fields',
189
+ * icon: RobotIcon,
190
+ * onAction: () => {
191
+ * //...
192
+ * }
193
+ * })
194
+ * ]
195
+ * }
196
+ * return useMemo(() => {
197
+ *
198
+ *
199
+ * }, [])
200
+ *
201
+ * ```
202
+ */
203
+ schemaType: SchemaType_2
204
+ }
205
+
65
206
  export declare interface AssistOptions {
66
207
  aiAssist?: {
67
208
  /** Set to true to disable assistance for this field or type */
@@ -80,6 +221,10 @@ declare interface AssistPluginConfig {
80
221
  * Config that affects all instructions
81
222
  */
82
223
  assist?: AssistConfig
224
+ fieldActions?: {
225
+ title?: string
226
+ useFieldActions?: (props: AssistFieldActionProps) => AssistFieldActionNode[]
227
+ }
83
228
  /**
84
229
  * @internal
85
230
  */
@@ -104,6 +249,35 @@ declare interface ContextBlock {
104
249
 
105
250
  export declare const contextDocumentTypeName: 'assist.instruction.context'
106
251
 
252
+ /**
253
+ *
254
+ */
255
+ export declare interface CustomInput {
256
+ /**
257
+ * Id for the input
258
+ */
259
+ id: string
260
+ /**
261
+ * Title of the input field
262
+ */
263
+ title: string
264
+ /**
265
+ * Additional info that will be displayed over the input
266
+ */
267
+ description?: string
268
+ }
269
+
270
+ export declare type CustomInputResult = {
271
+ /**
272
+ * Identifies which custom input the `result`belongs to
273
+ */
274
+ input: CustomInput
275
+ /**
276
+ * The text provided by the user in the input
277
+ */
278
+ result: string
279
+ }
280
+
107
281
  /**
108
282
  * Default implementation for plugin config `translate.field.translationOutputs`
109
283
  *
@@ -111,6 +285,16 @@ export declare const contextDocumentTypeName: 'assist.instruction.context'
111
285
  */
112
286
  export declare const defaultLanguageOutputs: TranslationOutputsFunction
113
287
 
288
+ export declare function defineAssistFieldAction(
289
+ action: Omit<AssistFieldActionItem, 'type'>,
290
+ ): AssistFieldActionItem
291
+
292
+ export declare function defineAssistFieldActionGroup(
293
+ group: Omit<AssistFieldActionGroup, 'type'>,
294
+ ): AssistFieldActionGroup
295
+
296
+ export declare function defineFieldActionDivider(): DocumentFieldActionDivider
297
+
114
298
  export declare interface DocumentMember {
115
299
  schemaType: SchemaType
116
300
  path: Path
@@ -252,10 +436,29 @@ export declare interface FieldTranslationConfig {
252
436
  maxPathDepth?: number
253
437
  }
254
438
 
439
+ export declare type GetUserInput = (args: {
440
+ /**
441
+ * Dialog title
442
+ */
443
+ title: string
444
+ /**
445
+ * One titled input per array item
446
+ */
447
+ inputs: CustomInput[]
448
+ }) => Promise<CustomInputResult[] | undefined>
449
+
255
450
  declare type InlinePromptBlock = PortableTextSpan | FieldRef | UserInputBlock | ContextBlock
256
451
 
257
452
  declare const instructionContextTypeName: 'sanity.assist.instruction.context'
258
453
 
454
+ /**
455
+ * Returns true if the `schemaType` or any of its parent types (`schemaType.type`)` has `name` equal
456
+ * to `typeName`.
457
+ *
458
+ * Useful for checking if `schemaType` is a type alias of `ìmage`, `code` or similar.
459
+ */
460
+ export declare function isType(schemaType: SchemaType, typeName: string): boolean
461
+
259
462
  export declare interface Language {
260
463
  id: string
261
464
  title?: string
@@ -395,6 +598,68 @@ declare interface UserInputBlock {
395
598
 
396
599
  declare const userInputTypeName: 'sanity.assist.instruction.userInput'
397
600
 
601
+ /**
602
+ * `useUserInput` returns a function that can be used to await user input.
603
+ *
604
+ * Useful for custom `fieldActions` to get user input for populating Agent Action requests,.
605
+ *
606
+ * ```ts
607
+ * fieldActions: {
608
+ * useFieldActions: (props) => {
609
+ * const {
610
+ * documentSchemaType,
611
+ * schemaId,
612
+ * getDocumentValue,
613
+ * getConditionalPaths,
614
+ * documentIdForAction,
615
+ * } = props
616
+ * const client = useClient({apiVersion: 'vX'})
617
+ * const getUserInput = useUserInput()
618
+ * return useMemo(() => {
619
+ * return [
620
+ * defineAssistFieldAction({
621
+ * title: 'Log user input',
622
+ * icon: UserIcon,
623
+ * onAction: async () => {
624
+ * const input = await getUserInput({
625
+ * title: 'Topic',
626
+ * inputs: [{id: 'about', title: 'What should the article be about?'}],
627
+ * })
628
+ * if (!input) return // user canceled input
629
+ * await client.agent.action.generate({
630
+ * schemaId,
631
+ * targetDocument: {
632
+ * operation: 'createIfNotExists',
633
+ * _id: documentIdForAction,
634
+ * _type: documentSchemaType.name,
635
+ * initialValues: getDocumentValue(),
636
+ * },
637
+ * instruction: `
638
+ * Create a document about the following topic:
639
+ * $about
640
+ * ---
641
+ * `,
642
+ * instructionParams: {about: input[0].result},
643
+ * conditionalPaths: {paths: getConditionalPaths()},
644
+ * })
645
+ * },
646
+ * }),
647
+ * ]
648
+ * }, [
649
+ * client,
650
+ * documentSchemaType,
651
+ * schemaId,
652
+ * getDocumentValue,
653
+ * getConditionalPaths,
654
+ * documentIdForAction,
655
+ * getUserInput,
656
+ * ])
657
+ * },
658
+ * }
659
+ * ```
660
+ */
661
+ export declare function useUserInput(): GetUserInput
662
+
398
663
  export {}
399
664
 
400
665
  declare module 'sanity' {