@sanity/assist 1.2.14 → 1.2.15-lang.2
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 +392 -6
- package/dist/index.d.ts +170 -3
- package/dist/index.esm.js +2019 -125
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2013 -119
- package/dist/index.js.map +1 -1
- package/package.json +12 -11
- package/src/_lib/form/DocumentForm.tsx +1 -1
- 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 +5 -4
- package/src/assistFormComponents/AssistFormBlock.tsx +2 -3
- package/src/assistFormComponents/validation/listItem.tsx +2 -2
- package/src/assistInspector/FieldAutocomplete.tsx +1 -0
- package/src/assistInspector/InstructionTaskHistoryButton.tsx +2 -3
- package/src/assistInspector/helpers.ts +7 -9
- package/src/assistLayout/AssistLayout.tsx +9 -6
- package/src/fieldActions/assistFieldActions.tsx +21 -8
- package/src/fieldActions/translateActions.tsx +141 -0
- package/src/helpers/assistSupported.ts +1 -1
- package/src/node_modules/.vitest/results.json +1 -0
- package/src/plugin.tsx +6 -0
- package/src/presence/AssistAvatar.tsx +1 -1
- package/src/schemas/assistDocumentSchema.tsx +39 -0
- package/src/schemas/serialize/serializeSchema.ts +6 -6
- package/src/schemas/typeDefExtensions.ts +12 -1
- package/src/translate/FieldTranslationProvider.tsx +267 -0
- package/src/translate/getLanguageParams.ts +26 -0
- package/src/translate/paths.test.ts +87 -0
- package/src/translate/paths.ts +151 -0
- package/src/translate/types.ts +159 -0
- package/src/types.ts +21 -2
- package/src/useApiClient.ts +63 -0
|
@@ -3,6 +3,7 @@ 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'
|
|
@@ -24,6 +25,7 @@ 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 './translateActions'
|
|
27
29
|
|
|
28
30
|
function node(node: DocumentFieldActionItem | DocumentFieldActionGroup) {
|
|
29
31
|
return node
|
|
@@ -47,6 +49,7 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
47
49
|
documentSchemaType,
|
|
48
50
|
documentId,
|
|
49
51
|
selectedPath,
|
|
52
|
+
assistableDocumentId,
|
|
50
53
|
} =
|
|
51
54
|
// document field actions do not have access to the document context
|
|
52
55
|
// conditional hook _should_ be safe here since the logical path will be stable
|
|
@@ -89,7 +92,13 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
89
92
|
const isSelected = isInspectorOpen && isPathSelected
|
|
90
93
|
|
|
91
94
|
const imageCaptionAction = generateCaptionsActions.useAction(props)
|
|
92
|
-
|
|
95
|
+
const translateAction = translateActions.useAction(
|
|
96
|
+
typed<TranslateProps>({
|
|
97
|
+
...props,
|
|
98
|
+
documentId: assistableDocumentId,
|
|
99
|
+
documentIsAssistable,
|
|
100
|
+
})
|
|
101
|
+
)
|
|
93
102
|
const manageInstructions = useCallback(
|
|
94
103
|
() =>
|
|
95
104
|
isSelected
|
|
@@ -134,7 +143,7 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
134
143
|
)
|
|
135
144
|
|
|
136
145
|
const runInstructionsGroup = useMemo(() => {
|
|
137
|
-
return instructions?.length || imageCaptionAction
|
|
146
|
+
return instructions?.length || imageCaptionAction || translateAction
|
|
138
147
|
? node({
|
|
139
148
|
type: 'group',
|
|
140
149
|
icon: () => null,
|
|
@@ -151,7 +160,7 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
151
160
|
})
|
|
152
161
|
),
|
|
153
162
|
imageCaptionAction,
|
|
154
|
-
].filter(
|
|
163
|
+
].filter((a): a is DocumentFieldActionItem => !!a),
|
|
155
164
|
expanded: true,
|
|
156
165
|
})
|
|
157
166
|
: undefined
|
|
@@ -163,6 +172,7 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
163
172
|
documentIsNew,
|
|
164
173
|
assistSupported,
|
|
165
174
|
imageCaptionAction,
|
|
175
|
+
translateAction,
|
|
166
176
|
])
|
|
167
177
|
|
|
168
178
|
const instructionsLength = instructions?.length || 0
|
|
@@ -185,12 +195,14 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
185
195
|
type: 'group',
|
|
186
196
|
icon: SparklesIcon,
|
|
187
197
|
title: pluginTitleShort,
|
|
188
|
-
children: [
|
|
189
|
-
|
|
190
|
-
|
|
198
|
+
children: [
|
|
199
|
+
runInstructionsGroup,
|
|
200
|
+
translateAction,
|
|
201
|
+
assistSupported && manageInstructionsItem,
|
|
202
|
+
].filter((c): c is DocumentFieldActionItem | DocumentFieldActionGroup => !!c),
|
|
191
203
|
expanded: false,
|
|
192
204
|
renderAsButton: true,
|
|
193
|
-
hidden: !assistSupported && !imageCaptionAction,
|
|
205
|
+
hidden: !assistSupported && !imageCaptionAction && !translateAction,
|
|
194
206
|
}),
|
|
195
207
|
[
|
|
196
208
|
//documentIsNew,
|
|
@@ -198,6 +210,7 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
198
210
|
manageInstructionsItem,
|
|
199
211
|
assistSupported,
|
|
200
212
|
imageCaptionAction,
|
|
213
|
+
translateAction,
|
|
201
214
|
]
|
|
202
215
|
)
|
|
203
216
|
|
|
@@ -216,7 +229,7 @@ export const assistFieldActions: DocumentFieldAction = {
|
|
|
216
229
|
)
|
|
217
230
|
|
|
218
231
|
// If there are no instructions, we don't want to render the group
|
|
219
|
-
if (instructionsLength === 0 && !imageCaptionAction) {
|
|
232
|
+
if (instructionsLength === 0 && !imageCaptionAction && !translateAction) {
|
|
220
233
|
return emptyAction
|
|
221
234
|
}
|
|
222
235
|
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
2
|
+
import {
|
|
3
|
+
DocumentFieldAction,
|
|
4
|
+
DocumentFieldActionGroup,
|
|
5
|
+
DocumentFieldActionItem,
|
|
6
|
+
DocumentFieldActionProps,
|
|
7
|
+
ObjectSchemaType,
|
|
8
|
+
} from 'sanity'
|
|
9
|
+
import {TranslateIcon} from '@sanity/icons'
|
|
10
|
+
import {useMemo, useRef} from 'react'
|
|
11
|
+
import {useApiClient, useTranslate} from '../useApiClient'
|
|
12
|
+
import {useAiAssistanceConfig} from '../assistLayout/AiAssistanceConfigContext'
|
|
13
|
+
import {Box, Spinner} from '@sanity/ui'
|
|
14
|
+
import {isAssistSupported} from '../helpers/assistSupported'
|
|
15
|
+
import {useDocumentPane} from 'sanity/desk'
|
|
16
|
+
import {useFieldTranslation} from '../translate/FieldTranslationProvider'
|
|
17
|
+
import {useDraftDelayedTask} from '../assistDocument/RequestRunInstructionProvider'
|
|
18
|
+
import {getLanguageParams} from '../translate/getLanguageParams'
|
|
19
|
+
|
|
20
|
+
function node(node: DocumentFieldActionItem | DocumentFieldActionGroup) {
|
|
21
|
+
return node
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type TranslateProps = DocumentFieldActionProps & {documentIsAssistable?: boolean}
|
|
25
|
+
export const translateActions: DocumentFieldAction = {
|
|
26
|
+
name: 'sanity-assist-translate',
|
|
27
|
+
useAction(props: TranslateProps) {
|
|
28
|
+
const {config} = useAiAssistanceConfig()
|
|
29
|
+
const apiClient = useApiClient(config?.__customApiClient)
|
|
30
|
+
|
|
31
|
+
const isDocumentLevel = props.path.length === 0
|
|
32
|
+
const {schemaType, documentId, documentIsAssistable} = props
|
|
33
|
+
|
|
34
|
+
const fieldTransEnabled = config.translate?.field?.documentTypes?.includes(schemaType.name)
|
|
35
|
+
const docTransTypes = config.translate?.document?.documentTypes
|
|
36
|
+
const documentTranslationEnabled =
|
|
37
|
+
isDocumentLevel &&
|
|
38
|
+
((!docTransTypes && isAssistSupported(schemaType)) ||
|
|
39
|
+
docTransTypes?.includes(schemaType.name))
|
|
40
|
+
|
|
41
|
+
// these checks are stable (ie, does not change after mount), so not breaking rules of hooks
|
|
42
|
+
if (documentTranslationEnabled || fieldTransEnabled) {
|
|
43
|
+
const {value: documentValue, onChange: documentOnChange} = useDocumentPane()
|
|
44
|
+
const docRef = useRef(documentValue)
|
|
45
|
+
|
|
46
|
+
const translationApi = useTranslate(apiClient)
|
|
47
|
+
const translate = useDraftDelayedTask({
|
|
48
|
+
documentOnChange,
|
|
49
|
+
isDocAssistable: documentIsAssistable ?? false,
|
|
50
|
+
task: translationApi.translate,
|
|
51
|
+
})
|
|
52
|
+
docRef.current = documentValue
|
|
53
|
+
const languagePath = config.translate?.document?.languageField
|
|
54
|
+
|
|
55
|
+
//const {value: languageId} = extractWithPath(languagePath, documentValue)[0] ?? {}
|
|
56
|
+
// if this is true, it is stable, and not breaking rules of hooks
|
|
57
|
+
const translateDocumentAction = useMemo(
|
|
58
|
+
() =>
|
|
59
|
+
languagePath && documentTranslationEnabled
|
|
60
|
+
? node({
|
|
61
|
+
type: 'action',
|
|
62
|
+
icon: translationApi.loading
|
|
63
|
+
? () => (
|
|
64
|
+
<Box style={{height: 17}}>
|
|
65
|
+
<Spinner style={{transform: 'translateY(6px)'}} />
|
|
66
|
+
</Box>
|
|
67
|
+
)
|
|
68
|
+
: TranslateIcon,
|
|
69
|
+
title: `Translate document`,
|
|
70
|
+
onAction: () => {
|
|
71
|
+
if (translationApi.loading || !languagePath || !documentId) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
translate({languagePath, documentId: documentId ?? ''})
|
|
75
|
+
},
|
|
76
|
+
renderAsButton: true,
|
|
77
|
+
disabled: translationApi.loading,
|
|
78
|
+
})
|
|
79
|
+
: undefined,
|
|
80
|
+
[languagePath, translate, documentId, translationApi.loading, documentTranslationEnabled]
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const fieldTranslate = useFieldTranslation()
|
|
84
|
+
const openFieldTranslation = useDraftDelayedTask({
|
|
85
|
+
documentOnChange,
|
|
86
|
+
isDocAssistable: documentIsAssistable ?? false,
|
|
87
|
+
task: fieldTranslate.openFieldTranslation,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const translateFieldsAction = useMemo(
|
|
91
|
+
() =>
|
|
92
|
+
fieldTransEnabled
|
|
93
|
+
? node({
|
|
94
|
+
type: 'action',
|
|
95
|
+
icon: fieldTranslate.translationLoading
|
|
96
|
+
? () => (
|
|
97
|
+
<Box style={{height: 17}}>
|
|
98
|
+
<Spinner style={{transform: 'translateY(6px)'}} />
|
|
99
|
+
</Box>
|
|
100
|
+
)
|
|
101
|
+
: TranslateIcon,
|
|
102
|
+
title: `Translate fields`,
|
|
103
|
+
onAction: () => {
|
|
104
|
+
if (fieldTranslate.translationLoading || !documentId) {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
openFieldTranslation({
|
|
108
|
+
document: docRef.current,
|
|
109
|
+
documentSchema: schemaType as ObjectSchemaType,
|
|
110
|
+
})
|
|
111
|
+
},
|
|
112
|
+
renderAsButton: true,
|
|
113
|
+
disabled: fieldTranslate.translationLoading,
|
|
114
|
+
})
|
|
115
|
+
: undefined,
|
|
116
|
+
[
|
|
117
|
+
openFieldTranslation,
|
|
118
|
+
schemaType,
|
|
119
|
+
documentId,
|
|
120
|
+
fieldTranslate.translationLoading,
|
|
121
|
+
fieldTransEnabled,
|
|
122
|
+
]
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
126
|
+
return useMemo(() => {
|
|
127
|
+
return node({
|
|
128
|
+
type: 'group',
|
|
129
|
+
icon: () => null,
|
|
130
|
+
title: 'Translate',
|
|
131
|
+
children: [translateDocumentAction, translateFieldsAction].filter(
|
|
132
|
+
(c): c is DocumentFieldActionItem => !!c
|
|
133
|
+
),
|
|
134
|
+
expanded: true,
|
|
135
|
+
})
|
|
136
|
+
}, [translateDocumentAction, translateFieldsAction])
|
|
137
|
+
}
|
|
138
|
+
// works but not supported by types
|
|
139
|
+
return undefined as unknown as DocumentFieldActionItem
|
|
140
|
+
},
|
|
141
|
+
}
|
|
@@ -43,7 +43,7 @@ function isUnsupportedType(type: SchemaType) {
|
|
|
43
43
|
type.jsonType === 'number' ||
|
|
44
44
|
type.name === 'sanity.imageCrop' ||
|
|
45
45
|
type.name === 'sanity.imageHotspot' ||
|
|
46
|
-
isType(type, 'reference') ||
|
|
46
|
+
(isType(type, 'reference') && !type?.options?.aiWritingAssistance?.embeddingsIndex) ||
|
|
47
47
|
isType(type, 'crossDatasetReference') ||
|
|
48
48
|
isType(type, 'slug') ||
|
|
49
49
|
isType(type, 'url') ||
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"0.32.2","results":[[":schemas/serialize/serializeSchema.test.ts",{"duration":12,"failed":false}],[":translate/paths.test.ts",{"duration":6,"failed":false}]]}
|
package/src/plugin.tsx
CHANGED
|
@@ -15,8 +15,11 @@ import {createAssistDocumentPresence} from './presence/AssistDocumentPresence'
|
|
|
15
15
|
import {isSchemaAssistEnabled} from './helpers/assistSupported'
|
|
16
16
|
import {isImage} from './helpers/typeUtils'
|
|
17
17
|
import {ImageContextProvider} from './components/ImageContext'
|
|
18
|
+
import {TranslationConfig} from './translate/types'
|
|
18
19
|
|
|
19
20
|
export interface AssistPluginConfig {
|
|
21
|
+
translate?: TranslationConfig
|
|
22
|
+
|
|
20
23
|
/**
|
|
21
24
|
* Set this to false to disable model migration from the alpha version of this plugin
|
|
22
25
|
*/
|
|
@@ -36,6 +39,9 @@ export const assist = definePlugin<AssistPluginConfig | void>((config) => {
|
|
|
36
39
|
schema: {
|
|
37
40
|
types: schemaTypes,
|
|
38
41
|
},
|
|
42
|
+
i18n: {
|
|
43
|
+
bundles: [{}],
|
|
44
|
+
},
|
|
39
45
|
|
|
40
46
|
document: {
|
|
41
47
|
inspectors: (prev, context) => {
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
instructionContextTypeName,
|
|
19
19
|
instructionTaskTypeName,
|
|
20
20
|
instructionTypeName,
|
|
21
|
+
outputFieldTypeName,
|
|
22
|
+
outputTypeTypeName,
|
|
21
23
|
promptTypeName,
|
|
22
24
|
userInputTypeName,
|
|
23
25
|
} from '../types'
|
|
@@ -37,6 +39,8 @@ import {PromptInput} from '../assistDocument/components/instruction/PromptInput'
|
|
|
37
39
|
import {instructionGuideUrl} from '../constants'
|
|
38
40
|
import {InstructionsArrayField} from '../assistDocument/components/InstructionsArrayField'
|
|
39
41
|
import {getFieldRefsWithDocument} from '../assistInspector/helpers'
|
|
42
|
+
import {InstructionOutputField} from '../assistDocument/components/instruction/InstructionOutputField'
|
|
43
|
+
import {InstructionOutputInput} from '../assistDocument/components/instruction/InstructionOutputInput'
|
|
40
44
|
|
|
41
45
|
export const fieldReference = defineType({
|
|
42
46
|
type: 'object',
|
|
@@ -329,6 +333,41 @@ export const instruction = defineType({
|
|
|
329
333
|
return context.currentUser?.id ?? ''
|
|
330
334
|
},
|
|
331
335
|
}),
|
|
336
|
+
defineField({
|
|
337
|
+
type: 'array',
|
|
338
|
+
name: 'output',
|
|
339
|
+
title: 'Output filter',
|
|
340
|
+
components: {
|
|
341
|
+
input: InstructionOutputInput,
|
|
342
|
+
field: InstructionOutputField,
|
|
343
|
+
},
|
|
344
|
+
of: [
|
|
345
|
+
defineArrayMember({
|
|
346
|
+
type: 'object' as const,
|
|
347
|
+
name: outputFieldTypeName,
|
|
348
|
+
title: 'Output field',
|
|
349
|
+
fields: [
|
|
350
|
+
{
|
|
351
|
+
type: 'string',
|
|
352
|
+
name: 'path',
|
|
353
|
+
title: 'Path',
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
}),
|
|
357
|
+
defineArrayMember({
|
|
358
|
+
type: 'object' as const,
|
|
359
|
+
name: outputTypeTypeName,
|
|
360
|
+
title: 'Output type',
|
|
361
|
+
fields: [
|
|
362
|
+
{
|
|
363
|
+
type: 'string',
|
|
364
|
+
name: 'type',
|
|
365
|
+
title: 'Type',
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
}),
|
|
369
|
+
],
|
|
370
|
+
}),
|
|
332
371
|
],
|
|
333
372
|
})
|
|
334
373
|
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
ArraySchemaType,
|
|
3
3
|
ImageOptions,
|
|
4
4
|
ObjectSchemaType,
|
|
5
|
+
ReferenceOptions,
|
|
5
6
|
ReferenceSchemaType,
|
|
6
7
|
Schema,
|
|
7
8
|
SchemaType,
|
|
@@ -78,13 +79,12 @@ function getBaseFields(
|
|
|
78
79
|
typeName: string,
|
|
79
80
|
options: Options | undefined
|
|
80
81
|
) {
|
|
81
|
-
const
|
|
82
|
+
const schemaOptions = removeUndef({
|
|
83
|
+
imagePromptField: (type.options as ImageOptions)?.imagePromptField,
|
|
84
|
+
embeddingsIndex: (type.options as ReferenceOptions)?.aiWritingAssistance?.embeddingsIndex,
|
|
85
|
+
})
|
|
82
86
|
return removeUndef({
|
|
83
|
-
options:
|
|
84
|
-
? {
|
|
85
|
-
imagePromptField: imagePromptField,
|
|
86
|
-
}
|
|
87
|
-
: undefined,
|
|
87
|
+
options: Object.keys(schemaOptions).length ? schemaOptions : undefined,
|
|
88
88
|
values: Array.isArray(type?.options?.list)
|
|
89
89
|
? type?.options?.list.map((v: string | {value: string; title: string}) =>
|
|
90
90
|
typeof v === 'string' ? v : v.value ?? `${v.title}`
|
|
@@ -22,7 +22,18 @@ declare module 'sanity' {
|
|
|
22
22
|
}
|
|
23
23
|
interface NumberOptions extends AssistOptions {}
|
|
24
24
|
interface ObjectOptions extends AssistOptions {}
|
|
25
|
-
interface ReferenceBaseOptions
|
|
25
|
+
interface ReferenceBaseOptions {
|
|
26
|
+
aiWritingAssistance?: {
|
|
27
|
+
/** Set to true to disable assistance for this field or type */
|
|
28
|
+
exclude?: boolean
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* When set, the reference field will allow instructions to be added to it.
|
|
32
|
+
* Should be the name of the embeddings-index where assist will look for contextually relevant documents
|
|
33
|
+
* */
|
|
34
|
+
embeddingsIndex?: string
|
|
35
|
+
}
|
|
36
|
+
}
|
|
26
37
|
interface SlugOptions extends AssistOptions {}
|
|
27
38
|
interface StringOptions extends AssistOptions {}
|
|
28
39
|
interface TextOptions extends AssistOptions {}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
PropsWithChildren,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useId,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react'
|
|
10
|
+
import {ObjectSchemaType, SanityDocumentLike, useClient} from 'sanity'
|
|
11
|
+
import {useAiAssistanceConfig} from '../assistLayout/AiAssistanceConfigContext'
|
|
12
|
+
import {useApiClient, useTranslate} from '../useApiClient'
|
|
13
|
+
import {Box, Button, Checkbox, Dialog, Flex, Radio, Spinner, Stack, Text, Tooltip} from '@sanity/ui'
|
|
14
|
+
import {
|
|
15
|
+
defaultLanguageOutputs,
|
|
16
|
+
getDocumentMembersFlat,
|
|
17
|
+
getTranslationMap,
|
|
18
|
+
TranslationMap,
|
|
19
|
+
} from './paths'
|
|
20
|
+
import {PlayIcon} from '@sanity/icons'
|
|
21
|
+
import {Language} from './types'
|
|
22
|
+
import {getLanguageParams} from './getLanguageParams'
|
|
23
|
+
|
|
24
|
+
export interface FieldTranslationContextValue {
|
|
25
|
+
openFieldTranslation: (args: {
|
|
26
|
+
document: SanityDocumentLike
|
|
27
|
+
documentSchema: ObjectSchemaType
|
|
28
|
+
}) => void
|
|
29
|
+
translationLoading: boolean
|
|
30
|
+
//loadLanguages: (document: SanityDocumentLike, documentSchema: ObjectSchemaType) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const FieldTranslationContext = createContext<FieldTranslationContextValue>({
|
|
34
|
+
openFieldTranslation: () => {},
|
|
35
|
+
translationLoading: false,
|
|
36
|
+
//loadLanguages: () => {},
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
export function useFieldTranslation() {
|
|
40
|
+
return useContext(FieldTranslationContext)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function FieldTranslationProvider(props: PropsWithChildren<{}>) {
|
|
44
|
+
const {config: assistConfig} = useAiAssistanceConfig()
|
|
45
|
+
const apiClient = useApiClient(assistConfig.__customApiClient)
|
|
46
|
+
const config = assistConfig.translate?.field
|
|
47
|
+
const {translate: runTranslate} = useTranslate(apiClient)
|
|
48
|
+
|
|
49
|
+
const [dialogOpen, setDialogOpen] = useState(false)
|
|
50
|
+
|
|
51
|
+
const [document, setDocument] = useState<SanityDocumentLike | undefined>()
|
|
52
|
+
const [documentSchema, setDocumentSchema] = useState<ObjectSchemaType | undefined>()
|
|
53
|
+
const [languages, setLanguages] = useState<Language[] | undefined>()
|
|
54
|
+
const [fromLanguage, setFromLanguage] = useState<Language | undefined>(undefined)
|
|
55
|
+
const [toLanguages, setToLanguages] = useState<Language[] | undefined>(undefined)
|
|
56
|
+
const [translationMap, setTranslationMap] = useState<TranslationMap[] | undefined>()
|
|
57
|
+
|
|
58
|
+
const close = useCallback(() => {
|
|
59
|
+
setDialogOpen(false)
|
|
60
|
+
setLanguages(undefined)
|
|
61
|
+
setDocument(undefined)
|
|
62
|
+
setDocument(undefined)
|
|
63
|
+
}, [])
|
|
64
|
+
const languageClient = useClient({apiVersion: config?.apiVersion ?? '2022-11-27'})
|
|
65
|
+
const documentId = document?._id
|
|
66
|
+
const id = useId()
|
|
67
|
+
|
|
68
|
+
const selectFromLanguage = useCallback(
|
|
69
|
+
(
|
|
70
|
+
from: Language,
|
|
71
|
+
languages: Language[] | undefined,
|
|
72
|
+
document: SanityDocumentLike | undefined,
|
|
73
|
+
documentSchema: ObjectSchemaType | undefined
|
|
74
|
+
) => {
|
|
75
|
+
setFromLanguage(from)
|
|
76
|
+
if (!document || !documentSchema || !languages) {
|
|
77
|
+
setTranslationMap(undefined)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const to = languages.filter((l) => l.id !== from?.id)
|
|
82
|
+
setToLanguages(to)
|
|
83
|
+
const fromId = from?.id
|
|
84
|
+
const toIds = to?.map((l) => l.id) ?? []
|
|
85
|
+
const docMembers = getDocumentMembersFlat(document, documentSchema)
|
|
86
|
+
if (fromId && toIds?.length) {
|
|
87
|
+
const transMap = getTranslationMap(
|
|
88
|
+
documentSchema,
|
|
89
|
+
docMembers,
|
|
90
|
+
fromId,
|
|
91
|
+
toIds,
|
|
92
|
+
config?.translationOutputs ?? defaultLanguageOutputs
|
|
93
|
+
)
|
|
94
|
+
setTranslationMap(transMap)
|
|
95
|
+
} else {
|
|
96
|
+
setTranslationMap(undefined)
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
[config]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const toggleToLanguage = useCallback(
|
|
103
|
+
(
|
|
104
|
+
toggledLang: Language,
|
|
105
|
+
toLanguages: Language[] | undefined,
|
|
106
|
+
languages: Language[] | undefined
|
|
107
|
+
) => {
|
|
108
|
+
if (!languages) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
const wasSelected = !!toLanguages?.find((l) => l.id === toggledLang.id)
|
|
112
|
+
const newToLangs = languages.filter(
|
|
113
|
+
(anyLang) =>
|
|
114
|
+
!!toLanguages?.find(
|
|
115
|
+
(selectedLang) => toggledLang.id !== selectedLang.id && selectedLang.id === anyLang.id
|
|
116
|
+
) ||
|
|
117
|
+
(toggledLang.id === anyLang.id && !wasSelected)
|
|
118
|
+
)
|
|
119
|
+
setToLanguages(newToLangs)
|
|
120
|
+
},
|
|
121
|
+
[]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
const openFieldTranslation = useCallback(
|
|
125
|
+
async ({
|
|
126
|
+
document,
|
|
127
|
+
documentSchema,
|
|
128
|
+
}: {
|
|
129
|
+
document: SanityDocumentLike
|
|
130
|
+
documentSchema: ObjectSchemaType
|
|
131
|
+
}) => {
|
|
132
|
+
setDialogOpen(true)
|
|
133
|
+
const languageParams = getLanguageParams(config?.selectLanguageParams, document)
|
|
134
|
+
const languages: Language[] | undefined = await (typeof config?.languages === 'function'
|
|
135
|
+
? config?.languages(languageClient, languageParams)
|
|
136
|
+
: Promise.resolve(config?.languages))
|
|
137
|
+
setLanguages(languages)
|
|
138
|
+
setDocument(document)
|
|
139
|
+
setDocumentSchema(documentSchema)
|
|
140
|
+
const fromLanguage = languages?.[0]
|
|
141
|
+
if (fromLanguage) {
|
|
142
|
+
selectFromLanguage(fromLanguage, languages, document, documentSchema)
|
|
143
|
+
} else {
|
|
144
|
+
console.error('No languages available for selected language params', languageParams)
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
[selectFromLanguage, config, languageClient]
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
const contextValue: FieldTranslationContextValue = useMemo(() => {
|
|
151
|
+
return {
|
|
152
|
+
openFieldTranslation,
|
|
153
|
+
translationLoading: false,
|
|
154
|
+
}
|
|
155
|
+
}, [openFieldTranslation])
|
|
156
|
+
|
|
157
|
+
const runDisabled =
|
|
158
|
+
!fromLanguage || !toLanguages?.length || !translationMap?.length || !documentId
|
|
159
|
+
|
|
160
|
+
const onRunTranslation = useCallback(() => {
|
|
161
|
+
if (translationMap && documentId) {
|
|
162
|
+
runTranslate({
|
|
163
|
+
documentId,
|
|
164
|
+
fieldLanguageMap: translationMap.map((map) => ({
|
|
165
|
+
...map,
|
|
166
|
+
// eslint-disable-next-line max-nested-callbacks
|
|
167
|
+
outputs: map.outputs.filter((out) => !!toLanguages?.find((l) => l.id === out.id)),
|
|
168
|
+
})),
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
close()
|
|
172
|
+
}, [translationMap, documentId, runTranslate, close, toLanguages])
|
|
173
|
+
|
|
174
|
+
const runButton = (
|
|
175
|
+
<Button
|
|
176
|
+
text={`Translate`}
|
|
177
|
+
tone="primary"
|
|
178
|
+
icon={PlayIcon}
|
|
179
|
+
style={{width: '100%'}}
|
|
180
|
+
disabled={runDisabled}
|
|
181
|
+
onClick={onRunTranslation}
|
|
182
|
+
/>
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<FieldTranslationContext.Provider value={contextValue}>
|
|
187
|
+
{dialogOpen ? (
|
|
188
|
+
<Dialog
|
|
189
|
+
id={id}
|
|
190
|
+
width={1}
|
|
191
|
+
open={dialogOpen}
|
|
192
|
+
onClose={close}
|
|
193
|
+
header="Translate fields"
|
|
194
|
+
footer={
|
|
195
|
+
<Flex justify="space-between" padding={2} flex={1}>
|
|
196
|
+
{runDisabled ? (
|
|
197
|
+
<Tooltip
|
|
198
|
+
content={
|
|
199
|
+
<Flex padding={2}>
|
|
200
|
+
<Text>Nothing to translate.</Text>
|
|
201
|
+
</Flex>
|
|
202
|
+
}
|
|
203
|
+
placement="top"
|
|
204
|
+
>
|
|
205
|
+
<Flex flex={1}>{runButton}</Flex>
|
|
206
|
+
</Tooltip>
|
|
207
|
+
) : (
|
|
208
|
+
runButton
|
|
209
|
+
)}
|
|
210
|
+
</Flex>
|
|
211
|
+
}
|
|
212
|
+
>
|
|
213
|
+
{languages ? (
|
|
214
|
+
<Flex padding={4} gap={5} align="flex-start" justify="center">
|
|
215
|
+
<Stack space={2}>
|
|
216
|
+
<Box marginBottom={2}>
|
|
217
|
+
<Text weight="semibold">From</Text>
|
|
218
|
+
</Box>
|
|
219
|
+
{languages?.map((l) => (
|
|
220
|
+
<Flex key={l.id} gap={3} align="center">
|
|
221
|
+
<Radio
|
|
222
|
+
name="fromLang"
|
|
223
|
+
value={l.id}
|
|
224
|
+
checked={l.id === fromLanguage?.id}
|
|
225
|
+
onClick={() => selectFromLanguage(l, languages, document, documentSchema)}
|
|
226
|
+
/>
|
|
227
|
+
<Text>{l.title ?? l.id}</Text>
|
|
228
|
+
</Flex>
|
|
229
|
+
))}
|
|
230
|
+
</Stack>
|
|
231
|
+
|
|
232
|
+
<Stack space={2}>
|
|
233
|
+
<Box marginBottom={2}>
|
|
234
|
+
<Text weight="semibold">To</Text>
|
|
235
|
+
</Box>
|
|
236
|
+
{languages
|
|
237
|
+
?.filter((l) => l.id !== fromLanguage?.id)
|
|
238
|
+
.map((l) => (
|
|
239
|
+
<Flex key={l.id} gap={3} align="center">
|
|
240
|
+
<Checkbox
|
|
241
|
+
name="toLang"
|
|
242
|
+
value={l.id}
|
|
243
|
+
checked={!!toLanguages?.find((tl) => tl.id === l.id)}
|
|
244
|
+
onClick={() => toggleToLanguage(l, toLanguages, languages)}
|
|
245
|
+
disabled={
|
|
246
|
+
!translationMap?.find((tm) => tm.outputs.find((o) => o.id === l.id))
|
|
247
|
+
}
|
|
248
|
+
/>
|
|
249
|
+
<Text>{l.title ?? l.id}</Text>
|
|
250
|
+
</Flex>
|
|
251
|
+
))}
|
|
252
|
+
</Stack>
|
|
253
|
+
</Flex>
|
|
254
|
+
) : (
|
|
255
|
+
<Flex padding={4} gap={2} align="flex-start" justify="center">
|
|
256
|
+
<Box>
|
|
257
|
+
<Spinner />
|
|
258
|
+
</Box>
|
|
259
|
+
<Text>Loading languages...</Text>
|
|
260
|
+
</Flex>
|
|
261
|
+
)}
|
|
262
|
+
</Dialog>
|
|
263
|
+
) : null}
|
|
264
|
+
{props.children}
|
|
265
|
+
</FieldTranslationContext.Provider>
|
|
266
|
+
)
|
|
267
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {SanityDocumentLike} from 'sanity'
|
|
2
|
+
import get from 'lodash/get'
|
|
3
|
+
|
|
4
|
+
export const getLanguageParams = (
|
|
5
|
+
select: Record<string, string> | undefined,
|
|
6
|
+
document: SanityDocumentLike | undefined
|
|
7
|
+
): Record<string, unknown> => {
|
|
8
|
+
if (!select || !document) {
|
|
9
|
+
return {}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const selection: Record<string, string> = select || {}
|
|
13
|
+
const selectedValue: Record<string, unknown> = {}
|
|
14
|
+
for (const [key, path] of Object.entries(selection)) {
|
|
15
|
+
let value = get(document, path)
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
// If there are references in the array, ensure they have `_ref` set, otherwise they are considered empty and can safely be ignored
|
|
18
|
+
value = value.filter((item) =>
|
|
19
|
+
typeof item === 'object' ? item?._type !== 'reference' || '_ref' in item : true
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
selectedValue[key] = value
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return selectedValue
|
|
26
|
+
}
|