@sanity/assist 1.2.15 → 2.0.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/LICENSE +1 -1
- package/README.md +551 -30
- package/dist/index.cjs.mjs +1 -0
- package/dist/index.d.ts +253 -11
- package/dist/index.esm.js +2405 -392
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2399 -385
- package/dist/index.js.map +1 -1
- package/package.json +15 -14
- package/src/_lib/form/DocumentForm.tsx +3 -2
- package/src/_lib/form/constants.ts +1 -0
- package/src/assistDocument/AssistDocumentInput.tsx +24 -4
- package/src/assistDocument/RequestRunInstructionProvider.tsx +37 -21
- package/src/assistDocument/components/instruction/InstructionInput.tsx +5 -4
- package/src/assistDocument/components/instruction/InstructionOutputField.tsx +45 -0
- package/src/assistDocument/components/instruction/InstructionOutputInput.tsx +205 -0
- package/src/assistDocument/hooks/useStudioAssistDocument.ts +5 -32
- package/src/assistFormComponents/AssistField.tsx +11 -5
- package/src/assistFormComponents/AssistFormBlock.tsx +2 -3
- package/src/assistFormComponents/validation/listItem.tsx +2 -2
- package/src/assistInspector/AssistInspector.tsx +6 -0
- package/src/assistInspector/FieldAutocomplete.tsx +1 -0
- package/src/assistInspector/InstructionTaskHistoryButton.tsx +2 -3
- package/src/assistInspector/helpers.ts +9 -11
- package/src/assistLayout/AssistLayout.tsx +9 -9
- package/src/components/ImageContext.tsx +19 -9
- package/src/components/SafeValueInput.tsx +4 -1
- package/src/fieldActions/assistFieldActions.tsx +42 -13
- package/src/fieldActions/generateCaptionActions.tsx +2 -2
- package/src/fieldActions/generateImageActions.tsx +57 -0
- package/src/helpers/assistSupported.ts +10 -16
- package/src/helpers/conditionalMembers.test.ts +200 -0
- package/src/helpers/conditionalMembers.ts +127 -0
- package/src/helpers/typeUtils.ts +19 -5
- package/src/index.ts +3 -0
- package/src/plugin.tsx +14 -5
- package/src/presence/AssistAvatar.tsx +1 -1
- package/src/schemas/assistDocumentSchema.tsx +40 -1
- package/src/schemas/serialize/serializeSchema.test.ts +239 -8
- package/src/schemas/serialize/serializeSchema.ts +77 -10
- package/src/schemas/typeDefExtensions.ts +89 -5
- package/src/translate/FieldTranslationProvider.tsx +360 -0
- package/src/translate/getLanguageParams.ts +26 -0
- package/src/translate/languageStore.ts +18 -0
- package/src/translate/paths.test.ts +133 -0
- package/src/translate/paths.ts +175 -0
- package/src/translate/translateActions.tsx +188 -0
- package/src/translate/types.ts +160 -0
- package/src/types.ts +33 -12
- package/src/useApiClient.ts +130 -2
- package/src/assistLayout/AlphaMigration.tsx +0 -310
- package/src/legacy-types.ts +0 -72
|
@@ -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
|
)
|
|
@@ -167,7 +167,7 @@ export function InstructionTaskHistoryButton(props: InstructionTaskHistoryButton
|
|
|
167
167
|
)
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
const TASK_STATUS_BUTTON_TOOLTIP_PROPS: StatusButtonProps['
|
|
170
|
+
const TASK_STATUS_BUTTON_TOOLTIP_PROPS: StatusButtonProps['tooltipProps'] = {
|
|
171
171
|
placement: 'top',
|
|
172
172
|
}
|
|
173
173
|
|
|
@@ -190,11 +190,10 @@ const TaskStatusButton = forwardRef(function TaskStatusButton(
|
|
|
190
190
|
mode="bleed"
|
|
191
191
|
onClick={onClick}
|
|
192
192
|
tone={hasErrors ? 'critical' : undefined}
|
|
193
|
-
fontSize={1}
|
|
194
193
|
disabled={disabled}
|
|
195
194
|
ref={ref}
|
|
196
195
|
selected={selected}
|
|
197
|
-
|
|
196
|
+
tooltipProps={TASK_STATUS_BUTTON_TOOLTIP_PROPS}
|
|
198
197
|
/>
|
|
199
198
|
)
|
|
200
199
|
})
|
|
@@ -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
|
|
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
|
|
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 {
|
|
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
|
-
<
|
|
30
|
-
{
|
|
31
|
-
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
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 {
|
|
3
|
+
import {getDescriptionFieldOption, getImageInstructionFieldOption} from '../helpers/typeUtils'
|
|
4
4
|
import {useAssistDocumentContext} from '../assistDocument/AssistDocumentContext'
|
|
5
5
|
import {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
|
-
|
|
10
|
+
imageDescriptionPath?: string
|
|
11
|
+
imageInstructionPath?: string
|
|
11
12
|
assetRef?: string
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export const ImageContext = createContext<ImageContextValue
|
|
15
|
+
export const ImageContext = createContext<ImageContextValue>({})
|
|
15
16
|
|
|
16
17
|
export function ImageContextProvider(props: InputProps) {
|
|
17
18
|
const {schemaType, path, value} = props
|
|
@@ -26,16 +27,25 @@ export function ImageContextProvider(props: InputProps) {
|
|
|
26
27
|
const {isSyncing} = useSyncState(publicId(documentId), documentSchemaType.name)
|
|
27
28
|
|
|
28
29
|
useEffect(() => {
|
|
29
|
-
const
|
|
30
|
-
if (assetRef && documentId &&
|
|
30
|
+
const descriptionField = getDescriptionFieldOption(schemaType)
|
|
31
|
+
if (assetRef && documentId && descriptionField && assetRef !== assetRefState && !isSyncing) {
|
|
31
32
|
setAssetRefState(assetRef)
|
|
32
|
-
generateCaption({path: pathToString([...path,
|
|
33
|
+
generateCaption({path: pathToString([...path, descriptionField]), documentId: documentId})
|
|
33
34
|
}
|
|
34
35
|
}, [schemaType, path, assetRef, assetRefState, documentId, generateCaption, isSyncing])
|
|
35
36
|
|
|
36
|
-
const context: ImageContextValue
|
|
37
|
-
const
|
|
38
|
-
|
|
37
|
+
const context: ImageContextValue = useMemo(() => {
|
|
38
|
+
const descriptionField = getDescriptionFieldOption(schemaType)
|
|
39
|
+
const imageInstructionField = getImageInstructionFieldOption(schemaType)
|
|
40
|
+
return {
|
|
41
|
+
imageDescriptionPath: descriptionField
|
|
42
|
+
? pathToString([...path, descriptionField])
|
|
43
|
+
: undefined,
|
|
44
|
+
imageInstructionPath: imageInstructionField
|
|
45
|
+
? pathToString([...path, imageInstructionField])
|
|
46
|
+
: undefined,
|
|
47
|
+
assetRef,
|
|
48
|
+
}
|
|
39
49
|
}, [schemaType, path, assetRef])
|
|
40
50
|
|
|
41
51
|
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(() =>
|
|
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 {
|
|
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
|
-
|
|
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: [
|
|
189
|
-
|
|
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
|
|
271
|
+
disabled: !assistSupported,
|
|
243
272
|
hidden,
|
|
244
273
|
})
|
|
245
274
|
}
|
|
@@ -23,7 +23,7 @@ export const generateCaptionsActions: DocumentFieldAction = {
|
|
|
23
23
|
|
|
24
24
|
const imageContext = useContext(ImageContext)
|
|
25
25
|
|
|
26
|
-
if (imageContext && pathKey === imageContext?.
|
|
26
|
+
if (imageContext && pathKey === imageContext?.imageDescriptionPath) {
|
|
27
27
|
//if this is true, it is stable, and not breaking rules of hooks
|
|
28
28
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
29
29
|
const {documentId} = useAssistDocumentContext()
|
|
@@ -38,7 +38,7 @@ export const generateCaptionsActions: DocumentFieldAction = {
|
|
|
38
38
|
</Box>
|
|
39
39
|
)
|
|
40
40
|
: ImageIcon,
|
|
41
|
-
title: 'Generate
|
|
41
|
+
title: 'Generate image description',
|
|
42
42
|
onAction: () => {
|
|
43
43
|
if (loading) {
|
|
44
44
|
return
|
|
@@ -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
|
+
}
|
|
@@ -1,41 +1,34 @@
|
|
|
1
|
-
import {SchemaType} from 'sanity'
|
|
1
|
+
import {ReferenceOptions, SchemaType} from 'sanity'
|
|
2
2
|
import {AssistOptions} from '../schemas/typeDefExtensions'
|
|
3
3
|
import {isType} from './typeUtils'
|
|
4
4
|
|
|
5
5
|
export function isSchemaAssistEnabled(type: SchemaType) {
|
|
6
|
-
return !(type.options as AssistOptions | undefined)?.
|
|
6
|
+
return !(type.options as AssistOptions | undefined)?.aiAssist?.exclude
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export function isAssistSupported(type: SchemaType
|
|
9
|
+
export function isAssistSupported(type: SchemaType) {
|
|
10
10
|
if (!isSchemaAssistEnabled(type)) {
|
|
11
11
|
return false
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
if (isDisabled(type
|
|
14
|
+
if (isDisabled(type)) {
|
|
15
15
|
return false
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
if (type.jsonType === 'array') {
|
|
19
|
-
const unsupportedArray = type.of.every((t) => isDisabled(t
|
|
19
|
+
const unsupportedArray = type.of.every((t) => isDisabled(t))
|
|
20
20
|
return !unsupportedArray
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
if (type.jsonType === 'object') {
|
|
24
|
-
const unsupportedObject = type.fields.every((field) =>
|
|
25
|
-
isDisabled(field.type, allowReadonlyHidden)
|
|
26
|
-
)
|
|
24
|
+
const unsupportedObject = type.fields.every((field) => isDisabled(field.type))
|
|
27
25
|
return !unsupportedObject
|
|
28
26
|
}
|
|
29
27
|
return true
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
function isDisabled(type: SchemaType
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
!isSchemaAssistEnabled(type) ||
|
|
36
|
-
isUnsupportedType(type) ||
|
|
37
|
-
(!allowReadonlyHidden && readonlyHidden)
|
|
38
|
-
)
|
|
30
|
+
function isDisabled(type: SchemaType) {
|
|
31
|
+
return !isSchemaAssistEnabled(type) || isUnsupportedType(type)
|
|
39
32
|
}
|
|
40
33
|
|
|
41
34
|
function isUnsupportedType(type: SchemaType) {
|
|
@@ -43,7 +36,8 @@ function isUnsupportedType(type: SchemaType) {
|
|
|
43
36
|
type.jsonType === 'number' ||
|
|
44
37
|
type.name === 'sanity.imageCrop' ||
|
|
45
38
|
type.name === 'sanity.imageHotspot' ||
|
|
46
|
-
isType(type, 'reference')
|
|
39
|
+
(isType(type, 'reference') &&
|
|
40
|
+
!(type?.options as ReferenceOptions)?.aiAssist?.embeddingsIndex) ||
|
|
47
41
|
isType(type, 'crossDatasetReference') ||
|
|
48
42
|
isType(type, 'slug') ||
|
|
49
43
|
isType(type, 'url') ||
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import {describe, expect, test} from 'vitest'
|
|
2
|
+
import {Schema} from '@sanity/schema'
|
|
3
|
+
import {ArraySchemaType, defineField, defineType, ObjectSchemaType} from 'sanity'
|
|
4
|
+
import {getConditionalMembers} from './conditionalMembers'
|
|
5
|
+
|
|
6
|
+
describe('conditionalMembers', () => {
|
|
7
|
+
test('should not include paths without conditional hidden/readonly', () => {
|
|
8
|
+
const docSchema: ObjectSchemaType = Schema.compile({
|
|
9
|
+
name: 'test',
|
|
10
|
+
types: [
|
|
11
|
+
defineType({
|
|
12
|
+
type: 'document',
|
|
13
|
+
name: 'article',
|
|
14
|
+
fields: [{type: 'string', name: 'title'}],
|
|
15
|
+
}),
|
|
16
|
+
],
|
|
17
|
+
}).get('article')
|
|
18
|
+
|
|
19
|
+
const docState = {
|
|
20
|
+
path: [],
|
|
21
|
+
schemaType: docSchema,
|
|
22
|
+
members: [
|
|
23
|
+
{
|
|
24
|
+
kind: 'field',
|
|
25
|
+
field: {path: [docSchema.fields[0].name], schemaType: docSchema.fields[0].type},
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
} as any
|
|
29
|
+
const conditionalMembers = getConditionalMembers(docState)
|
|
30
|
+
|
|
31
|
+
expect(conditionalMembers).toEqual([])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('should include path with conditional readonly', () => {
|
|
35
|
+
const docSchema: ObjectSchemaType = Schema.compile({
|
|
36
|
+
name: 'test',
|
|
37
|
+
types: [
|
|
38
|
+
defineType({
|
|
39
|
+
type: 'document',
|
|
40
|
+
name: 'article',
|
|
41
|
+
fields: [{type: 'string', name: 'title', readOnly: () => false}],
|
|
42
|
+
}),
|
|
43
|
+
],
|
|
44
|
+
}).get('article')
|
|
45
|
+
|
|
46
|
+
const docState = {
|
|
47
|
+
path: [],
|
|
48
|
+
schemaType: docSchema,
|
|
49
|
+
members: [
|
|
50
|
+
{
|
|
51
|
+
kind: 'field',
|
|
52
|
+
field: {path: [docSchema.fields[0].name], schemaType: docSchema.fields[0].type},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
} as any
|
|
56
|
+
const conditionalMembers = getConditionalMembers(docState)
|
|
57
|
+
|
|
58
|
+
expect(conditionalMembers).toEqual([{path: 'title', hidden: false, readOnly: false}])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('should include array item path with conditional readonly', () => {
|
|
62
|
+
const docSchema: ObjectSchemaType = Schema.compile({
|
|
63
|
+
name: 'test',
|
|
64
|
+
types: [
|
|
65
|
+
defineType({
|
|
66
|
+
type: 'document',
|
|
67
|
+
name: 'article',
|
|
68
|
+
fields: [{type: 'array', name: 'array', of: [{type: 'string', readOnly: () => true}]}],
|
|
69
|
+
}),
|
|
70
|
+
],
|
|
71
|
+
}).get('article')
|
|
72
|
+
|
|
73
|
+
const docState = {
|
|
74
|
+
path: [],
|
|
75
|
+
schemaType: docSchema,
|
|
76
|
+
members: [
|
|
77
|
+
{
|
|
78
|
+
kind: 'field',
|
|
79
|
+
field: {
|
|
80
|
+
path: [docSchema.fields[0].name],
|
|
81
|
+
schemaType: docSchema.fields[0].type,
|
|
82
|
+
members: [
|
|
83
|
+
{
|
|
84
|
+
kind: 'item',
|
|
85
|
+
item: {
|
|
86
|
+
path: [docSchema.fields[0].name, 0],
|
|
87
|
+
schemaType: (docSchema.fields[0].type as ArraySchemaType).of[0],
|
|
88
|
+
readOnly: true,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
} as any
|
|
96
|
+
const conditionalMembers = getConditionalMembers(docState)
|
|
97
|
+
|
|
98
|
+
expect(conditionalMembers).toEqual([
|
|
99
|
+
{
|
|
100
|
+
path: 'array[0]',
|
|
101
|
+
hidden: false,
|
|
102
|
+
readOnly: true,
|
|
103
|
+
},
|
|
104
|
+
])
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('should include object path with conditional hidden', () => {
|
|
108
|
+
const docSchema: ObjectSchemaType = Schema.compile({
|
|
109
|
+
name: 'test',
|
|
110
|
+
types: [
|
|
111
|
+
defineType({
|
|
112
|
+
type: 'document',
|
|
113
|
+
name: 'article',
|
|
114
|
+
fields: [
|
|
115
|
+
defineField({
|
|
116
|
+
type: 'object',
|
|
117
|
+
name: 'object',
|
|
118
|
+
fields: [{type: 'string', name: 'title', hidden: () => false}],
|
|
119
|
+
}),
|
|
120
|
+
],
|
|
121
|
+
}),
|
|
122
|
+
],
|
|
123
|
+
}).get('article')
|
|
124
|
+
|
|
125
|
+
const docState = {
|
|
126
|
+
path: [],
|
|
127
|
+
schemaType: docSchema,
|
|
128
|
+
members: [
|
|
129
|
+
{
|
|
130
|
+
kind: 'field',
|
|
131
|
+
field: {
|
|
132
|
+
path: [docSchema.fields[0].name],
|
|
133
|
+
schemaType: docSchema.fields[0].type,
|
|
134
|
+
members: [
|
|
135
|
+
{
|
|
136
|
+
kind: 'field',
|
|
137
|
+
field: {
|
|
138
|
+
path: [docSchema.fields[0].name, 'title'],
|
|
139
|
+
schemaType: (docSchema.fields[0].type as ObjectSchemaType).fields[0].type,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
} as any
|
|
147
|
+
const conditionalMembers = getConditionalMembers(docState)
|
|
148
|
+
|
|
149
|
+
expect(conditionalMembers).toEqual([
|
|
150
|
+
{
|
|
151
|
+
path: 'object.title',
|
|
152
|
+
hidden: false,
|
|
153
|
+
readOnly: false,
|
|
154
|
+
},
|
|
155
|
+
])
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('should include path with fieldset with conditional state', () => {
|
|
159
|
+
const docSchema: ObjectSchemaType = Schema.compile({
|
|
160
|
+
name: 'test',
|
|
161
|
+
types: [
|
|
162
|
+
defineType({
|
|
163
|
+
type: 'document',
|
|
164
|
+
name: 'article',
|
|
165
|
+
fieldsets: [{name: 'set', hidden: () => false}],
|
|
166
|
+
fields: [{type: 'string', fieldset: 'set', name: 'title'}],
|
|
167
|
+
}),
|
|
168
|
+
],
|
|
169
|
+
}).get('article')
|
|
170
|
+
|
|
171
|
+
const docState = {
|
|
172
|
+
path: [],
|
|
173
|
+
schemaType: docSchema,
|
|
174
|
+
members: [
|
|
175
|
+
{
|
|
176
|
+
kind: 'fieldSet',
|
|
177
|
+
fieldSet: {
|
|
178
|
+
name: 'set',
|
|
179
|
+
path: ['set'],
|
|
180
|
+
members: [
|
|
181
|
+
{
|
|
182
|
+
kind: 'field',
|
|
183
|
+
field: {path: [docSchema.fields[0].name], schemaType: docSchema.fields[0].type},
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
} as any
|
|
190
|
+
const conditionalMembers = getConditionalMembers(docState)
|
|
191
|
+
|
|
192
|
+
expect(conditionalMembers).toEqual([
|
|
193
|
+
{
|
|
194
|
+
path: 'title',
|
|
195
|
+
hidden: false,
|
|
196
|
+
readOnly: false,
|
|
197
|
+
},
|
|
198
|
+
])
|
|
199
|
+
})
|
|
200
|
+
})
|