@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/assist",
3
- "version": "4.1.0",
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.10.9",
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.1.0",
66
+ "@sanity/pkg-utils": "^6.13.4",
67
67
  "@sanity/plugin-kit": "^3.1.10",
68
- "@sanity/schema": "^3.81.0",
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.81.0",
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.0"
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": ">=14"
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[]}) | undefined
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
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
97
- const {instruction, userTexts, ...request} = runRequest
98
- runInstructionRequest({
99
- ...request,
100
- instructionKey: instruction._key,
101
- userTexts: Object.entries(inputs).map(([key, value]) => ({
102
- blockKey: key,
103
- userInput: value,
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={getInstructionTitle(runRequest?.instruction)}
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>>
@@ -40,7 +40,7 @@ export function ImageContextProvider(props: InputProps) {
40
40
  if (
41
41
  assetRef &&
42
42
  assistableDocumentId &&
43
- descriptionField &&
43
+ descriptionField?.updateOnImageChange &&
44
44
  assetRef !== assetRefState &&
45
45
  !isSyncing &&
46
46
  !isShowingOlderRevision &&
@@ -49,7 +49,7 @@ export function ImageContextProvider(props: InputProps) {
49
49
  setAssetRefState(assetRef)
50
50
  if (canUseAssist(status)) {
51
51
  generateCaption({
52
- path: pathToString([...path, descriptionField]),
52
+ path: pathToString([...path, descriptionField.path]),
53
53
  documentId: assistableDocumentId,
54
54
  })
55
55
  }
@@ -71,8 +71,8 @@ export function ImageContextProvider(props: InputProps) {
71
71
  const descriptionField = getDescriptionFieldOption(schemaType)
72
72
  const imageInstructionField = getImageInstructionFieldOption(schemaType)
73
73
  return {
74
- imageDescriptionPath: descriptionField
75
- ? pathToString([...path, descriptionField])
74
+ imageDescriptionPath: descriptionField?.path
75
+ ? pathToString([...path, descriptionField.path])
76
76
  : undefined,
77
77
  imageInstructionPath: imageInstructionField
78
78
  ? pathToString([...path, imageInstructionField])
@@ -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 instructionsLength = instructions?.length || 0
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 (instructionsLength === 0 && !imageCaptionAction && !translateAction && !imageGenAction) {
274
+ if (
275
+ !instructions?.length &&
276
+ !imageCaptionAction &&
277
+ !translateAction &&
278
+ !imageGenAction &&
279
+ !customActions.length
280
+ ) {
241
281
  return emptyAction
242
282
  }
243
283