@braze/sanity-plugin-ai-translation 0.2.75 → 0.2.77
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/dist/index.d.ts +0 -3
- package/dist/index.js +166 -160
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -284,7 +284,6 @@ declare interface OpenAIApiCallEvent extends TelemetryEventBase {
|
|
|
284
284
|
total: number;
|
|
285
285
|
cached: number;
|
|
286
286
|
};
|
|
287
|
-
cost: number;
|
|
288
287
|
durationMs: number;
|
|
289
288
|
};
|
|
290
289
|
}
|
|
@@ -694,8 +693,6 @@ declare interface TranslationJobCompletedEvent extends TelemetryEventBase {
|
|
|
694
693
|
targetLanguage: string;
|
|
695
694
|
translationMode: TranslationMode;
|
|
696
695
|
durationMs: number;
|
|
697
|
-
tokensUsed?: number;
|
|
698
|
-
estimatedCost?: number;
|
|
699
696
|
};
|
|
700
697
|
}
|
|
701
698
|
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getPublishedId, pathToString, useClient, defineType, defineField, definePlugin } from "sanity";
|
|
1
|
+
import { getPublishedId, pathToString, useClient, useValidationStatus, defineType, defineField, definePlugin } from "sanity";
|
|
2
2
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { WarningOutlineIcon, SyncIcon, SearchIcon,
|
|
3
|
+
import { WarningOutlineIcon, SyncIcon, SearchIcon, ErrorOutlineIcon, CheckmarkIcon, CircleIcon, ArrowRightIcon, ResetIcon, PanelRightIcon, TranslateIcon } from "@sanity/icons";
|
|
4
4
|
import { createContext, useReducer, useRef, useEffect, useContext, useMemo, useState, useCallback } from "react";
|
|
5
5
|
import defaults from "lodash.defaults";
|
|
6
6
|
import { Card, Stack, Flex, Text, Box, Heading, Button, Spinner, Inline, Checkbox, Grid, Radio, TextInput, Badge, Select, Label, Code, Tooltip } from "@sanity/ui";
|
|
@@ -10,6 +10,7 @@ import cloneDeep from "lodash.clonedeep";
|
|
|
10
10
|
import get from "lodash.get";
|
|
11
11
|
import set from "lodash.set";
|
|
12
12
|
import { useListeningQuery, useOpenInNewPane } from "sanity-plugin-utils";
|
|
13
|
+
import { IntentLink } from "sanity/router";
|
|
13
14
|
const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true" || process.env.SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true", devLog = {
|
|
14
15
|
info: (message, context) => {
|
|
15
16
|
isLoggingEnabled() && console.info("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
|
|
@@ -870,14 +871,11 @@ const fetchTranslationPrompts = async ({
|
|
|
870
871
|
languageFieldName
|
|
871
872
|
}) => {
|
|
872
873
|
try {
|
|
873
|
-
const doc = await retryWithBackoff(
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
context: { operation: "localizeReferences.fetchDocument", documentId }
|
|
879
|
-
}
|
|
880
|
-
);
|
|
874
|
+
const doc = await retryWithBackoff(() => client.fetch("*[_id == $documentId][0]", { documentId }), {
|
|
875
|
+
maxAttempts: 3,
|
|
876
|
+
initialDelayMs: 1e3,
|
|
877
|
+
context: { operation: "localizeReferences.fetchDocument", documentId }
|
|
878
|
+
});
|
|
881
879
|
if (!doc) {
|
|
882
880
|
devLog.error("Document not found", new Error("Document not found"), {
|
|
883
881
|
documentId,
|
|
@@ -904,20 +902,19 @@ const fetchTranslationPrompts = async ({
|
|
|
904
902
|
component: "localizeReferences"
|
|
905
903
|
});
|
|
906
904
|
}
|
|
907
|
-
}, updateReferences = async (documentId, client, refs,
|
|
905
|
+
}, updateReferences = async (documentId, client, refs, _sourceLanguage, targetLanguage) => {
|
|
908
906
|
if (!refs?.length)
|
|
909
907
|
return [];
|
|
910
908
|
const refIds = refs.map((r) => getPublishedId(r.value)), translations = await retryWithBackoff(
|
|
911
909
|
() => client.fetch(
|
|
912
910
|
`*[_type == $translationMetadataSchemaType && array::intersects(translations[].value._ref, $refIds)] {
|
|
913
|
-
"
|
|
911
|
+
"allSourceRefs": translations[_key != $targetLanguage][].value._ref,
|
|
914
912
|
"targetId": translations[_key == $targetLanguage][0].value._ref,
|
|
915
|
-
"targetType": translations[_key == $
|
|
913
|
+
"targetType": translations[_key == $targetLanguage][0].value -> _type
|
|
916
914
|
}`,
|
|
917
915
|
{
|
|
918
916
|
translationMetadataSchemaType: TRANSLATION_METADATA_TYPE,
|
|
919
917
|
refIds,
|
|
920
|
-
sourceLanguage,
|
|
921
918
|
targetLanguage
|
|
922
919
|
}
|
|
923
920
|
),
|
|
@@ -928,16 +925,21 @@ const fetchTranslationPrompts = async ({
|
|
|
928
925
|
}
|
|
929
926
|
), translationMap = /* @__PURE__ */ new Map();
|
|
930
927
|
for (const t of translations)
|
|
931
|
-
|
|
928
|
+
if (t.targetId)
|
|
929
|
+
for (const sourceRef of t.allSourceRefs ?? [])
|
|
930
|
+
sourceRef && translationMap.set(sourceRef, { targetId: t.targetId, targetType: t.targetType });
|
|
932
931
|
const patch = client.patch(documentId);
|
|
933
932
|
let hasChanges = !1;
|
|
934
933
|
for (const ref of refs) {
|
|
935
|
-
const sourceId = getPublishedId(ref.value),
|
|
936
|
-
|
|
934
|
+
const sourceId = getPublishedId(ref.value), match = translationMap.get(sourceId);
|
|
935
|
+
if (!match)
|
|
936
|
+
continue;
|
|
937
|
+
const newId = getPublishedId(match.targetId);
|
|
938
|
+
patch.set({ [pathToString(ref.path)]: newId }), patch.set({
|
|
937
939
|
[`${pathToString(ref.path.slice(0, -1))}._strengthenOnPublish`]: {
|
|
938
|
-
type:
|
|
940
|
+
type: match.targetType || void 0
|
|
939
941
|
}
|
|
940
|
-
}), hasChanges = !0
|
|
942
|
+
}), hasChanges = !0;
|
|
941
943
|
}
|
|
942
944
|
hasChanges && (await retryWithBackoff(
|
|
943
945
|
async () => {
|
|
@@ -1595,7 +1597,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1595
1597
|
reasoning: result.usage?.output_tokens_details?.reasoning_tokens || 0,
|
|
1596
1598
|
total: result.usage?.total_tokens || 0,
|
|
1597
1599
|
cached: result.usage?.input_tokens_details?.cached_tokens || 0
|
|
1598
|
-
}
|
|
1600
|
+
};
|
|
1599
1601
|
let content = "";
|
|
1600
1602
|
if (result.output && Array.isArray(result.output)) {
|
|
1601
1603
|
const messageItem = result.output.find((item) => item.type === "message");
|
|
@@ -1603,8 +1605,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1603
1605
|
}
|
|
1604
1606
|
return content || (content = result.output_text || result.text || ""), {
|
|
1605
1607
|
content,
|
|
1606
|
-
tokensUsed
|
|
1607
|
-
cost
|
|
1608
|
+
tokensUsed
|
|
1608
1609
|
};
|
|
1609
1610
|
}, callChatCompletionsAPI = async (apiKey, request) => {
|
|
1610
1611
|
const model = request.model || "gpt-4o", requestBody = {
|
|
@@ -1656,56 +1657,11 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
1656
1657
|
reasoning: usage.reasoning_tokens || 0,
|
|
1657
1658
|
total: usage.total_tokens || 0,
|
|
1658
1659
|
cached: usage.prompt_tokens_details?.cached_tokens || 0
|
|
1659
|
-
}
|
|
1660
|
+
};
|
|
1660
1661
|
return {
|
|
1661
1662
|
content: result.choices[0].message.content || "",
|
|
1662
|
-
tokensUsed
|
|
1663
|
-
cost
|
|
1663
|
+
tokensUsed
|
|
1664
1664
|
};
|
|
1665
|
-
}, calculateCost = (tokens, model) => {
|
|
1666
|
-
const pricing = {
|
|
1667
|
-
"gpt-5": {
|
|
1668
|
-
input: 125e-8,
|
|
1669
|
-
output: 1e-5,
|
|
1670
|
-
reasoning: 0
|
|
1671
|
-
},
|
|
1672
|
-
"gpt-5.1": {
|
|
1673
|
-
input: 125e-8,
|
|
1674
|
-
output: 1e-5,
|
|
1675
|
-
reasoning: 0
|
|
1676
|
-
},
|
|
1677
|
-
"gpt-5-mini": {
|
|
1678
|
-
input: 25e-8,
|
|
1679
|
-
output: 2e-6,
|
|
1680
|
-
reasoning: 0
|
|
1681
|
-
},
|
|
1682
|
-
"gpt-5-nano": {
|
|
1683
|
-
input: 50000000000000004e-24,
|
|
1684
|
-
output: 40000000000000003e-23,
|
|
1685
|
-
reasoning: 0
|
|
1686
|
-
},
|
|
1687
|
-
"gpt-5-pro": {
|
|
1688
|
-
input: 15e-6,
|
|
1689
|
-
output: 12e-5,
|
|
1690
|
-
reasoning: 0
|
|
1691
|
-
},
|
|
1692
|
-
"gpt-4o": {
|
|
1693
|
-
input: 25e-7,
|
|
1694
|
-
output: 1e-5,
|
|
1695
|
-
reasoning: 0
|
|
1696
|
-
},
|
|
1697
|
-
"gpt-4-turbo": {
|
|
1698
|
-
input: 1e-5,
|
|
1699
|
-
output: 3e-5,
|
|
1700
|
-
reasoning: 0
|
|
1701
|
-
},
|
|
1702
|
-
"gpt-4": {
|
|
1703
|
-
input: 3e-5,
|
|
1704
|
-
output: 6e-5,
|
|
1705
|
-
reasoning: 0
|
|
1706
|
-
}
|
|
1707
|
-
}, modelPricing = pricing[model] || pricing["gpt-5"], cachedInputRate = model === "gpt-5.1" ? 0.125 / 1e6 : modelPricing.input;
|
|
1708
|
-
return Math.max(0, tokens.prompt - tokens.cached) * modelPricing.input + tokens.cached * cachedInputRate + tokens.completion * modelPricing.output + tokens.reasoning * modelPricing.reasoning;
|
|
1709
1665
|
}, deserializeDocument = (originalDoc, reviewedText) => {
|
|
1710
1666
|
devLog.debug("Raw GPT-5 response received", {
|
|
1711
1667
|
length: reviewedText.length,
|
|
@@ -2348,7 +2304,6 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2348
2304
|
openaiResponseContent = response.content, devLog.info("OpenAI translation response received", {
|
|
2349
2305
|
...logContext,
|
|
2350
2306
|
tokensUsed: response.tokensUsed,
|
|
2351
|
-
cost: response.cost,
|
|
2352
2307
|
responseLength: response.content.length,
|
|
2353
2308
|
responsePreview: response.content.substring(0, 500)
|
|
2354
2309
|
});
|
|
@@ -2380,13 +2335,8 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2380
2335
|
originalValue: field.originalValue,
|
|
2381
2336
|
translatedValue: field.translatedValue
|
|
2382
2337
|
}))
|
|
2383
|
-
})), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai", {
|
|
2384
|
-
|
|
2385
|
-
estimatedCost: response.cost
|
|
2386
|
-
}), {
|
|
2387
|
-
document: result,
|
|
2388
|
-
tokensUsed: response.tokensUsed.total,
|
|
2389
|
-
cost: response.cost
|
|
2338
|
+
})), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai"), {
|
|
2339
|
+
document: result
|
|
2390
2340
|
};
|
|
2391
2341
|
} catch (error) {
|
|
2392
2342
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -2722,7 +2672,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2722
2672
|
documentId: sourceDocument._id,
|
|
2723
2673
|
documentType: sourceDocument._type
|
|
2724
2674
|
}), documentToTranslate = filteredDocument;
|
|
2725
|
-
let translatedDoc,
|
|
2675
|
+
let translatedDoc, previousError = null;
|
|
2726
2676
|
const performTranslation = async (errorContext) => {
|
|
2727
2677
|
switch (translationMode) {
|
|
2728
2678
|
case "sanity-only": {
|
|
@@ -2782,19 +2732,14 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2782
2732
|
telemetryConfig
|
|
2783
2733
|
});
|
|
2784
2734
|
return result ? {
|
|
2785
|
-
document: result.document
|
|
2786
|
-
tokensUsed: result.tokensUsed,
|
|
2787
|
-
cost: result.cost
|
|
2735
|
+
document: result.document
|
|
2788
2736
|
} : { document: null };
|
|
2789
2737
|
}
|
|
2790
2738
|
default:
|
|
2791
2739
|
throw new Error(`Invalid translation mode: ${translationMode}`);
|
|
2792
2740
|
}
|
|
2793
|
-
}
|
|
2794
|
-
if (translatedDoc =
|
|
2795
|
-
tokensUsed: translationResult.tokensUsed,
|
|
2796
|
-
cost: translationResult.cost
|
|
2797
|
-
}, !translatedDoc)
|
|
2741
|
+
};
|
|
2742
|
+
if (translatedDoc = (await performTranslation()).document, !translatedDoc)
|
|
2798
2743
|
return null;
|
|
2799
2744
|
let transformedResult;
|
|
2800
2745
|
Object.keys(originalStructure).length > 0 ? (devLog.debug("Restoring field transforms after translation", {
|
|
@@ -2857,13 +2802,9 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2857
2802
|
targetLanguage,
|
|
2858
2803
|
error: error instanceof Error ? error.message : String(error)
|
|
2859
2804
|
}), previousError = error instanceof Error ? error : new Error(String(error));
|
|
2860
|
-
const errorContext = `Previous translation attempt failed with Sanity validation error: ${previousError.message}. Please ensure the translated document structure exactly matches the source document structure
|
|
2861
|
-
if (translatedDoc =
|
|
2805
|
+
const errorContext = `Previous translation attempt failed with Sanity validation error: ${previousError.message}. Please ensure the translated document structure exactly matches the source document structure.`;
|
|
2806
|
+
if (translatedDoc = (await performTranslation(errorContext)).document, !translatedDoc)
|
|
2862
2807
|
throw new Error("Translation retry failed: no document returned");
|
|
2863
|
-
translationMetrics = {
|
|
2864
|
-
tokensUsed: retryResult.tokensUsed,
|
|
2865
|
-
cost: retryResult.cost
|
|
2866
|
-
};
|
|
2867
2808
|
let retryTransformedResult;
|
|
2868
2809
|
Object.keys(originalStructure).length > 0 ? retryTransformedResult = restoreTransforms({
|
|
2869
2810
|
translatedDocument: translatedDoc,
|
|
@@ -2895,17 +2836,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2895
2836
|
}
|
|
2896
2837
|
}
|
|
2897
2838
|
), {
|
|
2898
|
-
document: retryNewDoc
|
|
2899
|
-
tokensUsed: translationMetrics.tokensUsed,
|
|
2900
|
-
cost: translationMetrics.cost
|
|
2839
|
+
document: retryNewDoc
|
|
2901
2840
|
};
|
|
2902
2841
|
}
|
|
2903
2842
|
throw error;
|
|
2904
2843
|
}
|
|
2905
2844
|
return {
|
|
2906
|
-
document: newDoc
|
|
2907
|
-
tokensUsed: translationMetrics.tokensUsed,
|
|
2908
|
-
cost: translationMetrics.cost
|
|
2845
|
+
document: newDoc
|
|
2909
2846
|
};
|
|
2910
2847
|
}, translateDocument = async ({
|
|
2911
2848
|
dispatch,
|
|
@@ -2937,7 +2874,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2937
2874
|
client,
|
|
2938
2875
|
sourceDocument,
|
|
2939
2876
|
sourceLanguage
|
|
2940
|
-
}), existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage),
|
|
2877
|
+
}), existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = (await handleTranslation({
|
|
2941
2878
|
dispatch,
|
|
2942
2879
|
client,
|
|
2943
2880
|
schemaId,
|
|
@@ -2954,7 +2891,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2954
2891
|
replaceExisting,
|
|
2955
2892
|
mainSourceDocumentId,
|
|
2956
2893
|
telemetryConfig
|
|
2957
|
-
})
|
|
2894
|
+
}))?.document;
|
|
2958
2895
|
if (!newDocument) {
|
|
2959
2896
|
const failureError = new Error("Translation returned null");
|
|
2960
2897
|
devLog.error("Translation failed: no document returned", failureError, logContext), setJobStatus(
|
|
@@ -3005,9 +2942,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3005
2942
|
targetLanguage,
|
|
3006
2943
|
"completed",
|
|
3007
2944
|
{
|
|
3008
|
-
targetId: newDocument._id
|
|
3009
|
-
tokensUsed: translationResult?.tokensUsed,
|
|
3010
|
-
estimatedCost: translationResult?.cost
|
|
2945
|
+
targetId: newDocument._id
|
|
3011
2946
|
},
|
|
3012
2947
|
mainSourceDocumentId
|
|
3013
2948
|
), devLog.info("Translation completed successfully", {
|
|
@@ -3022,9 +2957,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3022
2957
|
sourceLanguage,
|
|
3023
2958
|
targetLanguage,
|
|
3024
2959
|
translationMode,
|
|
3025
|
-
durationMs: Date.now() - jobStartTime
|
|
3026
|
-
tokensUsed: translationResult?.tokensUsed,
|
|
3027
|
-
estimatedCost: translationResult?.cost
|
|
2960
|
+
durationMs: Date.now() - jobStartTime
|
|
3028
2961
|
}
|
|
3029
2962
|
}), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
|
|
3030
2963
|
} catch (error) {
|
|
@@ -3342,7 +3275,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3342
3275
|
] })
|
|
3343
3276
|
] })
|
|
3344
3277
|
] }) });
|
|
3345
|
-
}, ActionButtonBar = ({ setIsDialogOpen }) => {
|
|
3278
|
+
}, ActionButtonBar = ({ setIsDialogOpen, hasValidationErrors }) => {
|
|
3346
3279
|
const {
|
|
3347
3280
|
document,
|
|
3348
3281
|
config,
|
|
@@ -3474,7 +3407,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3474
3407
|
tone: "positive",
|
|
3475
3408
|
size: 2,
|
|
3476
3409
|
onClick: handleTranslate,
|
|
3477
|
-
disabled: selectedTargetLanguages.length === 0 || !["idle", "completed"].includes(overallStatus) || releaseConfig.mode === "existing" && !releaseConfig.existingReleaseId || isCheckingConflicts,
|
|
3410
|
+
disabled: selectedTargetLanguages.length === 0 || !["idle", "completed"].includes(overallStatus) || releaseConfig.mode === "existing" && !releaseConfig.existingReleaseId || isCheckingConflicts || hasValidationErrors,
|
|
3478
3411
|
children: isCheckingConflicts ? "Checking..." : "Translate"
|
|
3479
3412
|
}
|
|
3480
3413
|
)
|
|
@@ -3920,13 +3853,106 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3920
3853
|
] })
|
|
3921
3854
|
}
|
|
3922
3855
|
)
|
|
3856
|
+
] })
|
|
3857
|
+
] }) });
|
|
3858
|
+
}, DocumentValidator = ({ documentId, documentType, onResult }) => {
|
|
3859
|
+
const publishedId = getPublishedId(documentId), { isValidating, validation } = useValidationStatus(publishedId, documentType);
|
|
3860
|
+
return useEffect(() => {
|
|
3861
|
+
const errors = validation.filter((v) => v.level === "error");
|
|
3862
|
+
onResult(documentId, { isValidating, errors });
|
|
3863
|
+
}, [isValidating, validation, documentId, onResult]), null;
|
|
3864
|
+
}, ValidationPanel = ({ onValidationChange }) => {
|
|
3865
|
+
const { document, references, selectedReferenceIds, shouldTranslateReferences } = useTranslationContext(), [results, setResults] = useState(/* @__PURE__ */ new Map()), documentsToValidate = useMemo(() => {
|
|
3866
|
+
const docs = [
|
|
3867
|
+
{
|
|
3868
|
+
_id: document._id,
|
|
3869
|
+
_type: document._type,
|
|
3870
|
+
title: document.title || document.name || document._id
|
|
3871
|
+
}
|
|
3872
|
+
];
|
|
3873
|
+
if (shouldTranslateReferences)
|
|
3874
|
+
for (const ref of references)
|
|
3875
|
+
selectedReferenceIds.has(ref._id) && docs.push(ref);
|
|
3876
|
+
return docs;
|
|
3877
|
+
}, [document, references, selectedReferenceIds, shouldTranslateReferences]), handleResult = useCallback((id, result) => {
|
|
3878
|
+
setResults((prev) => {
|
|
3879
|
+
const next = new Map(prev);
|
|
3880
|
+
return next.set(id, result), next;
|
|
3881
|
+
});
|
|
3882
|
+
}, []), { shouldBlock, someStillValidating, documentsWithErrors } = useMemo(() => {
|
|
3883
|
+
const allValidated = documentsToValidate.every((doc) => results.has(doc._id)), validating = [...results.values()].some((r) => r.isValidating) || !allValidated, docsWithErrors = [];
|
|
3884
|
+
for (const doc of documentsToValidate) {
|
|
3885
|
+
const result = results.get(doc._id);
|
|
3886
|
+
result && result.errors.length > 0 && docsWithErrors.push({ ...doc, errors: result.errors });
|
|
3887
|
+
}
|
|
3888
|
+
const hasErrors = docsWithErrors.length > 0;
|
|
3889
|
+
return {
|
|
3890
|
+
shouldBlock: validating || hasErrors,
|
|
3891
|
+
someStillValidating: validating,
|
|
3892
|
+
documentsWithErrors: docsWithErrors
|
|
3893
|
+
};
|
|
3894
|
+
}, [documentsToValidate, results]);
|
|
3895
|
+
useEffect(() => {
|
|
3896
|
+
onValidationChange(shouldBlock);
|
|
3897
|
+
}, [shouldBlock, onValidationChange]), useEffect(() => {
|
|
3898
|
+
documentsWithErrors.length > 0 && devLog.warn("Documents have validation errors that block translation", {
|
|
3899
|
+
documentsWithErrors: documentsWithErrors.map((d) => ({
|
|
3900
|
+
id: d._id,
|
|
3901
|
+
type: d._type,
|
|
3902
|
+
errorCount: d.errors.length
|
|
3903
|
+
})),
|
|
3904
|
+
component: "ValidationPanel"
|
|
3905
|
+
});
|
|
3906
|
+
}, [documentsWithErrors]);
|
|
3907
|
+
const totalErrors = useMemo(
|
|
3908
|
+
() => documentsWithErrors.reduce((sum, doc) => sum + doc.errors.length, 0),
|
|
3909
|
+
[documentsWithErrors]
|
|
3910
|
+
);
|
|
3911
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3912
|
+
documentsToValidate.map((doc) => /* @__PURE__ */ jsx(DocumentValidator, { documentId: doc._id, documentType: doc._type, onResult: handleResult }, doc._id)),
|
|
3913
|
+
someStillValidating && documentsWithErrors.length === 0 && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, padding: 2, children: [
|
|
3914
|
+
/* @__PURE__ */ jsx(Spinner, { muted: !0 }),
|
|
3915
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Validating documents\u2026" })
|
|
3923
3916
|
] }),
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3917
|
+
documentsWithErrors.length > 0 && /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 4, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
3918
|
+
/* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
3919
|
+
/* @__PURE__ */ jsx(Text, { size: 2, children: /* @__PURE__ */ jsx(ErrorOutlineIcon, {}) }),
|
|
3920
|
+
/* @__PURE__ */ jsxs(Text, { weight: "semibold", children: [
|
|
3921
|
+
totalErrors,
|
|
3922
|
+
" validation error",
|
|
3923
|
+
totalErrors !== 1 ? "s" : "",
|
|
3924
|
+
" found in ",
|
|
3925
|
+
documentsWithErrors.length,
|
|
3926
|
+
" ",
|
|
3927
|
+
"document",
|
|
3928
|
+
documentsWithErrors.length !== 1 ? "s" : ""
|
|
3929
|
+
] })
|
|
3930
|
+
] }),
|
|
3931
|
+
/* @__PURE__ */ jsx(Text, { size: 1, children: "Documents must pass validation before they can be translated. Click a document to open it and fix the errors." }),
|
|
3932
|
+
/* @__PURE__ */ jsx(Card, { padding: 3, border: !0, radius: 2, children: /* @__PURE__ */ jsx(Box, { style: { maxHeight: "300px", overflowY: "auto" }, children: /* @__PURE__ */ jsx(Stack, { space: 3, children: documentsWithErrors.map((doc) => {
|
|
3933
|
+
const publishedId = getPublishedId(doc._id);
|
|
3934
|
+
return /* @__PURE__ */ jsxs(Flex, { align: "center", justify: "space-between", gap: 2, children: [
|
|
3935
|
+
/* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, flex: 1, style: { minWidth: 0 }, children: [
|
|
3936
|
+
/* @__PURE__ */ jsx(Text, { size: 1, children: /* @__PURE__ */ jsx(
|
|
3937
|
+
IntentLink,
|
|
3938
|
+
{
|
|
3939
|
+
intent: "edit",
|
|
3940
|
+
params: { id: publishedId, type: doc._type },
|
|
3941
|
+
style: { color: "inherit" },
|
|
3942
|
+
children: doc.title || doc._id
|
|
3943
|
+
}
|
|
3944
|
+
) }),
|
|
3945
|
+
/* @__PURE__ */ jsx(Badge, { tone: "primary", fontSize: 0, children: doc._type })
|
|
3946
|
+
] }),
|
|
3947
|
+
/* @__PURE__ */ jsxs(Badge, { tone: "critical", fontSize: 0, children: [
|
|
3948
|
+
doc.errors.length,
|
|
3949
|
+
" error",
|
|
3950
|
+
doc.errors.length !== 1 ? "s" : ""
|
|
3951
|
+
] })
|
|
3952
|
+
] }, doc._id);
|
|
3953
|
+
}) }) }) })
|
|
3928
3954
|
] }) })
|
|
3929
|
-
] })
|
|
3955
|
+
] });
|
|
3930
3956
|
}, assembleReferences = async ({
|
|
3931
3957
|
client,
|
|
3932
3958
|
sourceDocument,
|
|
@@ -4180,32 +4206,34 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
4180
4206
|
!isLoadingReferences && references.length > 0 && showSelectionPanel && /* @__PURE__ */ jsx(ReferenceSelectionPanel, {})
|
|
4181
4207
|
] }) : null
|
|
4182
4208
|
] });
|
|
4183
|
-
}, WriteSettingsPanel = () => /* @__PURE__ */ jsx(ReferenceConfigPanel, {}), TranslationConfigView = ({ setIsDialogOpen }) =>
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4209
|
+
}, WriteSettingsPanel = () => /* @__PURE__ */ jsx(ReferenceConfigPanel, {}), TranslationConfigView = ({ setIsDialogOpen }) => {
|
|
4210
|
+
const [hasValidationErrors, setHasValidationErrors] = useState(!0), handleValidationChange = useCallback((shouldBlock) => {
|
|
4211
|
+
setHasValidationErrors(shouldBlock);
|
|
4212
|
+
}, []);
|
|
4213
|
+
return /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
4214
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
4215
|
+
/* @__PURE__ */ jsx(Label, { children: "Languages" }),
|
|
4216
|
+
/* @__PURE__ */ jsx(LanguageSelectionPanel, {})
|
|
4217
|
+
] }),
|
|
4218
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
4219
|
+
/* @__PURE__ */ jsx(Label, { children: "Translation Method" }),
|
|
4220
|
+
/* @__PURE__ */ jsx(TranslationModePanel, {})
|
|
4221
|
+
] }),
|
|
4222
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
4223
|
+
/* @__PURE__ */ jsx(Label, { children: "Content Release" }),
|
|
4224
|
+
/* @__PURE__ */ jsx(ReleaseSelectionPanel, {})
|
|
4225
|
+
] }),
|
|
4226
|
+
/* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
4227
|
+
/* @__PURE__ */ jsx(Label, { children: "Settings" }),
|
|
4228
|
+
/* @__PURE__ */ jsx(Card, { padding: 4, border: !0, muted: !0, children: /* @__PURE__ */ jsx(WriteSettingsPanel, {}) })
|
|
4229
|
+
] }),
|
|
4230
|
+
/* @__PURE__ */ jsx(ValidationPanel, { onValidationChange: handleValidationChange }),
|
|
4231
|
+
/* @__PURE__ */ jsx(ActionButtonBar, { setIsDialogOpen, hasValidationErrors })
|
|
4232
|
+
] });
|
|
4233
|
+
}, TERMINAL_STATUSES = ["completed", "skipped", "error"], TranslationStatusPanel = () => {
|
|
4234
|
+
const { overallStatus, translationJobs, dispatch, document } = useTranslationContext(), headline = useMemo(() => overallStatus === "completed" ? "Translation completed" : overallStatus === "error" ? "Translation failed" : "Translation in progress", [overallStatus]), jobs = useMemo(() => Array.from(translationJobs.values()), [translationJobs]), isTerminalState = overallStatus === "completed" || overallStatus === "error", handleReset = useCallback(() => {
|
|
4203
4235
|
forceCleanupActiveTranslation(document._id), dispatch({ type: "RESET_TRANSLATION_STATE" });
|
|
4204
|
-
}, [dispatch, document._id])
|
|
4205
|
-
if (translationMode === "sanity-only") return null;
|
|
4206
|
-
const completedJobs = jobs.filter((j) => j.status === "completed"), totalTokens = completedJobs.reduce((sum, job) => sum + (job.tokensUsed || 0), 0), totalCost = completedJobs.reduce((sum, job) => sum + (job.estimatedCost || 0), 0);
|
|
4207
|
-
return { totalTokens, totalCost, jobsWithUsage: completedJobs.filter((j) => j.tokensUsed).length };
|
|
4208
|
-
}, [jobs, translationMode]);
|
|
4236
|
+
}, [dispatch, document._id]);
|
|
4209
4237
|
return /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
|
|
4210
4238
|
/* @__PURE__ */ jsx(Heading, { size: 2, children: headline }),
|
|
4211
4239
|
/* @__PURE__ */ jsx(ProgressBar, { progress: jobs.filter((job) => TERMINAL_STATUSES.includes(job.status)).length, max: jobs.length }),
|
|
@@ -4226,12 +4254,6 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
4226
4254
|
job._id?.slice(-6) || "unknown"
|
|
4227
4255
|
] }),
|
|
4228
4256
|
!["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: getStatusLabel(job.status) }),
|
|
4229
|
-
job.status === "completed" && job.tokensUsed ? /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
|
|
4230
|
-
job.tokensUsed.toLocaleString(),
|
|
4231
|
-
" tokens (~$",
|
|
4232
|
-
job.estimatedCost?.toFixed(4),
|
|
4233
|
-
")"
|
|
4234
|
-
] }) : null,
|
|
4235
4257
|
job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
4236
4258
|
"Error: ",
|
|
4237
4259
|
job.errorMessage
|
|
@@ -4253,22 +4275,6 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
4253
4275
|
] })
|
|
4254
4276
|
] }) }) }, job._id)) }) }) })
|
|
4255
4277
|
] }),
|
|
4256
|
-
openaiStats && openaiStats.jobsWithUsage > 0 && /* @__PURE__ */ jsx(Card, { tone: "primary", padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
4257
|
-
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "OpenAI Usage Summary" }),
|
|
4258
|
-
/* @__PURE__ */ jsxs(Inline, { space: 4, children: [
|
|
4259
|
-
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
4260
|
-
"Total Tokens: ",
|
|
4261
|
-
/* @__PURE__ */ jsx("strong", { children: openaiStats.totalTokens.toLocaleString() })
|
|
4262
|
-
] }),
|
|
4263
|
-
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
4264
|
-
"Total Cost: ",
|
|
4265
|
-
/* @__PURE__ */ jsxs("strong", { children: [
|
|
4266
|
-
"$",
|
|
4267
|
-
openaiStats.totalCost.toFixed(4)
|
|
4268
|
-
] })
|
|
4269
|
-
] })
|
|
4270
|
-
] })
|
|
4271
|
-
] }) }),
|
|
4272
4278
|
isTerminalState && /* @__PURE__ */ jsx(Flex, { justify: "flex-end", children: /* @__PURE__ */ jsx(Button, { mode: "ghost", tone: "primary", text: "Translate More", icon: ResetIcon, onClick: handleReset }) })
|
|
4273
4279
|
] });
|
|
4274
4280
|
}, ProgressBar = ({ progress, max = 100 }) => {
|