@sanity/assist 1.2.16 → 2.0.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.
Files changed (52) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +551 -30
  3. package/dist/index.cjs.mjs +1 -0
  4. package/dist/index.d.ts +333 -9
  5. package/dist/index.esm.js +2463 -390
  6. package/dist/index.esm.js.map +1 -1
  7. package/dist/index.js +2457 -383
  8. package/dist/index.js.map +1 -1
  9. package/package.json +12 -11
  10. package/src/_lib/form/DocumentForm.tsx +2 -1
  11. package/src/_lib/form/constants.ts +1 -0
  12. package/src/assistDocument/AssistDocumentInput.tsx +24 -4
  13. package/src/assistDocument/RequestRunInstructionProvider.tsx +37 -21
  14. package/src/assistDocument/components/AssistDocumentForm.tsx +65 -21
  15. package/src/assistDocument/components/instruction/InstructionInput.tsx +5 -4
  16. package/src/assistDocument/components/instruction/InstructionOutputField.tsx +45 -0
  17. package/src/assistDocument/components/instruction/InstructionOutputInput.tsx +205 -0
  18. package/src/assistDocument/hooks/useStudioAssistDocument.ts +5 -32
  19. package/src/assistFormComponents/AssistField.tsx +11 -5
  20. package/src/assistFormComponents/AssistFormBlock.tsx +2 -3
  21. package/src/assistFormComponents/validation/listItem.tsx +2 -2
  22. package/src/assistInspector/AssistInspector.tsx +6 -0
  23. package/src/assistInspector/FieldAutocomplete.tsx +1 -0
  24. package/src/assistInspector/helpers.ts +9 -11
  25. package/src/assistLayout/AssistLayout.tsx +9 -9
  26. package/src/components/ImageContext.tsx +30 -13
  27. package/src/components/SafeValueInput.tsx +4 -1
  28. package/src/fieldActions/assistFieldActions.tsx +42 -13
  29. package/src/fieldActions/generateCaptionActions.tsx +17 -6
  30. package/src/fieldActions/generateImageActions.tsx +57 -0
  31. package/src/helpers/assistSupported.ts +10 -16
  32. package/src/helpers/conditionalMembers.test.ts +200 -0
  33. package/src/helpers/conditionalMembers.ts +127 -0
  34. package/src/helpers/misc.ts +8 -4
  35. package/src/helpers/typeUtils.ts +19 -5
  36. package/src/index.ts +3 -0
  37. package/src/plugin.tsx +18 -4
  38. package/src/schemas/assistDocumentSchema.tsx +40 -1
  39. package/src/schemas/serialize/serializeSchema.test.ts +239 -8
  40. package/src/schemas/serialize/serializeSchema.ts +77 -10
  41. package/src/schemas/typeDefExtensions.ts +89 -5
  42. package/src/translate/FieldTranslationProvider.tsx +360 -0
  43. package/src/translate/getLanguageParams.ts +26 -0
  44. package/src/translate/languageStore.ts +18 -0
  45. package/src/translate/paths.test.ts +133 -0
  46. package/src/translate/paths.ts +175 -0
  47. package/src/translate/translateActions.tsx +188 -0
  48. package/src/translate/types.ts +160 -0
  49. package/src/types.ts +67 -15
  50. package/src/useApiClient.ts +134 -2
  51. package/src/assistLayout/AlphaMigration.tsx +0 -310
  52. package/src/legacy-types.ts +0 -72
@@ -3,24 +3,14 @@ import {
3
3
  assistDocumentTypeName,
4
4
  AssistTasksStatus,
5
5
  assistTasksStatusTypeName,
6
- FieldRef,
7
- fieldReferenceTypeName,
8
6
  InstructionTask,
9
7
  StudioAssistDocument,
10
8
  StudioAssistField,
11
9
  StudioInstruction,
12
10
  } from '../../types'
13
- import {
14
- ObjectSchemaType,
15
- pathToString,
16
- typed,
17
- useClient,
18
- useCurrentUser,
19
- useValidationStatus,
20
- ValidationMarker,
21
- } from 'sanity'
11
+ import {ObjectSchemaType, typed, useClient, useCurrentUser} from 'sanity'
22
12
  import {useDocumentState} from './useDocumentState'
