@sanity/assist 1.0.11 → 1.1.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/README.md +28 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +283 -78
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +283 -78
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/assistDocument/hooks/useInstructionToaster.tsx +1 -1
- package/src/assistInspector/AssistInspector.tsx +7 -4
- 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 +18 -11
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/assist",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"react": "^18.2.0",
|
|
82
82
|
"react-dom": "^18.2.0",
|
|
83
83
|
"rimraf": "^4.4.0",
|
|
84
|
-
"sanity": "^3.
|
|
84
|
+
"sanity": "^3.14.5",
|
|
85
85
|
"semantic-release": "^21.0.5",
|
|
86
86
|
"styled-components": "^5.3.9",
|
|
87
87
|
"typescript": "^5.1.3",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
},
|
|
90
90
|
"peerDependencies": {
|
|
91
91
|
"react": "^18",
|
|
92
|
-
"sanity": "^3.
|
|
92
|
+
"sanity": "^3.14.5",
|
|
93
93
|
"styled-components": "^5.2"
|
|
94
94
|
},
|
|
95
95
|
"engines": {
|
|
@@ -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}`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {ArrowRightIcon, CloseIcon, PlayIcon, RetryIcon} from '@sanity/icons'
|
|
2
2
|
import {Box, Button, Card, Flex, Spinner, Stack, Text} from '@sanity/ui'
|
|
3
|
-
import {useCallback, useMemo,
|
|
3
|
+
import {useCallback, useMemo, useRef} from 'react'
|
|
4
4
|
import {
|
|
5
5
|
DocumentInspectorProps,
|
|
6
6
|
PresenceOverlay,
|
|
@@ -187,7 +187,7 @@ export function AssistInspectorWrapper(props: DocumentInspectorProps) {
|
|
|
187
187
|
export function AssistInspector(props: DocumentInspectorProps) {
|
|
188
188
|
const {params} = useAiPaneRouter()
|
|
189
189
|
|
|
190
|
-
const
|
|
190
|
+
const boundary = useRef<HTMLDivElement | null>(null)
|
|
191
191
|
const pathKey = params?.[fieldPathParam]
|
|
192
192
|
const instructionKey = params?.[instructionParam]
|
|
193
193
|
const documentPane = useDocumentPane()
|
|
@@ -275,7 +275,7 @@ export function AssistInspector(props: DocumentInspectorProps) {
|
|
|
275
275
|
|
|
276
276
|
return (
|
|
277
277
|
<Flex
|
|
278
|
-
ref={
|
|
278
|
+
ref={boundary}
|
|
279
279
|
direction="column"
|
|
280
280
|
height="fill"
|
|
281
281
|
overflow="hidden"
|
|
@@ -290,7 +290,10 @@ export function AssistInspector(props: DocumentInspectorProps) {
|
|
|
290
290
|
<PresenceOverlay>
|
|
291
291
|
<Box padding={4}>
|
|
292
292
|
{selectedField && (
|
|
293
|
-
<VirtualizerScrollInstanceProvider
|
|
293
|
+
<VirtualizerScrollInstanceProvider
|
|
294
|
+
scrollElement={boundary.current}
|
|
295
|
+
containerElement={boundary}
|
|
296
|
+
>
|
|
294
297
|
<DocumentPaneProvider
|
|
295
298
|
paneKey={documentPane.paneKey}
|
|
296
299
|
index={documentPane.index}
|
|
@@ -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
|
|
@@ -121,16 +124,19 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
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
|
|
@@ -203,7 +210,7 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
203
210
|
)
|
|
204
211
|
|
|
205
212
|
// If there are no instructions, we don't want to render the group
|
|
206
|
-
if (instructionsLength === 0) {
|
|
213
|
+
if (instructionsLength === 0 && !imageCaptionAction) {
|
|
207
214
|
return emptyAction
|
|
208
215
|
}
|
|
209
216
|
|
|
@@ -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'})
|