@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
@@ -1,8 +1,11 @@
1
- import {useClient, useCurrentUser, useSchema} from 'sanity'
1
+ import {Path, pathToString, useClient, useCurrentUser, useSchema} from 'sanity'
2
2
  import {useCallback, useMemo, useState} from 'react'
3
3
  import {serializeSchema} from './schemas/serialize/serializeSchema'
4
4
  import {useToast} from '@sanity/ui'
5
5
  import {SanityClient} from '@sanity/client'
6
+ import {FieldLanguageMap} from './translate/paths'
7
+ import {documentRootKey} from './types'
8
+ import {ConditionalMemberState} from './helpers/conditionalMembers'
6
9
 
7
10
  export interface UserTextInstance {
8
11
  blockKey: string
@@ -17,6 +20,7 @@ export interface RunInstructionRequest {
17
20
  instructionKey: string
18
21
  userId?: string
19
22
  userTexts?: UserTextInstance[]
23
+ conditionalMembers?: ConditionalMemberState[]
20
24
  }
21
25
 
22
26
  export interface InstructStatus {
@@ -25,8 +29,20 @@ export interface InstructStatus {
25
29
  validToken: boolean
26
30
  }
27
31
 
32
+ export interface TranslateRequest {
33
+ documentId: string
34
+ translatePath: Path
35
+ languagePath?: string
36
+ fieldLanguageMap?: FieldLanguageMap[]
37
+ conditionalMembers?: ConditionalMemberState[]
38
+ }
39
+
28
40
  const basePath = '/assist/tasks/instruction'
29
41
 
42
+ export function canUseAssist(status: InstructStatus | undefined) {
43
+ return status?.enabled && status.initialized && status.validToken
44
+ }
45
+
30
46
  export function useApiClient(customApiClient?: (defaultClient: SanityClient) => SanityClient) {
31
47
  const client = useClient({apiVersion: '2023-06-05'})
32
48
  return useMemo(
@@ -35,6 +51,69 @@ export function useApiClient(customApiClient?: (defaultClient: SanityClient) =>
35
51
  )
36
52
  }
37
53
 
54
+ export function useTranslate(apiClient: SanityClient) {
55
+ const [loading, setLoading] = useState(false)
56
+ const user = useCurrentUser()
57
+ const schema = useSchema()
58
+ const types = useMemo(() => serializeSchema(schema, {leanFormat: true}), [schema])
59
+ const toast = useToast()
60
+
61
+ const translate = useCallback(
62
+ ({
63
+ documentId,
64
+ languagePath,
65
+ translatePath,
66
+ fieldLanguageMap,
67
+ conditionalMembers,
68
+ }: TranslateRequest) => {
69
+ setLoading(true)
70
+
71
+ return apiClient
72
+ .request({
73
+ method: 'POST',
74
+ url: `/assist/tasks/translate/${apiClient.config().dataset}?projectId=${
75
+ apiClient.config().projectId
76
+ }`,
77
+ body: {
78
+ documentId,
79
+ types,
80
+ languagePath,
81
+ fieldLanguageMap,
82
+ conditionalMembers,
83
+ translatePath:
84
+ translatePath.length === 0 ? documentRootKey : pathToString(translatePath),
85
+ userId: user?.id,
86
+ },
87
+ })
88
+ .catch((e) => {
89
+ toast.push({
90
+ status: 'error',
91
+ title: 'Translate failed',
92
+ description: e.message,
93
+ })
94
+ setLoading(false)
95
+ throw e
96
+ })
97
+ .finally(() => {
98
+ // adding some artificial delay here
99
+ // server responds with 201 then proceeds; we dont need to allow spamming the button
100
+ setTimeout(() => {
101
+ setLoading(false)
102
+ }, 2000)
103
+ })
104
+ },
105
+ [setLoading, apiClient, toast, user, types]
106
+ )
107
+
108
+ return useMemo(
109
+ () => ({
110
+ translate,
111
+ loading,
112
+ }),
113
+ [translate, loading]
114
+ )
115
+ }
116
+
38
117
  export function useGenerateCaption(apiClient: SanityClient) {
39
118
  const [loading, setLoading] = useState(false)
40
119
  const user = useCurrentUser()
@@ -62,7 +141,7 @@ export function useGenerateCaption(apiClient: SanityClient) {
62
141
  .catch((e) => {
63
142
  toast.push({
64
143
  status: 'error',
65
- title: 'Generate caption failed',
144
+ title: 'Generate image description failed',
66
145
  description: e.message,
67
146
  })
68
147
  setLoading(false)
@@ -88,6 +167,59 @@ export function useGenerateCaption(apiClient: SanityClient) {
88
167
  )
89
168
  }
90
169
 
170
+ export function useGenerateImage(apiClient: SanityClient) {
171
+ const [loading, setLoading] = useState(false)
172
+ const user = useCurrentUser()
173
+ const schema = useSchema()
174
+ const types = useMemo(() => serializeSchema(schema, {leanFormat: true}), [schema])
175
+ const toast = useToast()
176
+
177
+ const generateImage = useCallback(
178
+ ({path, documentId}: {path: string; documentId: string}) => {
179
+ setLoading(true)
180
+
181
+ return apiClient
182
+ .request({
183
+ method: 'POST',
184
+ url: `/assist/tasks/generate-image/${apiClient.config().dataset}?projectId=${
185
+ apiClient.config().projectId
186
+ }`,
187
+ body: {
188
+ path,
189
+ documentId,
190
+ types,
191
+ userId: user?.id,
192
+ },
193
+ })
194
+ .catch((e) => {
195
+ toast.push({
196
+ status: 'error',
197
+ title: 'Generate image from prompt failed',
198
+ description: e.message,
199
+ })
200
+ setLoading(false)
201
+ throw e
202
+ })
203
+ .finally(() => {
204
+ // adding some artificial delay here
205
+ // server responds with 201 then proceeds; we dont need to allow spamming the button
206
+ setTimeout(() => {
207
+ setLoading(false)
208
+ }, 2000)
209
+ })
210
+ },
211
+ [setLoading, apiClient, toast, user, types]
212
+ )
213
+
214
+ return useMemo(
215
+ () => ({
216
+ generateImage,
217
+ loading,
218
+ }),
219
+ [generateImage, loading]
220
+ )
221
+ }
222
+
91
223
  export function useGetInstructStatus(apiClient: SanityClient) {
92
224
  const [loading, setLoading] = useState(true)
93
225
 
@@ -1,310 +0,0 @@
1
- import {SanityClient, SanityDocument, useClient} from 'sanity'
2
- import {useCallback, useEffect, useState} from 'react'
3
- import {Box, Button, Card, Dialog, Spinner, Stack, Text, useToast} from '@sanity/ui'
4
- import {CheckmarkIcon} from '@sanity/icons'
5
- import {
6
- LegacyAssistDocument,
7
- legacyAssistDocumentIdPrefix,
8
- legacyAssistDocumentTypeName,
9
- legacyAssistStatusDocumentTypeName,
10
- LegacyContextBlock,
11
- legacyContextDocumentTypeName,
12
- LegacyFieldRef,
13
- LegacyPromptBlock,
14
- LegacyPromptTextBlock,
15
- LegacyUserInputBlock,
16
- } from '../legacy-types'
17
- import {assistDocumentId} from '../helpers/ids'
18
- import {
19
- assistDocumentIdPrefix,
20
- assistDocumentTypeName,
21
- AssistField,
22
- assistFieldTypeName,
23
- contextDocumentTypeName,
24
- fieldReferenceTypeName,
25
- InlinePromptBlock,
26
- instructionContextTypeName,
27
- instructionTypeName,
28
- PromptBlock,
29
- userInputTypeName,
30
- } from '../types'
31
- import {PortableTextSpan} from '@portabletext/types'
32
- import {pluginTitle} from '../constants'
33
-
34
- const NO_ASSIST_DOCS: LegacyAssistDocument[] = []
35
- const NO_CONTEXT_DOCS: SanityDocument[] = []
36
- const NO_IDS: string[] = []
37
-
38
- interface MigratedContextDoc {
39
- _id: string
40
- _alphaId: string
41
- }
42
-
43
- type Task = (subtaskProgress: (percentage: number) => void) => Promise<void>
44
-
45
- export function AlphaMigration() {
46
- const [alphaAssistDocs, setAlphaAssistDocs] = useState(NO_ASSIST_DOCS)
47
- const [contextDocs, setContextDocs] = useState(NO_CONTEXT_DOCS)
48
- const [staleStatusDocIds, setStaleStatusDocs] = useState(NO_IDS)
49
- const [error, setError] = useState<Error | undefined>(undefined)
50
- const [progress, setProgress] = useState<number | undefined>(undefined)
51
- const toast = useToast()
52
- const client = useClient({apiVersion: '2023-06-01'})
53
-
54
- useEffect(() => {
55
- let canUpdate = true
56
- client
57
- .fetch<{
58
- assistDocs?: LegacyAssistDocument[]
59
- staleStatusDocIds?: string[]
60
- contextDocs?: SanityDocument[]
61
- }>(
62
- `
63
- {
64
- "assistDocs": *[_type=="${legacyAssistDocumentTypeName}"],
65
- "staleStatusDocIds": *[_type=="${legacyAssistStatusDocumentTypeName}"]._id,
66
- "contextDocs": *[_type=="${legacyContextDocumentTypeName}"],
67
- }
68
- `
69
- )
70
- .then((result) => {
71
- if (!canUpdate || !result) {
72
- return
73
- }
74
- setAlphaAssistDocs(result?.assistDocs ?? NO_ASSIST_DOCS)
75
- setStaleStatusDocs(result?.staleStatusDocIds ?? NO_IDS)
76
- setContextDocs(result?.contextDocs ?? NO_CONTEXT_DOCS)
77
- })
78
- return () => {
79
- canUpdate = false
80
- }
81
- }, [client, setAlphaAssistDocs, setStaleStatusDocs, setContextDocs])
82
-
83
- const convert = useCallback(async () => {
84
- try {
85
- setProgress(0.0001)
86
-
87
- const tasks: Task[] = [
88
- () => convertContextDocs(client, contextDocs),
89
- (subtaskProgress) => deleteDocs(client, staleStatusDocIds, subtaskProgress),
90
- (subtaskProgress) => convertDocs(client, alphaAssistDocs, subtaskProgress),
91
- (subtaskProgress) =>
92
- deleteDocs(
93
- client,
94
- contextDocs.map((d) => d._id),
95
- subtaskProgress
96
- ),
97
- ]
98
-
99
- const taskSize = 1 / tasks.length
100
- for (let i = 0; i < tasks.length; i++) {
101
- const startProgress = i / tasks.length
102
- await tasks[i]((subProgress) => setProgress(startProgress + subProgress * taskSize))
103
- setProgress((i + 1) / tasks.length)
104
- }
105
-
106
- setProgress(1)
107
- setAlphaAssistDocs(NO_ASSIST_DOCS)
108
- setContextDocs(NO_CONTEXT_DOCS)
109
- setStaleStatusDocs(NO_IDS)
110
- toast.push({
111
- title: `Converted instructions to new format.`,
112
- status: 'success',
113
- closable: true,
114
- })
115
- } catch (e: any) {
116
- console.error(e)
117
- toast.push({
118
- title: `An error occurred`,
119
- status: 'error',
120
- closable: true,
121
- })
122
- setError(e)
123
- setProgress(undefined)
124
- }
125
- }, [contextDocs, client, alphaAssistDocs, staleStatusDocIds, setProgress, toast])
126
-
127
- if (
128
- (alphaAssistDocs.length || staleStatusDocIds.length || contextDocs.length) &&
129
- (!progress || progress < 1)
130
- ) {
131
- return (
132
- <Dialog id="outdated-assist-docs" header={pluginTitle}>
133
- <Card padding={3}>
134
- <Stack space={4} style={{maxWidth: 500}}>
135
- <Text size={1}>
136
- It seems like this workspace contains documents from an{' '}
137
- <strong>older version of {pluginTitle}</strong>.
138
- </Text>
139
- <Text size={1}>Cleanup is required.</Text>
140
- {error ? (
141
- <Card padding={2} tone="critical" border>
142
- <Text size={1}>An error occurred. See console for details.</Text>{' '}
143
- </Card>
144
- ) : null}
145
- <Button
146
- icon={
147
- progress ? (
148
- <Box style={{marginTop: 5}}>
149
- <Spinner />
150
- </Box>
151
- ) : (
152
- CheckmarkIcon
153
- )
154
- }
155
- disabled={!!progress}
156
- text={progress ? `${Math.floor(progress * 100)}%` : 'Ok, convert to new format!'}
157
- tone="primary"
158
- onClick={convert}
159
- />
160
- </Stack>
161
- </Card>
162
- </Dialog>
163
- )
164
- }
165
-
166
- return null
167
- }
168
-
169
- async function deleteDocs(
170
- client: SanityClient,
171
- ids: string[],
172
- updateProgress: (percentage: number) => void
173
- ) {
174
- const chunkSize = 50
175
- for (let i = 0; i < ids.length; i += chunkSize) {
176
- const progressCount = Math.min(ids.length, i + chunkSize)
177
- const chunk = ids.slice(i, progressCount)
178
- const trans = client.transaction()
179
- chunk.forEach((id) => trans.delete(id))
180
- await trans.commit()
181
- updateProgress(progressCount / ids.length)
182
- }
183
- }
184
-
185
- async function convertContextDocs(client: SanityClient, docs: SanityDocument[]) {
186
- const trans = client.transaction()
187
-
188
- for (const doc of docs) {
189
- const {_id, _type, ...rest} = doc
190
- trans.createOrReplace({
191
- ...rest,
192
- _id: `port.${_id}`,
193
- _alphaId: _id,
194
- _type: contextDocumentTypeName,
195
- })
196
- }
197
-
198
- await trans.commit()
199
- }
200
-
201
- async function convertDocs(
202
- client: SanityClient,
203
- docs: LegacyAssistDocument[],
204
- updateProgress: (percentage: number) => void
205
- ) {
206
- const chunkSize = 10
207
- for (let i = 0; i < docs.length; i += chunkSize) {
208
- const progressCount = Math.min(docs.length, i + chunkSize)
209
- const chunk = docs.slice(i, progressCount)
210
-
211
- const trans = client.transaction()
212
- const contextDocs: MigratedContextDoc[] = await client.fetch(
213
- `*[_type=="${contextDocumentTypeName}" && _alphaId != null]{_id, _alphaId}`
214
- )
215
-
216
- chunk.forEach((oldDoc) => {
217
- const documentType = oldDoc._id.replace(
218
- new RegExp(`^(${legacyAssistDocumentIdPrefix}|${assistDocumentIdPrefix})`),
219
- ''
220
- )
221
-
222
- const id = assistDocumentId(documentType)
223
-
224
- const fields: AssistField[] = (oldDoc.fields ?? [])
225
- .filter((field) => field.instructions?.length)
226
- .map((oldField) => {
227
- const instructions = (oldField.instructions ?? []).map((inst) => {
228
- // eslint-disable-next-line max-nested-callbacks
229
- const prompt = (inst.prompt ?? []).map((block) => {
230
- return mapBlock(block, contextDocs) as PromptBlock
231
- })
232
- return {
233
- ...inst,
234
- _type: instructionTypeName,
235
- prompt,
236
- }
237
- })
238
- return {
239
- ...oldField,
240
- _type: assistFieldTypeName,
241
- instructions,
242
- }
243
- })
244
-
245
- if (fields.length) {
246
- trans.createOrReplace({
247
- _id: id,
248
- _type: assistDocumentTypeName,
249
- fields,
250
- })
251
- }
252
- trans.delete(oldDoc._id)
253
- })
254
- await trans.commit()
255
- updateProgress(progressCount / docs.length)
256
- }
257
- }
258
-
259
- type Blocks = LegacyPromptBlock | LegacyPromptTextBlock | PortableTextSpan
260
-
261
- function isFieldRef(block: Blocks): block is LegacyFieldRef {
262
- return block._type === 'sanity.ai.prompt.fieldRef'
263
- }
264
-
265
- function isContext(block: Blocks): block is LegacyContextBlock {
266
- return block._type === 'sanity.ai.prompt.context'
267
- }
268
-
269
- function isUserInput(block: Blocks): block is LegacyUserInputBlock {
270
- return block._type === 'sanity.ai.prompt.userInput'
271
- }
272
-
273
- function isSpan(block: Blocks): block is PortableTextSpan {
274
- return block._type === 'span'
275
- }
276
-
277
- function mapBlock(
278
- block: Blocks,
279
- migratedContexts: MigratedContextDoc[]
280
- ): PromptBlock | InlinePromptBlock {
281
- if (isFieldRef(block)) {
282
- return {...block, _type: fieldReferenceTypeName}
283
- }
284
- if (isUserInput(block)) {
285
- return {...block, _type: userInputTypeName}
286
- }
287
- if (isContext(block)) {
288
- const newBlock = {
289
- ...block,
290
- _type: instructionContextTypeName,
291
- reference: {
292
- _type: 'reference',
293
- _ref:
294
- migratedContexts.find((c) => c._alphaId === block.reference?._ref)?._id ??
295
- block.reference?._ref,
296
- },
297
- } as const
298
- return newBlock
299
- }
300
- if (isSpan(block)) {
301
- return block
302
- }
303
- const textBlock = block
304
- return {
305
- ...textBlock,
306
- children: (textBlock.children ?? []).map(
307
- (child) => mapBlock(child, migratedContexts) as InlinePromptBlock
308
- ),
309
- }
310
- }
@@ -1,72 +0,0 @@
1
- import {SanityDocument} from 'sanity'
2
- import {PortableTextBlock, PortableTextMarkDefinition, PortableTextSpan} from '@portabletext/types'
3
-
4
- //id prefixes
5
- export const legacyAssistDocumentIdPrefix = 'sanity.ai.'
6
-
7
- export const legacyAssistDocumentTypeName = 'sanity.ai.docType' as const
8
- const aiFieldTypeName = 'sanity.ai.docType.field' as const
9
- const instructionTypeName = 'sanity.ai.field.instruction' as const
10
-
11
- const userInputTypeName = 'sanity.ai.prompt.userInput' as const
12
- const promptContextTypeName = 'sanity.ai.prompt.context' as const
13
- const fieldReferenceTypeName = 'sanity.ai.prompt.fieldRef' as const
14
-
15
- export const legacyContextDocumentTypeName = 'ai.instruction.context' as const
16
-
17
- export const legacyAssistStatusDocumentTypeName = 'sanity.ai.instructionStatus' as const
18
-
19
- export interface FieldPrompts {
20
- _key: string
21
- _type: typeof aiFieldTypeName
22
- path?: string
23
- instructions?: LegacyInstruction[]
24
- }
25
-
26
- export interface LegacyAssistDocument extends SanityDocument {
27
- fields: FieldPrompts[]
28
- }
29
-
30
- export interface LegacyFieldRef extends PortableTextMarkDefinition {
31
- _type: typeof fieldReferenceTypeName
32
- path?: string
33
- }
34
-
35
- export interface LegacyContextBlock {
36
- _type: typeof promptContextTypeName
37
- reference?: {
38
- _type: 'reference'
39
- _ref?: string
40
- }
41
- }
42
-
43
- export interface LegacyUserInputBlock {
44
- _type: typeof userInputTypeName
45
- _key: string
46
- message?: string
47
- description?: string
48
- }
49
-
50
- export type LegacyPromptTextBlock = PortableTextBlock<
51
- never,
52
- PortableTextSpan | LegacyFieldRef | LegacyUserInputBlock | LegacyContextBlock,
53
- 'normal',
54
- never
55
- >
56
-
57
- export type LegacyPromptBlock =
58
- | LegacyPromptTextBlock
59
- | LegacyFieldRef
60
- | LegacyContextBlock
61
- | LegacyUserInputBlock
62
-
63
- export interface LegacyInstruction {
64
- _key: string
65
- _type: typeof instructionTypeName
66
- prompt?: LegacyPromptBlock[]
67
-
68
- icon?: string
69
- userId?: string
70
- title?: string
71
- placeholder?: string
72
- }