23
- import {assistDocumentId, assistTasksStatusId, publicId} from '../../helpers/ids'
13
+ import {assistDocumentId, assistTasksStatusId} from '../../helpers/ids'
24
14
  import {maxHistoryVisibilityMs} from '../../constants'
25
15
 
26
16
  interface UseAssistDocumentProps {
@@ -37,7 +27,6 @@ export function useStudioAssistDocument({
37
27
  const documentTypeName = schemaType.name
38
28
  const currentUser = useCurrentUser()
39
29
 
40
- const validation = useValidationStatus(publicId(documentId), schemaType.name).validation
41
30
  const assistDocument = useDocumentState<StudioAssistDocument>(
42
31
  assistDocumentId(documentTypeName),
43
32
  assistDocumentTypeName
@@ -73,7 +62,7 @@ export function useStudioAssistDocument({
73
62
  tasks: tasks.filter((task) => task.path === assistField.path),
74
63
  instructions: assistField.instructions
75
64
  ?.filter((p) => !p.userId || p.userId === currentUser?.id)
76
- .map((instruction) => asStudioInstruction(instruction, tasks, validation)),
65
+ .map((instruction) => asStudioInstruction(instruction, tasks)),
77
66
  }
78
67
  })
79
68
  return typed<StudioAssistDocument>({
@@ -89,23 +78,13 @@ export function useStudioAssistDocument({
89
78
  }),
90
79
  fields: fields,
91
80
  })
92
- }, [assistDocument, assistTasksStatus, currentUser, validation])
81
+ }, [assistDocument, assistTasksStatus, currentUser])
93
82
  }
94
83
 
95
84
  function asStudioInstruction(
96
85
  instruction: StudioInstruction,
97
- run: InstructionTask[],
98
- validation: ValidationMarker[]
86
+ run: InstructionTask[]
99
87
  ): StudioInstruction {
100
- const errors = validation.filter((marker) => marker.level === 'error')
101
-
102
- const fieldRefs: FieldRef[] = (instruction?.prompt ?? []).flatMap((block) => {
103
- if (block._type === 'block') {
104
- return block.children.filter((c): c is FieldRef => c._type === fieldReferenceTypeName)
105
- }
106
- return []
107
- })
108
-
109
88
  return {
110
89
  ...instruction,
111
90
  tasks: run
@@ -115,11 +94,5 @@ function asStudioInstruction(
115
94
  task.started &&
116
95
  new Date().getTime() - new Date(task.started).getTime() < maxHistoryVisibilityMs
117
96
  ),
118
- validation: errors.filter((marker) =>
119
- fieldRefs
120
- .map((r) => r.path)
121
- .filter((p): p is string => !!p)
122
- .find((path) => pathToString(marker.path) === path)
123
- ),
124
97
  }
125
98
  }
@@ -9,6 +9,7 @@ import {AiFieldPresence} from '../presence/AiFieldPresence'
9
9
  import {AssistOnboardingPopover} from '../onboarding/FieldActionsOnboarding'
10
10
  import {FirstAssistedPathContext} from '../onboarding/FirstAssistedPathProvider'
11
11
  import {fieldOnboardingKey, useOnboardingFeature} from '../onboarding/onboardingStore'
12
+ import {assistFormId} from '../_lib/form/constants'
12
13
 
