@sanity/assist 1.0.12 → 1.1.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 +27 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +209 -49
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +209 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/assistDocument/hooks/useInstructionToaster.tsx +1 -1
- package/src/assistInspector/InstructionTaskHistoryButton.tsx +1 -1
- package/src/components/FadeInContent.tsx +18 -11
- package/src/components/ImageContext.tsx +39 -0
- package/src/fieldActions/assistFieldActions.tsx +24 -26
- package/src/fieldActions/generateCaptionActions.tsx +58 -0
- package/src/helpers/typeUtils.ts +16 -1
- package/src/plugin.tsx +18 -0
- package/src/schemas/typeDefExtensions.ts +1 -0
- package/src/types.ts +1 -0
- package/src/useApiClient.ts +53 -0
package/package.json
CHANGED
|
@@ -36,7 +36,7 @@ export function useInstructionToaster(documentId: string, documentSchemaType: Ob
|
|
|
36
36
|
.filter((task) => task.ended && isAfter(addSeconds(new Date(task.ended), 30), new Date()))
|
|
37
37
|
|
|
38
38
|
endedTasks?.forEach((task) => {
|
|
39
|
-
const title = getInstructionTitle(task.instruction)
|
|
39
|
+
const title = task.title ?? getInstructionTitle(task.instruction)
|
|
40
40
|
if (task.reason === 'error') {
|
|
41
41
|
toast.push({
|
|
42
42
|
title: `Failed: ${title}`,
|
|
@@ -111,7 +111,7 @@ export function InstructionTaskHistoryButton(props: InstructionTaskHistoryButton
|
|
|
111
111
|
const instruction = instructions?.find((i) => i._key === task.instructionKey)
|
|
112
112
|
return {
|
|
113
113
|
...task,
|
|
114
|
-
title: showTitles ? getInstructionTitle(instruction) : undefined,
|
|
114
|
+
title: showTitles ? task.title ?? getInstructionTitle(instruction) : undefined,
|
|
115
115
|
cancel: () => cancelRun(task._key),
|
|
116
116
|
}
|
|
117
117
|
}) ?? []
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {ReactElement, ReactNode} from 'react'
|
|
1
|
+
import {forwardRef, ReactElement, ReactNode} from 'react'
|
|
2
2
|
import styled, {keyframes} from 'styled-components'
|
|
3
3
|
|
|
4
4
|
const fadeIn = keyframes`
|
|
@@ -21,13 +21,20 @@ const FadeInDiv = styled.div`
|
|
|
21
21
|
animation-timing-function: ease-in-out;
|
|
22
22
|
`
|
|
23
23
|
|
|
24
|
-
export function FadeInContent(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
export const FadeInContent = forwardRef(function FadeInContent(
|
|
25
|
+
{
|
|
26
|
+
children,
|
|
27
|
+
durationMs = 250,
|
|
28
|
+
}: {
|
|
29
|
+
children?: ReactNode
|
|
30
|
+
ms?: number
|
|
31
|
+
durationMs?: number
|
|
32
|
+
},
|
|
33
|
+
ref: any
|
|
34
|
+
): ReactElement {
|
|
35
|
+
return (
|
|
36
|
+
<FadeInDiv ref={ref} style={{animationDuration: `${durationMs}ms`}}>
|
|
37
|
+
{children}
|
|
38
|
+
</FadeInDiv>
|
|
39
|
+
)
|
|
40
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {createContext, useEffect, useMemo, useState} from 'react'
|
|
2
|
+
import {InputProps, pathToString} from 'sanity'
|
|
3
|
+
import {getCaptionFieldOption} from '../helpers/typeUtils'
|
|
4
|
+
import {useAssistDocumentContext} from '../assistDocument/AssistDocumentContext'
|
|
5
|
+
import {useApiClient, useGenerateCaption} from '../useApiClient'
|
|
6
|
+
import {useAiAssistanceConfig} from '../assistLayout/AiAssistanceConfigContext'
|
|
7
|
+
|
|
8
|
+
export interface ImageContextValue {
|
|
9
|
+
captionPath: string
|
|
10
|
+
assetRef?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ImageContext = createContext<ImageContextValue | undefined>(undefined)
|
|
14
|
+
|
|
15
|
+
export function ImageContextProvider(props: InputProps) {
|
|
16
|
+
const {schemaType, path, value} = props
|
|
17
|
+
const assetRef = (value as any)?.asset?._ref
|
|
18
|
+
const [assetRefState, setAssetRefState] = useState<string | undefined>(assetRef)
|
|
19
|
+
|
|
20
|
+
const {documentId} = useAssistDocumentContext()
|
|
21
|
+
const {config} = useAiAssistanceConfig()
|
|
22
|
+
const apiClient = useApiClient(config?.__customApiClient)
|
|
23
|
+
const {generateCaption} = useGenerateCaption(apiClient)
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const captionField = getCaptionFieldOption(schemaType)
|
|
27
|
+
if (assetRef && documentId && captionField && assetRef !== assetRefState) {
|
|
28
|
+
setAssetRefState(assetRef)
|
|
29
|
+
generateCaption({path: pathToString([...path, captionField]), documentId: documentId})
|
|
30
|
+
}
|
|
31
|
+
}, [schemaType, path, assetRef, assetRefState, documentId, generateCaption])
|
|
32
|
+
|
|
33
|
+
const context: ImageContextValue | undefined = useMemo(() => {
|
|
34
|
+
const captionField = getCaptionFieldOption(schemaType)
|
|
35
|
+
return captionField ? {captionPath: pathToString([...path, captionField]), assetRef} : undefined
|
|
36
|
+
}, [schemaType, path, assetRef])
|
|
37
|
+
|
|
38
|
+
return <ImageContext.Provider value={context}>{props.renderDefault(props)}</ImageContext.Provider>
|
|
39
|
+
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
useRequestRunInstruction,
|
|
21
21
|
} from '../assistDocument/RequestRunInstructionProvider'
|
|
22
22
|
import {PrivateIcon} from './PrivateIcon'
|
|
23
|
+
import {generateCaptionsActions} from './generateCaptionActions'
|
|
23
24
|
|
|
24
25
|
function node(node: DocumentFieldActionItem | DocumentFieldActionGroup) {
|
|
25
26
|
return node
|
|
@@ -73,6 +74,8 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
73
74
|
const isPathSelected = pathKey === selectedPath
|
|
74
75
|
const isSelected = isInspectorOpen && isPathSelected
|
|
75
76
|
|
|
77
|
+
const imageCaptionAction = generateCaptionsActions.useAction(props)
|
|
78
|
+
|
|
76
79
|
const manageInstructions = useCallback(
|
|
77
80
|
() =>
|
|
78
81
|
isSelected
|
|
@@ -116,21 +119,24 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
116
119
|
)
|
|
117
120
|
|
|
118
121
|
const runInstructionsGroup = useMemo(() => {
|
|
119
|
-
return instructions?.length
|
|
122
|
+
return instructions?.length || imageCaptionAction
|
|
120
123
|
? node({
|
|
121
124
|
type: 'group',
|
|
122
125
|
icon: () => null,
|
|
123
126
|
title: 'Run instructions',
|
|
124
|
-
children:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
children: [
|
|
128
|
+
...instructions?.map((instruction) =>
|
|
129
|
+
instructionItem({
|
|
130
|
+
instruction,
|
|
131
|
+
isPrivate: Boolean(instruction.userId && instruction.userId === currentUser?.id),
|
|
132
|
+
onInstructionAction,
|
|
133
|
+
hidden: isHidden,
|
|
134
|
+
documentIsNew: !!documentIsNew,
|
|
135
|
+
assistSupported,
|
|
136
|
+
})
|
|
137
|
+
),
|
|
138
|
+
imageCaptionAction,
|
|
139
|
+
].filter(Boolean),
|
|
134
140
|
expanded: true,
|
|
135
141
|
})
|
|
136
142
|
: undefined
|
|
@@ -141,6 +147,7 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
141
147
|
isHidden,
|
|
142
148
|
documentIsNew,
|
|
143
149
|
assistSupported,
|
|
150
|
+
imageCaptionAction,
|
|
144
151
|
])
|
|
145
152
|
|
|
146
153
|
const instructionsLength = instructions?.length || 0
|
|
@@ -163,28 +170,19 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
163
170
|
type: 'group',
|
|
164
171
|
icon: SparklesIcon,
|
|
165
172
|
title: pluginTitleShort,
|
|
166
|
-
children: [
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
type: 'action',
|
|
170
|
-
disabled: true,
|
|
171
|
-
title: `Document is new. Make an edit to enable `,
|
|
172
|
-
icon: WarningOutlineIcon,
|
|
173
|
-
tone: 'caution',
|
|
174
|
-
status: 'warning',
|
|
175
|
-
onAction: () => {},
|
|
176
|
-
},*/
|
|
177
|
-
manageInstructionsItem,
|
|
178
|
-
].filter((c): c is DocumentFieldActionItem | DocumentFieldActionGroup => !!c),
|
|
173
|
+
children: [runInstructionsGroup, assistSupported && manageInstructionsItem].filter(
|
|
174
|
+
(c): c is DocumentFieldActionItem | DocumentFieldActionGroup => !!c
|
|
175
|
+
),
|
|
179
176
|
expanded: false,
|
|
180
177
|
renderAsButton: true,
|
|
181
|
-
hidden: !assistSupported,
|
|
178
|
+
hidden: !assistSupported && !imageCaptionAction,
|
|
182
179
|
}),
|
|
183
180
|
[
|
|
184
181
|
//documentIsNew,
|
|
185
182
|
runInstructionsGroup,
|
|
186
183
|
manageInstructionsItem,
|
|
187
184
|
assistSupported,
|
|
185
|
+
imageCaptionAction,
|
|
188
186
|
]
|
|
189
187
|
)
|
|
190
188
|
|
|
@@ -203,7 +201,7 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
203
201
|
)
|
|
204
202
|
|
|
205
203
|
// If there are no instructions, we don't want to render the group
|
|
206
|
-
if (instructionsLength === 0) {
|
|
204
|
+
if (instructionsLength === 0 && !imageCaptionAction) {
|
|
207
205
|
return emptyAction
|
|
208
206
|
}
|
|
209
207
|
|
|
@@ -0,0 +1,58 @@
|
|
|
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, useGenerateCaption} 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 generateCaptionsActions: DocumentFieldAction = {
|
|
16
|
+
name: 'sanity-assist-generate-captions',
|
|
17
|
+
useAction(props) {
|
|
18
|
+
const pathKey = usePathKey(props.path)
|
|
19
|
+
|
|
20
|
+
const {config} = useAiAssistanceConfig()
|
|
21
|
+
const apiClient = useApiClient(config?.__customApiClient)
|
|
22
|
+
const {generateCaption, loading} = useGenerateCaption(apiClient)
|
|
23
|
+
|
|
24
|
+
const imageContext = useContext(ImageContext)
|
|
25
|
+
|
|
26
|
+
if (imageContext && pathKey === imageContext?.captionPath) {
|
|
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 caption',
|
|
42
|
+
onAction: () => {
|
|
43
|
+
if (loading) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
generateCaption({path: pathKey, documentId: documentId ?? ''})
|
|
47
|
+
},
|
|
48
|
+
renderAsButton: true,
|
|
49
|
+
disabled: loading,
|
|
50
|
+
hidden: !imageContext.assetRef,
|
|
51
|
+
})
|
|
52
|
+
}, [generateCaption, pathKey, documentId, loading, imageContext])
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// works but not supported by types
|
|
56
|
+
return undefined as unknown as DocumentFieldActionItem
|
|
57
|
+
},
|
|
58
|
+
}
|
package/src/helpers/typeUtils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {ArraySchemaType, SchemaType} from 'sanity'
|
|
1
|
+
import {ArraySchemaType, ImageOptions, SchemaType} from 'sanity'
|
|
2
2
|
|
|
3
3
|
export function isPortableTextArray(type: ArraySchemaType) {
|
|
4
4
|
return type.of.find((t) => isType(t, 'block'))
|
|
@@ -13,3 +13,18 @@ export function isType(schemaType: SchemaType, typeName: string): boolean {
|
|
|
13
13
|
}
|
|
14
14
|
return isType(schemaType.type, typeName)
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
export function isImage(schemaType: SchemaType) {
|
|
18
|
+
return isType(schemaType, 'image')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getCaptionFieldOption(schemaType: SchemaType | undefined): string | undefined {
|
|
22
|
+
if (!schemaType) {
|
|
23
|
+
return undefined
|
|
24
|
+
}
|
|
25
|
+
const captionField = (schemaType.options as ImageOptions)?.captionField
|
|
26
|
+
if (captionField) {
|
|
27
|
+
return captionField
|
|
28
|
+
}
|
|
29
|
+
return getCaptionFieldOption(schemaType.type)
|
|
30
|
+
}
|
package/src/plugin.tsx
CHANGED
|
@@ -13,6 +13,8 @@ import {packageName} from './constants'
|
|
|
13
13
|
import {AssistDocumentInputWrapper} from './assistDocument/AssistDocumentInput'
|
|
14
14
|
import {createAssistDocumentPresence} from './presence/AssistDocumentPresence'
|
|
15
15
|
import {isSchemaAssistEnabled} from './helpers/assistSupported'
|
|
16
|
+
import {isImage} from './helpers/typeUtils'
|
|
17
|
+
import {ImageContextProvider} from './components/ImageContext'
|
|
16
18
|
|
|
17
19
|
export interface AssistPluginConfig {
|
|
18
20
|
/**
|
|
@@ -75,6 +77,22 @@ export const assist = definePlugin<AssistPluginConfig | void>((config) => {
|
|
|
75
77
|
name: `${packageName}/safe-value-input`,
|
|
76
78
|
form: {components: {input: SafeValueInput}},
|
|
77
79
|
})(),
|
|
80
|
+
|
|
81
|
+
definePlugin({
|
|
82
|
+
name: `${packageName}/generate-caption`,
|
|
83
|
+
form: {
|
|
84
|
+
components: {
|
|
85
|
+
input: (props) => {
|
|
86
|
+
const {schemaType} = props
|
|
87
|
+
|
|
88
|
+
if (isImage(schemaType)) {
|
|
89
|
+
return <ImageContextProvider {...props} />
|
|
90
|
+
}
|
|
91
|
+
return props.renderDefault(props)
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
})(),
|
|
78
96
|
],
|
|
79
97
|
}
|
|
80
98
|
})
|
|
@@ -18,6 +18,7 @@ declare module 'sanity' {
|
|
|
18
18
|
interface GeopointOptions extends AssistOptions {}
|
|
19
19
|
interface ImageOptions extends AssistOptions {
|
|
20
20
|
imagePromptField?: string
|
|
21
|
+
captionField?: string
|
|
21
22
|
}
|
|
22
23
|
interface NumberOptions extends AssistOptions {}
|
|
23
24
|
interface ObjectOptions extends AssistOptions {}
|
package/src/types.ts
CHANGED
package/src/useApiClient.ts
CHANGED
|
@@ -35,6 +35,59 @@ export function useApiClient(customApiClient?: (defaultClient: SanityClient) =>
|
|
|
35
35
|
)
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export function useGenerateCaption(apiClient: SanityClient) {
|
|
39
|
+
const [loading, setLoading] = useState(false)
|
|
40
|
+
const user = useCurrentUser()
|
|
41
|
+
const schema = useSchema()
|
|
42
|
+
const types = useMemo(() => serializeSchema(schema, {leanFormat: true}), [schema])
|
|
43
|
+
const toast = useToast()
|
|
44
|
+
|
|
45
|
+
const generateCaption = useCallback(
|
|
46
|
+
({path, documentId}: {path: string; documentId: string}) => {
|
|
47
|
+
setLoading(true)
|
|
48
|
+
|
|
49
|
+
return apiClient
|
|
50
|
+
.request({
|
|
51
|
+
method: 'POST',
|
|
52
|
+
url: `/assist/tasks/generate-caption/${apiClient.config().dataset}?projectId=${
|
|
53
|
+
apiClient.config().projectId
|
|
54
|
+
}`,
|
|
55
|
+
body: {
|
|
56
|
+
path,
|
|
57
|
+
documentId,
|
|
58
|
+
types,
|
|
59
|
+
userId: user?.id,
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
.catch((e) => {
|
|
63
|
+
toast.push({
|
|
64
|
+
status: 'error',
|
|
65
|
+
title: 'Generate caption failed',
|
|
66
|
+
description: e.message,
|
|
67
|
+
})
|
|
68
|
+
setLoading(false)
|
|
69
|
+
throw e
|
|
70
|
+
})
|
|
71
|
+
.finally(() => {
|
|
72
|
+
// adding some artificial delay here
|
|
73
|
+
// server responds with 201 then proceeds; we dont need to allow spamming the button
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
setLoading(false)
|
|
76
|
+
}, 2000)
|
|
77
|
+
})
|
|
78
|
+
},
|
|
79
|
+
[setLoading, apiClient, toast, user, types]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return useMemo(
|
|
83
|
+
() => ({
|
|
84
|
+
generateCaption,
|
|
85
|
+
loading,
|
|
86
|
+
}),
|
|
87
|
+
[generateCaption, loading]
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
38
91
|
export function useGetInstructStatus(apiClient: SanityClient) {
|
|
39
92
|
const [loading, setLoading] = useState(true)
|
|
40
93
|
const projectClient = useClient({apiVersion: '2023-06-05'})
|