13
14
  export function AssistFieldWrapper(props: FieldProps) {
14
15
  const {schemaType} = props
@@ -22,7 +23,11 @@ export function AssistFieldWrapper(props: FieldProps) {
22
23
  ) {
23
24
  return props.renderDefault(props)
24
25
  }
25
- if (!isType(props.schemaType, 'document') && props.inputId !== 'root') {
26
+ if (
27
+ !isType(props.schemaType, 'document') &&
28
+ props.inputId !== 'root' &&
29
+ props.inputId !== assistFormId
30
+ ) {
26
31
  return <AssistField {...props}>{props.children}</AssistField>
27
32
  }
28
33
 
@@ -46,14 +51,15 @@ export function AssistField(props: FieldProps) {
46
51
  )
47
52
 
48
53
  const {showOnboarding, dismissOnboarding} = useOnboardingFeature(fieldOnboardingKey)
54
+ const singlePresence = presence[0]
49
55
 
50
56
  const actions = (
51
57
  <Flex gap={2} align="center" justify="space-between">
52
- {presence.map((pre) => (
53
- <Box key={pre.user.id}>
54
- <AiFieldPresence key={pre.lastActiveAt} presence={pre} />
58
+ {singlePresence && (
59
+ <Box>
60
+ <AiFieldPresence presence={singlePresence} />
55
61
  </Box>
56
- ))}
62
+ )}
57
63
 
58
64
  {isFirstAssisted && showOnboarding && <AssistOnboardingPopover dismiss={dismissOnboarding} />}
59
65
  </Flex>
@@ -18,13 +18,12 @@ export function AssistFormBlock(props: BlockProps) {
18
18
  },
19
19
  [onChange, key]
20
20
  )
21
+ const singlePresence = presence[0]
21
22
  return (
22
23
  <ErrorWrapper onChange={localOnChange}>
23
24
  <Flex align="center" justify="space-between">
24
25
  <Box flex={1}>{props.renderDefault(props)}</Box>
25
- {presence.map((pre) => (
26
- <AiFieldPresence key={pre.lastActiveAt} presence={pre} />
27
- ))}
26
+ {singlePresence && <AiFieldPresence presence={singlePresence} />}
28
27
  </Flex>
29
28
  </ErrorWrapper>
30
29
  )
@@ -47,9 +47,9 @@ export function ListItem(props: ValidationListItemProps) {
47
47
  {path}
48
48
  </StyledText>
49
49
  )}
50
- {marker.item.message && (
50
+ {marker.item?.message && (
51
51
  <StyledText muted size={1} textOverflow={truncate ? 'ellipsis' : undefined}>
52
- {marker.item.message}
52
+ {marker.item?.message}
53
53
  </StyledText>
54
54
  )}
55
55
  </Stack>
@@ -31,6 +31,7 @@ import {InspectorOnboarding} from '../onboarding/InspectorOnboarding'
31
31
  import {inspectorOnboardingKey, useOnboardingFeature} from '../onboarding/onboardingStore'
32
32
  import {TypePathContext} from '../assistDocument/components/AssistDocumentForm'
33
33
  import {FieldTitle} from './FieldAutocomplete'
34
+ import {getConditionalMembers} from '../helpers/conditionalMembers'
34
35
 
35
36
  const CardWithShadowBelow = styled(Card)`
36
37
  position: relative;
@@ -199,9 +200,13 @@ export function AssistInspector(props: DocumentInspectorProps) {
199
200
  value: docValue,
200
201
  schemaType,
201
202
  onChange: documentOnChange,
203
+ formState,
202
204
  } = documentPane
203
205
  const {published, draft} = useEditState(documentId, documentType, 'low')
204
206
 
207
+ const formStateRef = useRef(formState)
208
+ formStateRef.current = formState
209
+
205
210
  const assistableDocId = getAssistableDocId(schemaType, documentId)
206
211
  const {instructionLoading, requestRunInstruction} = useRequestRunInstruction({
207
212
  documentOnChange,
@@ -261,6 +266,7 @@ export function AssistInspector(props: DocumentInspectorProps) {
261
266
  typePath,
262
267
  assistDocumentId: assistDocumentId(documentType),
263
268
  instruction,
269
+ conditionalMembers: formStateRef.current ? getConditionalMembers(formStateRef.current) : [],
264
270
  }),
265
271
  [pathKey, instruction, typePath, documentType, assistableDocId, requestRunInstruction]
266
272
  )
@@ -32,6 +32,7 @@ export function FieldAutocomplete(props: FieldSelectorProps) {
32
32
  () =>
33
33
  fieldRefs
34
34
  .filter((field) => (filter ? filter(field) : true))
35
+ .filter((f) => !isType(f.schemaType, 'reference'))
35
36
  .map((field) => ({value: field.key, field})),
36
37
  [fieldRefs, filter]
37
38
  )
@@ -20,7 +20,7 @@ import {
20
20
  } from 'sanity'
21
21
  import {ComponentType, useContext, useMemo} from 'react'
22
22
  import {AssistInspectorRouteParams, documentRootKey, fieldPathParam} from '../types'
23
- import {usePaneRouter} from 'sanity/desk'
23
+ import {type PaneRouterContextValue, usePaneRouter} from 'sanity/desk'
24
24
  import {isAssistSupported} from '../helpers/assistSupported'
25
25
  import {isPortableTextArray, isType} from '../helpers/typeUtils'
26
26
  import {SelectedFieldContext} from '../assistDocument/components/SelectedFieldContext'
@@ -96,7 +96,7 @@ export function getFieldRefs(
96
96
 
97
97
  const syntheticFields =
98
98
  field.type.jsonType === 'array' ? getSyntheticFields(field.type, fieldRef, depth + 1) : []
99
- if (!isAssistSupported(field.type, true)) {
99
+ if (!isAssistSupported(field.type)) {
100
100
  return [...fields, ...syntheticFields]
101
101
  }
102
102
  return [fieldRef, ...fields, ...syntheticFields]
@@ -125,7 +125,7 @@ function getSyntheticFields(schemaType: ArraySchemaType, parent?: FieldRef, dept
125
125
  const fields =
126
126
  itemType.jsonType === 'object' ? getFieldRefs(itemType, fieldRef, depth + 1) : []
127
127
 
128
- if (!isAssistSupported(itemType, true)) {
128
+ if (!isAssistSupported(itemType)) {
129
129
  return fields
130
130
  }
131
131
  return [fieldRef, ...fields]
@@ -198,18 +198,16 @@ export function getFieldTitle(field?: FieldRef) {
198
198
  return field?.title ?? schemaType?.title ?? schemaType?.name ?? 'Untitled'
199
199
  }
200
200
 
201
+ export type AiPaneRouter = Omit<PaneRouterContextValue, 'setParams' | 'params'> & {
202
+ params: AssistInspectorRouteParams
203
+ setParams: (p: Record<keyof AssistInspectorRouteParams, string | undefined>) => void
204
+ }
205
+
201
206
  export function useAiPaneRouter() {
202
207
  const paneRouter = usePaneRouter()
203
208
 
204
209
  return useMemo(
205
- () =>
206
- ({...paneRouter, params: paneRouter.params ?? {}} as Omit<
207
- typeof paneRouter,
208
- 'setParams' | 'params'
209
- > & {
210
- params: AssistInspectorRouteParams
211
- setParams: (p: Record<keyof AssistInspectorRouteParams, string | undefined>) => void
212
- }),
210
+ () => ({...paneRouter, params: paneRouter.params ?? {}} as AiPaneRouter),
213
211
  [paneRouter]
214
212
  )
215
213
  }
@@ -8,7 +8,7 @@ import {RunInstructionRequest} from '../useApiClient'
8
8
  import {StudioInstruction} from '../types'
9
9
  import {RunInstructionProvider} from './RunInstructionProvider'
10
10
  import {ThemeProvider} from '@sanity/ui'
11
- import {AlphaMigration} from './AlphaMigration'
11
+ import {FieldTranslationProvider} from '../translate/FieldTranslationProvider'
12
12
 
13
13
  export interface AIStudioLayoutProps extends LayoutProps {
14
14
  config: AssistPluginConfig
@@ -20,18 +20,18 @@ export type RunInstructionArgs = Omit<RunInstructionRequest, 'instructionKey' |
20
20
 
21
21
  export function AssistLayout(props: AIStudioLayoutProps) {
22
22
  const [connectors, setConnectors] = useState<Connector[]>([])
23
- const migrate = props.config.alphaMigration ?? true
24
23
 
25
24
  return (
26
25
  <AiAssistanceConfigProvider config={props.config}>
27
- {migrate ? <AlphaMigration /> : null}
28
26
  <RunInstructionProvider>
29
- <ConnectorsProvider onConnectorsChange={setConnectors}>
30
- {props.renderDefault(props)}
31
- <ThemeProvider tone="default">
32
- <AssistConnectorsOverlay connectors={connectors} />
33
- </ThemeProvider>
34
- </ConnectorsProvider>
27
+ <FieldTranslationProvider>
28
+ <ConnectorsProvider onConnectorsChange={setConnectors}>
29
+ {props.renderDefault(props)}
30
+ <ThemeProvider tone="default">
31
+ <AssistConnectorsOverlay connectors={connectors} />
32
+ </ThemeProvider>
33
+ </ConnectorsProvider>
34
+ </FieldTranslationProvider>
35
35
  </RunInstructionProvider>
36
36
  </AiAssistanceConfigProvider>
37
37
  )
@@ -1,17 +1,18 @@
1
1
  import {createContext, useEffect, useMemo, useState} from 'react'
2
2
  import {InputProps, pathToString, useSyncState} from 'sanity'
3
- import {getCaptionFieldOption} from '../helpers/typeUtils'
3
+ import {getDescriptionFieldOption, getImageInstructionFieldOption} from '../helpers/typeUtils'
4
4
  import {useAssistDocumentContext} from '../assistDocument/AssistDocumentContext'
5
- import {useApiClient, useGenerateCaption} from '../useApiClient'
5
+ import {canUseAssist, useApiClient, useGenerateCaption} from '../useApiClient'
6
6
  import {useAiAssistanceConfig} from '../assistLayout/AiAssistanceConfigContext'
7
7
  import {publicId} from '../helpers/ids'
8
8
 
9
9
  export interface ImageContextValue {
10
- captionPath: string
10
+ imageDescriptionPath?: string
11
+ imageInstructionPath?: string
11
12
  assetRef?: string
12
13
  }
13
14
 
14
- export const ImageContext = createContext<ImageContextValue | undefined>(undefined)
15
+ export const ImageContext = createContext<ImageContextValue>({})
15
16
 
16
17
  export function ImageContextProvider(props: InputProps) {
17
18
  const {schemaType, path, value} = props
@@ -19,23 +20,39 @@ export function ImageContextProvider(props: InputProps) {
19
20
  const [assetRefState, setAssetRefState] = useState<string | undefined>(assetRef)
20
21
 
21
22
  const {documentId, documentSchemaType} = useAssistDocumentContext()
22
- const {config} = useAiAssistanceConfig()
23
+ const {config, status} = useAiAssistanceConfig()
23
24
  const apiClient = useApiClient(config?.__customApiClient)
24
25
  const {generateCaption} = useGenerateCaption(apiClient)
25
26
 
26
27
  const {isSyncing} = useSyncState(publicId(documentId), documentSchemaType.name)
27
28
 
28
29
  useEffect(() => {
29
- const captionField = getCaptionFieldOption(schemaType)
30
- if (assetRef && documentId && captionField && assetRef !== assetRefState && !isSyncing) {
30
+ const descriptionField = getDescriptionFieldOption(schemaType)
31
+ if (
32
+ assetRef &&
33
+ documentId &&
34
+ descriptionField &&
35
+ assetRef !== assetRefState &&
36
+ !isSyncing &&
37
+ canUseAssist(status)
38
+ ) {
31
39
  setAssetRefState(assetRef)
32
- generateCaption({path: pathToString([...path, captionField]), documentId: documentId})
40
+ generateCaption({path: pathToString([...path, descriptionField]), documentId: documentId})
41
+ }
42
+ }, [schemaType, path, assetRef, assetRefState, documentId, generateCaption, isSyncing, status])
43
+
44
+ const context: ImageContextValue = useMemo(() => {
45
+ const descriptionField = getDescriptionFieldOption(schemaType)
46
+ const imageInstructionField = getImageInstructionFieldOption(schemaType)
47
+ return {
48
+ imageDescriptionPath: descriptionField
49
+ ? pathToString([...path, descriptionField])
50
+ : undefined,
51
+ imageInstructionPath: imageInstructionField
52
+ ? pathToString([...path, imageInstructionField])
53
+ : undefined,
54
+ assetRef,
33
55
  }
34
- }, [schemaType, path, assetRef, assetRefState, documentId, generateCaption, isSyncing])
35
-
36
- const context: ImageContextValue | undefined = useMemo(() => {
37
- const captionField = getCaptionFieldOption(schemaType)
38
- return captionField ? {captionPath: pathToString([...path, captionField]), assetRef} : undefined
39
56
  }, [schemaType, path, assetRef])
40
57
 
41
58
  return <ImageContext.Provider value={context}>{props.renderDefault(props)}</ImageContext.Provider>
@@ -31,7 +31,10 @@ export function ErrorWrapper(
31
31
  [setError]
32
32
  )
33
33
 
34
- const unsetValue = useCallback(() => onChange(PatchEvent.from(unset())), [onChange])
34
+ const unsetValue = useCallback(() => {
35
+ onChange(PatchEvent.from(unset()))
36
+ setError(undefined)
37
+ }, [onChange])
35
38
  const dismiss = useCallback(() => setError(undefined), [])
36
39
  const catcher = <ErrorBoundary onCatch={catchError}>{props.children}</ErrorBoundary>
37
40
 
@@ -3,11 +3,12 @@ import {
3
3
  DocumentFieldActionGroup,
4
4
  DocumentFieldActionItem,
5
5
  ObjectSchemaType,
6
+ typed,
6
7
  useCurrentUser,
7
8
  } from 'sanity'
8
9
  import {ControlsIcon, SparklesIcon} from '@sanity/icons'
9
- import {useCallback, useMemo} from 'react'
10
- import {pluginTitle, pluginTitleShort} from '../constants'
10
+ import {useCallback, useMemo, useRef} from 'react'
11
+ import {pluginTitleShort} from '../constants'
11
12
  import {useAssistSupported} from '../helpers/useAssistSupported'
12
13
  import {useAssistDocumentContext} from '../assistDocument/AssistDocumentContext'
13
14
  import {getInstructionTitle, usePathKey} from '../helpers/misc'
@@ -24,6 +25,9 @@ import {generateCaptionsActions} from './generateCaptionActions'
24
25
  import {useDocumentPane} from 'sanity/desk'
25
26
  import {useSelectedField, useTypePath} from '../assistInspector/helpers'
26
27
  import {isSchemaAssistEnabled} from '../helpers/assistSupported'
28
+ import {translateActions, TranslateProps} from '../translate/translateActions'
29
+ import {generateImagActions} from './generateImageActions'
30
+ import {getConditionalMembers} from '../helpers/conditionalMembers'
27
31
 
28
32
  function node(node: DocumentFieldActionItem | DocumentFieldActionGroup) {
29
33
  return node
@@ -47,6 +51,7 @@ export const assistFieldActions: DocumentFieldAction = {
47
51
  documentSchemaType,
48
52
  documentId,
49
53
  selectedPath,
54
+ assistableDocumentId,
50
55
  } =
51
56
  // document field actions do not have access to the document context
52
57
  // conditional hook _should_ be safe here since the logical path will be stable
@@ -56,7 +61,10 @@ export const assistFieldActions: DocumentFieldAction = {
56
61
  : // eslint-disable-next-line react-hooks/rules-of-hooks
57
62
  useAssistDocumentContext()
58
63
 
59
- const {value: docValue} = useDocumentPane()
64
+ const {value: docValue, formState} = useDocumentPane()
65
+ const formStateRef = useRef(formState)
66
+ formStateRef.current = formState
67
+
60
68
  const currentUser = useCurrentUser()
61
69
  const isHidden = !assistDocument
62
70
  const pathKey = usePathKey(props.path)
@@ -73,7 +81,8 @@ export const assistFieldActions: DocumentFieldAction = {
73
81
  const assistSupported =
74
82
  useAssistSupported(props.path, schemaType) &&
75
83
  isSelectable &&
76
- isSchemaAssistEnabled(documentSchemaType)
84
+ isSchemaAssistEnabled(documentSchemaType) &&
85
+ schemaType.readOnly !== true
77
86
 
78
87
  const fieldAssist = useMemo(
79
88
  () =>
@@ -89,7 +98,15 @@ export const assistFieldActions: DocumentFieldAction = {
89
98
  const isSelected = isInspectorOpen && isPathSelected
90
99
 
91
100
  const imageCaptionAction = generateCaptionsActions.useAction(props)
92
-
101
+ const imageGenAction = generateImagActions.useAction(props)
102
+ const translateAction = translateActions.useAction(
103
+ typed<TranslateProps>({
104
+ ...props,
105
+ documentId: assistableDocumentId,
106
+ documentIsAssistable,
107
+ documentSchemaType,
108
+ })
109
+ )
93
110
  const manageInstructions = useCallback(
94
111
  () =>
95
112
  isSelected
@@ -112,6 +129,9 @@ export const assistFieldActions: DocumentFieldAction = {
112
129
  path: pathKey,
113
130
  typePath,
114
131
  instruction,
132
+ conditionalMembers: formStateRef.current
133
+ ? getConditionalMembers(formStateRef.current)
134
+ : [],
115
135
  })
116
136
  },
117
137
  [requestRunInstruction, assistableDocId, pathKey, typePath, assistDocumentId, fieldAssistKey]
@@ -134,7 +154,7 @@ export const assistFieldActions: DocumentFieldAction = {
134
154
  )
135
155
 
136
156
  const runInstructionsGroup = useMemo(() => {
137
- return instructions?.length || imageCaptionAction
157
+ return instructions?.length || imageCaptionAction || translateAction || imageGenAction
138
158
  ? node({
139
159
  type: 'group',
140
160
  icon: () => null,
@@ -151,7 +171,8 @@ export const assistFieldActions: DocumentFieldAction = {
151
171
  })
152
172
  ),
153
173
  imageCaptionAction,
154
- ].filter(Boolean),
174
+ imageGenAction,
175
+ ].filter((a): a is DocumentFieldActionItem => !!a),
155
176
  expanded: true,
156
177
  })
157
178
  : undefined
@@ -163,6 +184,8 @@ export const assistFieldActions: DocumentFieldAction = {
163
184
  documentIsNew,
164
185
  assistSupported,
165
186
  imageCaptionAction,
187
+ translateAction,
188
+ imageGenAction,
166
189
  ])
167
190
 
168
191
  const instructionsLength = instructions?.length || 0
@@ -185,12 +208,16 @@ export const assistFieldActions: DocumentFieldAction = {
185
208
  type: 'group',
186
209
  icon: SparklesIcon,
187
210
  title: pluginTitleShort,
188
- children: [runInstructionsGroup, assistSupported && manageInstructionsItem].filter(
189
- (c): c is DocumentFieldActionItem | DocumentFieldActionGroup => !!c
190
- ),
211
+ children: [
212
+ runInstructionsGroup,
213
+ translateAction,
214
+ assistSupported && manageInstructionsItem,
215
+ ]
216
+ .filter((c): c is DocumentFieldActionItem | DocumentFieldActionGroup => !!c)
217
+ .filter((c) => (c.type === 'group' ? c.children.length : true)),
191
218
  expanded: false,
192
219
  renderAsButton: true,
193
- hidden: !assistSupported && !imageCaptionAction,
220
+ hidden: !assistSupported && !imageCaptionAction && !translateAction && !imageGenAction,
194
221
  }),
195
222
  [
196
223
  //documentIsNew,
@@ -198,6 +225,8 @@ export const assistFieldActions: DocumentFieldAction = {
198
225
  manageInstructionsItem,
199
226
  assistSupported,
200
227
  imageCaptionAction,
228
+ translateAction,
229
+ imageGenAction,
201
230
  ]
202
231
  )
203
232
 
@@ -216,7 +245,7 @@ export const assistFieldActions: DocumentFieldAction = {
216
245
  )
217
246
 
218
247
  // If there are no instructions, we don't want to render the group
219
- if (instructionsLength === 0 && !imageCaptionAction) {
248
+ if (instructionsLength === 0 && !imageCaptionAction && !translateAction && !imageGenAction) {
220
249
  return emptyAction
221
250
  }
222
251
 
@@ -239,7 +268,7 @@ function instructionItem(props: {
239
268
  iconRight: isPrivate ? PrivateIcon : undefined,
240
269
  title: getInstructionTitle(instruction),
241
270
  onAction: () => onInstructionAction(instruction),
242
- disabled: assistSupported ? false : {reason: `${pluginTitle} is not supported for this field`},
271
+ disabled: !assistSupported,
243
272
  hidden,
244
273
  })
245
274
  }
@@ -2,11 +2,14 @@ import {DocumentFieldAction, DocumentFieldActionGroup, DocumentFieldActionItem}
2
2
  import {ImageIcon} from '@sanity/icons'
3
3
  import {useContext, useMemo} from 'react'
4
4
  import {usePathKey} from '../helpers/misc'
5
- import {useApiClient, useGenerateCaption} from '../useApiClient'
5
+ import {canUseAssist, useApiClient, useGenerateCaption} from '../useApiClient'
6
6
  import {useAiAssistanceConfig} from '../assistLayout/AiAssistanceConfigContext'
7
7
  import {useAssistDocumentContext} from '../assistDocument/AssistDocumentContext'
8
8
  import {ImageContext} from '../components/ImageContext'
9
9
  import {Box, Spinner} from '@sanity/ui'
10
+ import {useDocumentPane} from 'sanity/desk'
11
+ import {aiInspectorId} from '../assistInspector/constants'
12
+ import {fieldPathParam, instructionParam} from '../types'
10
13
 
11
14
  function node(node: DocumentFieldActionItem | DocumentFieldActionGroup) {
12
15
  return node
@@ -16,14 +19,14 @@ export const generateCaptionsActions: DocumentFieldAction = {
16
19
  name: 'sanity-assist-generate-captions',
17
20
  useAction(props) {
18
21
  const pathKey = usePathKey(props.path)
22
+ const {openInspector} = useDocumentPane()
19
23
 
20
- const {config} = useAiAssistanceConfig()
24
+ const {config, status} = useAiAssistanceConfig()
21
25
  const apiClient = useApiClient(config?.__customApiClient)
22
26
  const {generateCaption, loading} = useGenerateCaption(apiClient)
23
-
24
27
  const imageContext = useContext(ImageContext)
25
28
 
26
- if (imageContext && pathKey === imageContext?.captionPath) {
29
+ if (imageContext && pathKey === imageContext?.imageDescriptionPath) {
27
30
  //if this is true, it is stable, and not breaking rules of hooks
28
31
  // eslint-disable-next-line react-hooks/rules-of-hooks
29
32
  const {documentId} = useAssistDocumentContext()
@@ -38,18 +41,26 @@ export const generateCaptionsActions: DocumentFieldAction = {
38
41
  </Box>
39
42
  )
40
43
  : ImageIcon,
41
- title: 'Generate caption',
44
+ title: 'Generate image description',
42
45
  onAction: () => {
43
46
  if (loading) {
44
47
  return
45
48
  }
49
+ if (!canUseAssist(status)) {
50
+ openInspector(aiInspectorId, {
51
+ [fieldPathParam]: pathKey,
52
+ [instructionParam]: undefined as any,
53
+ })
54
+ return
55
+ }
56
+
46
57
  generateCaption({path: pathKey, documentId: documentId ?? ''})
47
58
  },
48
59
  renderAsButton: true,
49
60
  disabled: loading,
50
61
  hidden: !imageContext.assetRef,
51
62
  })
52
- }, [generateCaption, pathKey, documentId, loading, imageContext])
63
+ }, [generateCaption, pathKey, documentId, loading, imageContext, status, openInspector])
53
64
  }
54
65
 
55
66
  // works but not supported by types
@@ -0,0 +1,57 @@
1
+ import {DocumentFieldAction, DocumentFieldActionGroup, DocumentFieldActionItem} from 'sanity'
2
+ import {ImageIcon} from '@sanity/icons'
3
+ import {useContext, useMemo} from 'react'
4
+ import {usePathKey} from '../helpers/misc'
5
+ import {useApiClient, useGenerateImage} from '../useApiClient'
6
+ import {useAiAssistanceConfig} from '../assistLayout/AiAssistanceConfigContext'
7
+ import {useAssistDocumentContext} from '../assistDocument/AssistDocumentContext'
8
+ import {ImageContext} from '../components/ImageContext'
9
+ import {Box, Spinner} from '@sanity/ui'
10
+
11
+ function node(node: DocumentFieldActionItem | DocumentFieldActionGroup) {
12
+ return node
13
+ }
14
+
15
+ export const generateImagActions: DocumentFieldAction = {
16
+ name: 'sanity-assist-generate-image',
17
+ useAction(props) {
18
+ const pathKey = usePathKey(props.path)
19
+
20
+ const {config} = useAiAssistanceConfig()
21
+ const apiClient = useApiClient(config?.__customApiClient)
22
+ const {generateImage, loading} = useGenerateImage(apiClient)
23
+
24
+ const imageContext = useContext(ImageContext)
25
+
26
+ if (imageContext && pathKey === imageContext?.imageInstructionPath) {
27
+ //if this is true, it is stable, and not breaking rules of hooks
28
+ // eslint-disable-next-line react-hooks/rules-of-hooks
29
+ const {documentId} = useAssistDocumentContext()
30
+ // eslint-disable-next-line react-hooks/rules-of-hooks
31
+ return useMemo(() => {
32
+ return node({
33
+ type: 'action',
34
+ icon: loading
35
+ ? () => (
36
+ <Box style={{height: 17}}>
37
+ <Spinner style={{transform: 'translateY(6px)'}} />
38
+ </Box>
39
+ )
40
+ : ImageIcon,
41
+ title: 'Generate image from prompt',
42
+ onAction: () => {
43
+ if (loading) {
44
+ return
45
+ }
46
+ generateImage({path: pathKey, documentId: documentId ?? ''})
47
+ },
48
+ renderAsButton: true,
49
+ disabled: loading,
50
+ })
51
+ }, [generateImage, pathKey, documentId, loading])
52
+ }
53
+
54
+ // works but not supported by types
55
+ return undefined as unknown as DocumentFieldActionItem
56
+ },
57
+ }