@braze/sanity-plugin-ai-translation 0.2.86 → 0.3.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 +47 -14
- package/dist/_chunks-es/retry.js +142 -0
- package/dist/_chunks-es/retry.js.map +1 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.js +145 -485
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +87 -0
- package/dist/server.js +144 -0
- package/dist/server.js.map +1 -0
- package/package.json +23 -14
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { getPublishedId, pathToString,
|
|
1
|
+
import { useClient, getPublishedId, pathToString, useWorkspace, validateDocument, defineType, defineField, definePlugin } from "sanity";
|
|
2
2
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { WarningOutlineIcon, ErrorOutlineIcon, CheckmarkIcon,
|
|
4
|
-
import { createContext, useReducer, useRef,
|
|
3
|
+
import { WarningOutlineIcon, ErrorOutlineIcon, CheckmarkIcon, SyncIcon, PanelRightIcon, SearchIcon, CircleIcon, ArrowRightIcon, ResetIcon, TranslateIcon } from "@sanity/icons";
|
|
4
|
+
import { createContext, useEffect, useReducer, useRef, useContext, useMemo, useState, useCallback } from "react";
|
|
5
|
+
import { devLog, retryWithBackoff, isNetworkError, isSanityMutationError } from "./_chunks-es/retry.js";
|
|
5
6
|
import defaults from "lodash.defaults";
|
|
6
7
|
import { Card, Stack, Flex, Text, Box, Heading, Button, Spinner, Badge, Inline, Radio, Checkbox, Grid, TextInput, Select, Label, Code, Tooltip } from "@sanity/ui";
|
|
7
8
|
import { IntentLink } from "sanity/router";
|
|
@@ -11,59 +12,55 @@ import { uuid } from "@sanity/uuid";
|
|
|
11
12
|
import cloneDeep from "lodash.clonedeep";
|
|
12
13
|
import get from "lodash.get";
|
|
13
14
|
import set from "lodash.set";
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
15
|
+
const DEFAULT_TRANSLATION_FUNCTION_NAME = "ai-translation", DEFAULT_MODEL = "gpt-5", TRANSLATION_FUNCTION_TIMEOUT_MS = 9e5, isOpenAIConfigured = (openaiConfig) => !!(openaiConfig?.enabled && typeof openaiConfig.stackId == "string" && openaiConfig.stackId.trim() !== ""), supportsFunctionInvoke = (client) => typeof client.functions?.invoke == "function", toNumber = (value) => typeof value == "number" && Number.isFinite(value) ? value : 0, parseResult = (result, functionName) => {
|
|
16
|
+
const value = result ?? {};
|
|
17
|
+
if (typeof value.content != "string")
|
|
18
|
+
throw new Error(`OpenAI translation function "${functionName}" returned no translated content`);
|
|
19
|
+
const tokensUsed = value.tokensUsed ?? {};
|
|
20
|
+
return {
|
|
21
|
+
content: value.content,
|
|
22
|
+
tokensUsed: {
|
|
23
|
+
prompt: toNumber(tokensUsed.prompt),
|
|
24
|
+
completion: toNumber(tokensUsed.completion),
|
|
25
|
+
reasoning: toNumber(tokensUsed.reasoning),
|
|
26
|
+
total: toNumber(tokensUsed.total),
|
|
27
|
+
cached: toNumber(tokensUsed.cached)
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}, invokeTranslationFunction = async ({
|
|
31
|
+
client,
|
|
32
|
+
openaiConfig,
|
|
33
|
+
messages
|
|
34
|
+
}) => {
|
|
35
|
+
if (!isOpenAIConfigured(openaiConfig))
|
|
36
|
+
throw new Error("OpenAI mode requires openai.enabled and openai.stackId");
|
|
37
|
+
if (!supportsFunctionInvoke(client))
|
|
38
|
+
throw new Error("OpenAI mode requires @sanity/client 8.1 or newer in Studio to invoke the translation function");
|
|
39
|
+
const functionName = openaiConfig.functionName || DEFAULT_TRANSLATION_FUNCTION_NAME, payload = {
|
|
40
|
+
model: openaiConfig.model || DEFAULT_MODEL,
|
|
41
|
+
messages,
|
|
42
|
+
reasoningEffort: openaiConfig.reasoningEffort
|
|
43
|
+
};
|
|
44
|
+
devLog.debug("Invoking OpenAI translation function", {
|
|
45
|
+
functionName,
|
|
46
|
+
stackId: openaiConfig.stackId,
|
|
47
|
+
model: payload.model,
|
|
48
|
+
reasoningEffort: payload.reasoningEffort,
|
|
49
|
+
messages: messages.map((m) => ({ role: m.role, contentLength: m.content.length }))
|
|
50
|
+
});
|
|
51
|
+
let result;
|
|
40
52
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
});
|
|
47
|
-
if (!response.ok)
|
|
48
|
-
return {
|
|
49
|
-
valid: !1,
|
|
50
|
-
error: (await response.json()).error?.message || "Invalid API key"
|
|
51
|
-
};
|
|
52
|
-
const availableModels = (await response.json()).data.map((m) => m.id);
|
|
53
|
-
return availableModels.includes(model) ? {
|
|
54
|
-
valid: !0,
|
|
55
|
-
modelAvailable: availableModels
|
|
56
|
-
} : {
|
|
57
|
-
valid: !1,
|
|
58
|
-
error: `Model ${model} not available. Try: ${availableModels.slice(0, 5).join(", ")}`
|
|
59
|
-
};
|
|
53
|
+
result = await client.functions.invoke(
|
|
54
|
+
functionName,
|
|
55
|
+
{ event: { data: payload }, stackId: openaiConfig.stackId, timeout: TRANSLATION_FUNCTION_TIMEOUT_MS },
|
|
56
|
+
{ sync: !0 }
|
|
57
|
+
);
|
|
60
58
|
} catch (error) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
error: error instanceof Error ? error.message : "Network error"
|
|
64
|
-
};
|
|
59
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
60
|
+
throw new Error(`OpenAI translation function "${functionName}" failed: ${message}`);
|
|
65
61
|
}
|
|
66
|
-
|
|
62
|
+
return parseResult(result, functionName);
|
|
63
|
+
}, undraftId = (id) => id.replace(/^drafts\./, ""), draftId = (id) => `drafts.${undraftId(id)}`, activeTranslations = /* @__PURE__ */ new Map(), registerActiveTranslation = (documentId, releaseId) => {
|
|
67
64
|
const entry = {
|
|
68
65
|
state: {
|
|
69
66
|
documentId,
|
|
@@ -102,7 +99,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
102
99
|
try {
|
|
103
100
|
subscriber(entry.state);
|
|
104
101
|
} catch (error) {
|
|
105
|
-
|
|
102
|
+
devLog.error("Error in active translation subscriber", error);
|
|
106
103
|
}
|
|
107
104
|
}, forceCleanupActiveTranslation = (documentId) => {
|
|
108
105
|
activeTranslations.delete(documentId);
|
|
@@ -253,7 +250,14 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
253
250
|
supportedLanguages: config.supportedLanguages?.map((l) => l.id),
|
|
254
251
|
component: "TranslationContextProvider"
|
|
255
252
|
});
|
|
256
|
-
const defaultReleaseTitle = generateDefaultReleaseTitle(document),
|
|
253
|
+
const defaultReleaseTitle = generateDefaultReleaseTitle(document), client = useClient({ apiVersion: "vX" }), openaiConfigured = isOpenAIConfigured(config?.openai), openaiAvailable = openaiConfigured && supportsFunctionInvoke(client);
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
openaiConfigured && !openaiAvailable && devLog.warn("OpenAI mode is configured but the Studio client cannot invoke functions", {
|
|
256
|
+
requires: "@sanity/client 8.1 or newer",
|
|
257
|
+
component: "TranslationContextProvider"
|
|
258
|
+
});
|
|
259
|
+
}, [openaiConfigured, openaiAvailable]);
|
|
260
|
+
const activeTranslation = getActiveTranslation(document._id), initialContext = {
|
|
257
261
|
document,
|
|
258
262
|
config,
|
|
259
263
|
// If there's an active translation, restore its state
|
|
@@ -266,8 +270,8 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
266
270
|
selectedReferenceIds: /* @__PURE__ */ new Set(),
|
|
267
271
|
// Restore jobs from active translation if available
|
|
268
272
|
translationJobs: activeTranslation?.translationJobs ?? /* @__PURE__ */ new Map(),
|
|
269
|
-
translationMode: config
|
|
270
|
-
openaiValidated:
|
|
273
|
+
translationMode: openaiAvailable && config.openai?.defaultMode || "sanity-only",
|
|
274
|
+
openaiValidated: openaiAvailable,
|
|
271
275
|
releaseConfig: {
|
|
272
276
|
mode: "new",
|
|
273
277
|
newReleaseTitle: defaultReleaseTitle
|
|
@@ -302,30 +306,6 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
302
306
|
unsubscribe();
|
|
303
307
|
};
|
|
304
308
|
}, [document._id]), useEffect(() => {
|
|
305
|
-
(async () => {
|
|
306
|
-
if (config?.openai?.enabled && config?.openai?.apiKey)
|
|
307
|
-
try {
|
|
308
|
-
const validation = await validateOpenAIApiKey(config.openai.apiKey, config.openai.model || "gpt-5");
|
|
309
|
-
dispatch({
|
|
310
|
-
type: "SET_OPENAI_VALIDATED",
|
|
311
|
-
payload: validation.valid
|
|
312
|
-
}), validation.valid ? devLog.info("OpenAI API key validated successfully", {
|
|
313
|
-
model: config.openai.model || "gpt-5",
|
|
314
|
-
component: "TranslationContextProvider"
|
|
315
|
-
}) : devLog.error("OpenAI API key validation failed", {
|
|
316
|
-
error: validation.error,
|
|
317
|
-
component: "TranslationContextProvider"
|
|
318
|
-
});
|
|
319
|
-
} catch (error) {
|
|
320
|
-
devLog.error("OpenAI validation error", error instanceof Error ? error : new Error(String(error)), {
|
|
321
|
-
component: "TranslationContextProvider"
|
|
322
|
-
}), dispatch({
|
|
323
|
-
type: "SET_OPENAI_VALIDATED",
|
|
324
|
-
payload: !1
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
})();
|
|
328
|
-
}, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]), useEffect(() => {
|
|
329
309
|
const currentRefsKey = context.references.map((ref) => ref._id).sort().join(",");
|
|
330
310
|
if (currentRefsKey !== lastProcessedRefsRef.current) {
|
|
331
311
|
if (context.references.length > 0) {
|
|
@@ -361,7 +341,12 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
361
341
|
"useTranslationContext must be used within a TranslationContextProvider with document and config props"
|
|
362
342
|
);
|
|
363
343
|
return context;
|
|
364
|
-
}
|
|
344
|
+
};
|
|
345
|
+
async function fetchSourceDocument(client, documentId, perspective) {
|
|
346
|
+
const publishedId = undraftId(documentId), [draft, published] = await client.getDocuments([draftId(publishedId), publishedId]);
|
|
347
|
+
return perspective === "published" ? published : draft ?? published;
|
|
348
|
+
}
|
|
349
|
+
const checkSourceDrafts = async ({
|
|
365
350
|
client,
|
|
366
351
|
referenceIds
|
|
367
352
|
}) => {
|
|
@@ -449,10 +434,10 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
449
434
|
return devLog.debug("No stale drafts found", { sourceDocumentId }), [];
|
|
450
435
|
const draftMap = new Map(drafts.map((d) => [d._id, d])), results = [];
|
|
451
436
|
for (const entry of deduped) {
|
|
452
|
-
const
|
|
437
|
+
const draftId2 = `drafts.${entry.targetId}`, draft = draftMap.get(draftId2);
|
|
453
438
|
draft && results.push({
|
|
454
439
|
targetId: entry.targetId,
|
|
455
|
-
draftId,
|
|
440
|
+
draftId: draftId2,
|
|
456
441
|
_type: draft._type,
|
|
457
442
|
title: draft.title || draft.name,
|
|
458
443
|
sourceDocumentId: entry.sourceDocumentId,
|
|
@@ -498,7 +483,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
498
483
|
translationHistory: []
|
|
499
484
|
});
|
|
500
485
|
} catch (error) {
|
|
501
|
-
throw
|
|
486
|
+
throw devLog.error("Error creating translation metadata", error, { documentId: sourceDocument._id }), error;
|
|
502
487
|
}
|
|
503
488
|
}, checkDocumentConflicts = async ({
|
|
504
489
|
client,
|
|
@@ -587,115 +572,6 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
|
|
|
587
572
|
}
|
|
588
573
|
return allConflicts;
|
|
589
574
|
};
|
|
590
|
-
function isNetworkError(error) {
|
|
591
|
-
if (!error) return !1;
|
|
592
|
-
if (error instanceof TypeError) {
|
|
593
|
-
const message = error.message.toLowerCase();
|
|
594
|
-
return message.includes("fetch") || message.includes("network") || message.includes("connection") || message.includes("failed to fetch") || message.includes("networkerror");
|
|
595
|
-
}
|
|
596
|
-
if (error && typeof error == "object" && "code" in error) {
|
|
597
|
-
const code = error.code;
|
|
598
|
-
return ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "ECONNREFUSED", "EHOSTUNREACH"].includes(code);
|
|
599
|
-
}
|
|
600
|
-
if (error && typeof error == "object" && "response" in error) {
|
|
601
|
-
const response = error.response;
|
|
602
|
-
if (response?.status)
|
|
603
|
-
return response.status >= 500 || response.status === 429;
|
|
604
|
-
}
|
|
605
|
-
if (error instanceof Error) {
|
|
606
|
-
const message = error.message.toLowerCase();
|
|
607
|
-
if (/\b(50[0-9]|429)\b/.test(message) || message.includes("rate limit"))
|
|
608
|
-
return !0;
|
|
609
|
-
}
|
|
610
|
-
return !1;
|
|
611
|
-
}
|
|
612
|
-
function isSanityMutationError(error) {
|
|
613
|
-
if (!error) return !1;
|
|
614
|
-
if (error instanceof Error) {
|
|
615
|
-
const message = error.message.toLowerCase();
|
|
616
|
-
return [
|
|
617
|
-
"validation",
|
|
618
|
-
"schema",
|
|
619
|
-
"invalid document",
|
|
620
|
-
"invalid field",
|
|
621
|
-
"type mismatch",
|
|
622
|
-
"reference",
|
|
623
|
-
"patch",
|
|
624
|
-
"mutation",
|
|
625
|
-
"document structure",
|
|
626
|
-
"required field",
|
|
627
|
-
"field type",
|
|
628
|
-
"sanity"
|
|
629
|
-
].some((pattern) => message.includes(pattern));
|
|
630
|
-
}
|
|
631
|
-
if (error && typeof error == "object") {
|
|
632
|
-
const errorObj = error;
|
|
633
|
-
if (("statusCode" in errorObj || "response" in errorObj || "details" in errorObj) && "statusCode" in errorObj) {
|
|
634
|
-
const statusCode = errorObj.statusCode;
|
|
635
|
-
if (statusCode >= 400 && statusCode < 500 && statusCode !== 429)
|
|
636
|
-
return !0;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
return !1;
|
|
640
|
-
}
|
|
641
|
-
const DEFAULT_RETRY_OPTIONS = {
|
|
642
|
-
maxAttempts: 2,
|
|
643
|
-
initialDelayMs: 0,
|
|
644
|
-
maxDelayMs: 5e3,
|
|
645
|
-
backoffMultiplier: 2,
|
|
646
|
-
retryableErrors: isNetworkError,
|
|
647
|
-
logRetries: !0
|
|
648
|
-
};
|
|
649
|
-
function calculateDelay(attempt, options) {
|
|
650
|
-
const delay = options.initialDelayMs * options.backoffMultiplier ** attempt;
|
|
651
|
-
return Math.min(delay, options.maxDelayMs);
|
|
652
|
-
}
|
|
653
|
-
function sleep(ms) {
|
|
654
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
655
|
-
}
|
|
656
|
-
async function retryWithBackoff(operation, options = {}) {
|
|
657
|
-
const opts = {
|
|
658
|
-
...DEFAULT_RETRY_OPTIONS,
|
|
659
|
-
...options,
|
|
660
|
-
retryableErrors: options.retryableErrors || DEFAULT_RETRY_OPTIONS.retryableErrors
|
|
661
|
-
};
|
|
662
|
-
let lastError;
|
|
663
|
-
const maxAttempts = opts.maxAttempts + 1;
|
|
664
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++)
|
|
665
|
-
try {
|
|
666
|
-
const result = await operation();
|
|
667
|
-
return attempt > 1 && opts.logRetries && devLog.info(`Operation succeeded after ${attempt} attempts`, {
|
|
668
|
-
...opts.context,
|
|
669
|
-
attempt,
|
|
670
|
-
totalAttempts: attempt
|
|
671
|
-
}), result;
|
|
672
|
-
} catch (error) {
|
|
673
|
-
if (lastError = error, !opts.retryableErrors(error))
|
|
674
|
-
throw opts.logRetries && devLog.warn("Error is not retryable, aborting retries", {
|
|
675
|
-
...opts.context,
|
|
676
|
-
attempt,
|
|
677
|
-
error: error instanceof Error ? error.message : String(error)
|
|
678
|
-
}), error;
|
|
679
|
-
if (attempt >= maxAttempts)
|
|
680
|
-
throw opts.logRetries && devLog.error(
|
|
681
|
-
`Operation failed after ${maxAttempts} attempts`,
|
|
682
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
683
|
-
{
|
|
684
|
-
...opts.context,
|
|
685
|
-
totalAttempts: maxAttempts
|
|
686
|
-
}
|
|
687
|
-
), error;
|
|
688
|
-
const delay = calculateDelay(attempt - 1, opts);
|
|
689
|
-
opts.logRetries && devLog.warn(`Operation failed, retrying in ${delay}ms (attempt ${attempt}/${maxAttempts})`, {
|
|
690
|
-
...opts.context,
|
|
691
|
-
attempt,
|
|
692
|
-
totalAttempts: maxAttempts,
|
|
693
|
-
delayMs: delay,
|
|
694
|
-
error: error instanceof Error ? error.message : String(error)
|
|
695
|
-
}), delay > 0 && await sleep(delay);
|
|
696
|
-
}
|
|
697
|
-
throw lastError;
|
|
698
|
-
}
|
|
699
575
|
class Semaphore {
|
|
700
576
|
permits;
|
|
701
577
|
queue = [];
|
|
@@ -1662,127 +1538,7 @@ class TransformRegistry {
|
|
|
1662
1538
|
}), matches[0];
|
|
1663
1539
|
}
|
|
1664
1540
|
}
|
|
1665
|
-
const globalTransformRegistry = new TransformRegistry(),
|
|
1666
|
-
const model = request.model || "gpt-5", requestBody = {
|
|
1667
|
-
model: model === "gpt-5" ? "gpt-5.1" : model,
|
|
1668
|
-
// Map gpt-5 to gpt-5.1, use others as-is
|
|
1669
|
-
input: request.messages
|
|
1670
|
-
// Uses 'input' instead of 'messages'
|
|
1671
|
-
};
|
|
1672
|
-
request.reasoningEffort && (requestBody.reasoning = {
|
|
1673
|
-
effort: request.reasoningEffort,
|
|
1674
|
-
summary: "auto"
|
|
1675
|
-
// Enable automatic reasoning summaries
|
|
1676
|
-
}), devLog.debug("OpenAI GPT-5 Request", {
|
|
1677
|
-
model: requestBody.model,
|
|
1678
|
-
reasoningEffort: request.reasoningEffort,
|
|
1679
|
-
messages: request.messages.map((m) => ({
|
|
1680
|
-
role: m.role,
|
|
1681
|
-
contentLength: m.content.length,
|
|
1682
|
-
contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? "..." : "")
|
|
1683
|
-
}))
|
|
1684
|
-
}), devLog.debug("OpenAI GPT-5 Full Prompt", {
|
|
1685
|
-
messages: request.messages
|
|
1686
|
-
});
|
|
1687
|
-
const response = await retryWithBackoff(
|
|
1688
|
-
async () => {
|
|
1689
|
-
const fetchResponse = await fetch("https://api.openai.com/v1/responses", {
|
|
1690
|
-
method: "POST",
|
|
1691
|
-
headers: {
|
|
1692
|
-
"Content-Type": "application/json",
|
|
1693
|
-
Authorization: `Bearer ${apiKey}`
|
|
1694
|
-
},
|
|
1695
|
-
body: JSON.stringify(requestBody)
|
|
1696
|
-
});
|
|
1697
|
-
if (!fetchResponse.ok) {
|
|
1698
|
-
const errorMessage = `GPT-5 API error: ${(await fetchResponse.json()).error?.message || "Unknown error"}`, errorObj = new Error(errorMessage);
|
|
1699
|
-
throw errorObj.response = { status: fetchResponse.status }, errorObj;
|
|
1700
|
-
}
|
|
1701
|
-
return fetchResponse;
|
|
1702
|
-
},
|
|
1703
|
-
{
|
|
1704
|
-
maxAttempts: 2,
|
|
1705
|
-
initialDelayMs: 0,
|
|
1706
|
-
retryableErrors: isNetworkError,
|
|
1707
|
-
context: { model: requestBody.model, endpoint: "responses" }
|
|
1708
|
-
}
|
|
1709
|
-
);
|
|
1710
|
-
if (!response.ok) {
|
|
1711
|
-
const error = await response.json();
|
|
1712
|
-
throw new Error(`GPT-5 API error: ${error.error?.message || "Unknown error"}`);
|
|
1713
|
-
}
|
|
1714
|
-
const result = await response.json(), tokensUsed = {
|
|
1715
|
-
prompt: result.usage?.input_tokens || 0,
|
|
1716
|
-
completion: result.usage?.output_tokens || 0,
|
|
1717
|
-
reasoning: result.usage?.output_tokens_details?.reasoning_tokens || 0,
|
|
1718
|
-
total: result.usage?.total_tokens || 0,
|
|
1719
|
-
cached: result.usage?.input_tokens_details?.cached_tokens || 0
|
|
1720
|
-
};
|
|
1721
|
-
let content = "";
|
|
1722
|
-
if (result.output && Array.isArray(result.output)) {
|
|
1723
|
-
const messageItem = result.output.find((item) => item.type === "message");
|
|
1724
|
-
messageItem?.content && Array.isArray(messageItem.content) && (content = messageItem.content.find((item) => item.type === "output_text")?.text || "");
|
|
1725
|
-
}
|
|
1726
|
-
return content || (content = result.output_text || result.text || ""), {
|
|
1727
|
-
content,
|
|
1728
|
-
tokensUsed
|
|
1729
|
-
};
|
|
1730
|
-
}, callChatCompletionsAPI = async (apiKey, request) => {
|
|
1731
|
-
const model = request.model || "gpt-4o", requestBody = {
|
|
1732
|
-
model,
|
|
1733
|
-
messages: request.messages,
|
|
1734
|
-
temperature: request.temperature ?? 0.3
|
|
1735
|
-
};
|
|
1736
|
-
devLog.debug("OpenAI Chat Completions Request", {
|
|
1737
|
-
model,
|
|
1738
|
-
temperature: request.temperature ?? 0.3,
|
|
1739
|
-
messages: request.messages.map((m) => ({
|
|
1740
|
-
role: m.role,
|
|
1741
|
-
contentLength: m.content.length,
|
|
1742
|
-
contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? "..." : "")
|
|
1743
|
-
}))
|
|
1744
|
-
}), devLog.debug("OpenAI Chat Completions Full Prompt", {
|
|
1745
|
-
messages: request.messages
|
|
1746
|
-
});
|
|
1747
|
-
const response = await retryWithBackoff(
|
|
1748
|
-
async () => {
|
|
1749
|
-
const fetchResponse = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
1750
|
-
method: "POST",
|
|
1751
|
-
headers: {
|
|
1752
|
-
"Content-Type": "application/json",
|
|
1753
|
-
Authorization: `Bearer ${apiKey}`
|
|
1754
|
-
},
|
|
1755
|
-
body: JSON.stringify(requestBody)
|
|
1756
|
-
});
|
|
1757
|
-
if (!fetchResponse.ok) {
|
|
1758
|
-
const errorMessage = `OpenAI API error: ${(await fetchResponse.json()).error?.message || "Unknown error"}`, errorObj = new Error(errorMessage);
|
|
1759
|
-
throw errorObj.response = { status: fetchResponse.status }, errorObj;
|
|
1760
|
-
}
|
|
1761
|
-
return fetchResponse;
|
|
1762
|
-
},
|
|
1763
|
-
{
|
|
1764
|
-
maxAttempts: 2,
|
|
1765
|
-
initialDelayMs: 0,
|
|
1766
|
-
retryableErrors: isNetworkError,
|
|
1767
|
-
context: { model, endpoint: "chat/completions" }
|
|
1768
|
-
}
|
|
1769
|
-
);
|
|
1770
|
-
if (!response.ok) {
|
|
1771
|
-
const error = await response.json();
|
|
1772
|
-
throw new Error(`OpenAI API error: ${error.error?.message || "Unknown error"}`);
|
|
1773
|
-
}
|
|
1774
|
-
const result = await response.json(), usage = result.usage || {}, tokensUsed = {
|
|
1775
|
-
prompt: usage.prompt_tokens || 0,
|
|
1776
|
-
completion: usage.completion_tokens || 0,
|
|
1777
|
-
reasoning: usage.reasoning_tokens || 0,
|
|
1778
|
-
total: usage.total_tokens || 0,
|
|
1779
|
-
cached: usage.prompt_tokens_details?.cached_tokens || 0
|
|
1780
|
-
};
|
|
1781
|
-
return {
|
|
1782
|
-
content: result.choices[0].message.content || "",
|
|
1783
|
-
tokensUsed
|
|
1784
|
-
};
|
|
1785
|
-
}, deserializeDocument = (originalDoc, reviewedText) => {
|
|
1541
|
+
const globalTransformRegistry = new TransformRegistry(), deserializeDocument = (originalDoc, reviewedText) => {
|
|
1786
1542
|
devLog.debug("Raw GPT-5 response received", {
|
|
1787
1543
|
length: reviewedText.length,
|
|
1788
1544
|
preview: reviewedText.substring(0, 1e3),
|
|
@@ -2382,6 +2138,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2382
2138
|
}
|
|
2383
2139
|
}, getOpenAITranslation = async ({
|
|
2384
2140
|
dispatch,
|
|
2141
|
+
client,
|
|
2385
2142
|
sourceDocument,
|
|
2386
2143
|
languageFieldName,
|
|
2387
2144
|
translationPrompts,
|
|
@@ -2410,16 +2167,16 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
|
|
|
2410
2167
|
targetLanguage,
|
|
2411
2168
|
translationPrompts,
|
|
2412
2169
|
errorContext
|
|
2413
|
-
}), response = await
|
|
2170
|
+
}), response = await invokeTranslationFunction({
|
|
2171
|
+
client,
|
|
2172
|
+
openaiConfig,
|
|
2414
2173
|
messages: [
|
|
2415
2174
|
{
|
|
2416
2175
|
role: "developer",
|
|
2417
2176
|
content: "You are a translation quality reviewer. Review and refine translations while preserving structure."
|
|
2418
2177
|
},
|
|
2419
2178
|
{ role: "user", content: prompt }
|
|
2420
|
-
]
|
|
2421
|
-
model: openaiConfig.model,
|
|
2422
|
-
reasoningEffort: openaiConfig.reasoningEffort
|
|
2179
|
+
]
|
|
2423
2180
|
});
|
|
2424
2181
|
openaiResponseContent = response.content, devLog.info("OpenAI translation response received", {
|
|
2425
2182
|
...logContext,
|
|
@@ -2727,25 +2484,18 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2727
2484
|
languageFieldName
|
|
2728
2485
|
});
|
|
2729
2486
|
const documentAfterUnset = unsetDocumentFields(sourceDocument, safeUnsetFields), { preserveFieldPatterns } = normalizeUnsetFields(safeUnsetFields);
|
|
2730
|
-
let
|
|
2487
|
+
let preservedValues = {};
|
|
2731
2488
|
if (preserveFieldPatterns.length > 0 && existingTranslationId)
|
|
2732
2489
|
try {
|
|
2733
2490
|
const publishedTarget = await client.getDocument(existingTranslationId);
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
preservedFields: Object.keys(preserveValues)
|
|
2743
|
-
}));
|
|
2744
|
-
} else
|
|
2745
|
-
devLog.debug("Published target not found for preserveExisting fields", {
|
|
2746
|
-
documentId: sourceDocument._id,
|
|
2747
|
-
existingTranslationId
|
|
2748
|
-
});
|
|
2491
|
+
publishedTarget ? (preservedValues = filterExcludedFields(publishedTarget, preserveFieldPatterns).excludedValues, Object.keys(preservedValues).length > 0 && devLog.info("Preserving fields from published target during re-translation", {
|
|
2492
|
+
documentId: sourceDocument._id,
|
|
2493
|
+
existingTranslationId,
|
|
2494
|
+
preservedFields: Object.keys(preservedValues)
|
|
2495
|
+
})) : devLog.debug("Published target not found for preserveExisting fields", {
|
|
2496
|
+
documentId: sourceDocument._id,
|
|
2497
|
+
existingTranslationId
|
|
2498
|
+
});
|
|
2749
2499
|
} catch (error) {
|
|
2750
2500
|
devLog.warn("Failed to fetch published target for preserveExisting fields", {
|
|
2751
2501
|
documentId: sourceDocument._id,
|
|
@@ -2764,10 +2514,10 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2764
2514
|
documentType: sourceDocument._type,
|
|
2765
2515
|
excludeFields: safeExcludeFields,
|
|
2766
2516
|
excludeFieldsLength: safeExcludeFields?.length ?? 0,
|
|
2767
|
-
sourceHasTitle: "title" in
|
|
2768
|
-
sourceTitleValue:
|
|
2517
|
+
sourceHasTitle: "title" in documentAfterUnset,
|
|
2518
|
+
sourceTitleValue: documentAfterUnset.title
|
|
2769
2519
|
});
|
|
2770
|
-
const { filteredDocument, excludedValues } = filterExcludedFields(
|
|
2520
|
+
const { filteredDocument, excludedValues } = filterExcludedFields(documentAfterUnset, safeExcludeFields);
|
|
2771
2521
|
devLog.info("After filtering excluded fields", {
|
|
2772
2522
|
documentId: sourceDocument._id,
|
|
2773
2523
|
filteredHasTitle: "title" in filteredDocument,
|
|
@@ -2830,7 +2580,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2830
2580
|
}
|
|
2831
2581
|
}
|
|
2832
2582
|
case "openai-only": {
|
|
2833
|
-
if (!openaiConfig
|
|
2583
|
+
if (!isOpenAIConfigured(openaiConfig))
|
|
2834
2584
|
throw new Error("OpenAI is not configured but openai-only mode was selected");
|
|
2835
2585
|
setJobStatus(
|
|
2836
2586
|
dispatch,
|
|
@@ -2842,6 +2592,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2842
2592
|
);
|
|
2843
2593
|
const result = await getOpenAITranslation({
|
|
2844
2594
|
dispatch,
|
|
2595
|
+
client,
|
|
2845
2596
|
sourceDocument: documentToTranslate,
|
|
2846
2597
|
targetLanguage,
|
|
2847
2598
|
languageFieldName,
|
|
@@ -2861,32 +2612,39 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2861
2612
|
};
|
|
2862
2613
|
if (translatedDoc = (await performTranslation()).document, !translatedDoc)
|
|
2863
2614
|
return null;
|
|
2864
|
-
let transformedResult;
|
|
2865
|
-
Object.keys(originalStructure).length > 0 ? (devLog.debug("Restoring field transforms after translation", {
|
|
2866
|
-
documentId: sourceDocument._id,
|
|
2867
|
-
documentType: sourceDocument._type
|
|
2868
|
-
}), transformedResult = restoreTransforms({
|
|
2869
|
-
translatedDocument: translatedDoc,
|
|
2870
|
-
originalStructure,
|
|
2871
|
-
registry: globalTransformRegistry,
|
|
2872
|
-
schemaType: sourceDocument._type
|
|
2873
|
-
})) : transformedResult = translatedDoc;
|
|
2874
2615
|
const excludedValuesToRestore = { ...excludedValues };
|
|
2875
|
-
delete excludedValuesToRestore[languageFieldName]
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2616
|
+
delete excludedValuesToRestore[languageFieldName];
|
|
2617
|
+
const finalizeDocument = (translated) => {
|
|
2618
|
+
let transformedResult;
|
|
2619
|
+
Object.keys(originalStructure).length > 0 ? (devLog.debug("Restoring field transforms after translation", {
|
|
2620
|
+
documentId: sourceDocument._id,
|
|
2621
|
+
documentType: sourceDocument._type
|
|
2622
|
+
}), transformedResult = restoreTransforms({
|
|
2623
|
+
translatedDocument: translated,
|
|
2624
|
+
originalStructure,
|
|
2625
|
+
registry: globalTransformRegistry,
|
|
2626
|
+
schemaType: sourceDocument._type
|
|
2627
|
+
})) : transformedResult = translated;
|
|
2628
|
+
const withoutUnsetFields = unsetDocumentFields(transformedResult, safeUnsetFields);
|
|
2629
|
+
devLog.info("Before restoring excluded fields", {
|
|
2630
|
+
documentId: sourceDocument._id,
|
|
2631
|
+
excludedFieldCount: Object.keys(excludedValues).length,
|
|
2632
|
+
excludedValuesKeys: Object.keys(excludedValues),
|
|
2633
|
+
languageFieldExcluded: languageFieldName in excludedValues,
|
|
2634
|
+
transformedResultHasTitle: "title" in withoutUnsetFields,
|
|
2635
|
+
transformedResultTitle: withoutUnsetFields.title
|
|
2636
|
+
});
|
|
2637
|
+
const finalDoc2 = restoreExcludedFields(
|
|
2638
|
+
restoreExcludedFields(withoutUnsetFields, excludedValuesToRestore),
|
|
2639
|
+
preservedValues
|
|
2640
|
+
);
|
|
2641
|
+
return devLog.info("After restoring excluded fields", {
|
|
2642
|
+
documentId: sourceDocument._id,
|
|
2643
|
+
finalDocHasTitle: "title" in finalDoc2,
|
|
2644
|
+
finalDocTitle: finalDoc2.title,
|
|
2645
|
+
preservedFields: Object.keys(preservedValues)
|
|
2646
|
+
}), finalDoc2;
|
|
2647
|
+
}, finalDoc = finalizeDocument(translatedDoc), targetDocumentPublishedId = existingTranslationId || uuid(), targetDocumentVersionId = `versions.${releaseId}.${targetDocumentPublishedId}`, newDoc = {
|
|
2890
2648
|
...finalDoc,
|
|
2891
2649
|
_id: targetDocumentVersionId
|
|
2892
2650
|
};
|
|
@@ -2925,15 +2683,8 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
2925
2683
|
const errorContext = `Previous translation attempt failed with Sanity validation error: ${previousError.message}. Please ensure the translated document structure exactly matches the source document structure.`;
|
|
2926
2684
|
if (translatedDoc = (await performTranslation(errorContext)).document, !translatedDoc)
|
|
2927
2685
|
throw new Error("Translation retry failed: no document returned");
|
|
2928
|
-
let retryTransformedResult;
|
|
2929
|
-
Object.keys(originalStructure).length > 0 ? retryTransformedResult = restoreTransforms({
|
|
2930
|
-
translatedDocument: translatedDoc,
|
|
2931
|
-
originalStructure,
|
|
2932
|
-
registry: globalTransformRegistry,
|
|
2933
|
-
schemaType: sourceDocument._type
|
|
2934
|
-
}) : retryTransformedResult = translatedDoc;
|
|
2935
2686
|
const retryNewDoc = {
|
|
2936
|
-
...
|
|
2687
|
+
...finalizeDocument(translatedDoc),
|
|
2937
2688
|
_id: targetDocumentVersionId
|
|
2938
2689
|
};
|
|
2939
2690
|
return await retryWithBackoff(
|
|
@@ -3224,9 +2975,11 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3224
2975
|
}
|
|
3225
2976
|
try {
|
|
3226
2977
|
await translationSemaphore.withPermit(async () => {
|
|
3227
|
-
const document = await
|
|
3228
|
-
|
|
3229
|
-
|
|
2978
|
+
const document = await fetchSourceDocument(
|
|
2979
|
+
client,
|
|
2980
|
+
documentId,
|
|
2981
|
+
sourcePerspective ?? "drafts"
|
|
2982
|
+
);
|
|
3230
2983
|
if (document || devLog.error("\u274C SOURCE DOCUMENT NOT FOUND", { documentId }), document && releaseId) {
|
|
3231
2984
|
const versionedTargetDocumentId = await translateDocument({
|
|
3232
2985
|
dispatch,
|
|
@@ -3415,12 +3168,8 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3415
3168
|
hasRun.current || (hasRun.current = !0, (async () => {
|
|
3416
3169
|
onResult(documentId, { isValidating: !0, errors: [] });
|
|
3417
3170
|
try {
|
|
3418
|
-
const
|
|
3419
|
-
|
|
3420
|
-
if (perspective === "published" ? doc = await client.fetch("*[_id == $publishedId][0]", { publishedId }) : doc = await client.fetch(
|
|
3421
|
-
"*[_id in [$draftId, $publishedId]] | order(_id desc) [0]",
|
|
3422
|
-
{ draftId, publishedId }
|
|
3423
|
-
), !doc) {
|
|
3171
|
+
const doc = await fetchSourceDocument(client, documentId, perspective);
|
|
3172
|
+
if (!doc) {
|
|
3424
3173
|
devLog.warn("Document not found for validation", { documentId, perspective }), onResult(documentId, { isValidating: !1, errors: [] });
|
|
3425
3174
|
return;
|
|
3426
3175
|
}
|
|
@@ -3436,14 +3185,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3436
3185
|
component: "DocumentValidator"
|
|
3437
3186
|
}), onResult(documentId, { isValidating: !1, errors });
|
|
3438
3187
|
} catch (error) {
|
|
3439
|
-
devLog.error(
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
), onResult(documentId, { isValidating: !1, errors: [] });
|
|
3188
|
+
devLog.error("Failed to validate document", error instanceof Error ? error : new Error(String(error)), {
|
|
3189
|
+
documentId,
|
|
3190
|
+
component: "DocumentValidator"
|
|
3191
|
+
}), onResult(documentId, { isValidating: !1, errors: [] });
|
|
3444
3192
|
}
|
|
3445
3193
|
})());
|
|
3446
|
-
}, [documentId,
|
|
3194
|
+
}, [documentId, perspective, onResult, client, workspace]), null;
|
|
3447
3195
|
}, PreflightView = () => {
|
|
3448
3196
|
const {
|
|
3449
3197
|
document,
|
|
@@ -3457,7 +3205,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3457
3205
|
translationMode,
|
|
3458
3206
|
releaseConfig,
|
|
3459
3207
|
documentSource
|
|
3460
|
-
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [scanKey, setScanKey] = useState(0), [validationResults, setValidationResults] = useState(/* @__PURE__ */ new Map()), [sourceRefsWithDrafts, setSourceRefsWithDrafts] = useState([]), [isSourceDraftCheckLoading, setIsSourceDraftCheckLoading] = useState(!0), [staleDrafts, setStaleDrafts] = useState([]), [isDraftCheckLoading, setIsDraftCheckLoading] = useState(!0), [draftAction, setDraftAction] = useState("none"), [
|
|
3208
|
+
} = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [scanKey, setScanKey] = useState(0), [validationResults, setValidationResults] = useState(/* @__PURE__ */ new Map()), [sourceRefsWithDrafts, setSourceRefsWithDrafts] = useState([]), [isSourceDraftCheckLoading, setIsSourceDraftCheckLoading] = useState(!0), [staleDrafts, setStaleDrafts] = useState([]), [isDraftCheckLoading, setIsDraftCheckLoading] = useState(!0), [draftAction, setDraftAction] = useState("none"), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!0), filteredReferences = useMemo(
|
|
3461
3209
|
() => references.filter((ref) => selectedReferenceIds.has(ref._id)),
|
|
3462
3210
|
[references, selectedReferenceIds]
|
|
3463
3211
|
), documentsToValidate = useMemo(() => {
|
|
@@ -3559,58 +3307,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3559
3307
|
useEffect(() => {
|
|
3560
3308
|
runSourceDraftCheck(), runDraftCheck(), runConflictCheck();
|
|
3561
3309
|
}, [scanKey]);
|
|
3562
|
-
const
|
|
3563
|
-
setIsDraftActionInProgress(!0);
|
|
3564
|
-
try {
|
|
3565
|
-
const draftIds = staleDrafts.map((d) => d.draftId);
|
|
3566
|
-
devLog.info("Discarding stale drafts", { draftIds, component: "PreflightView" });
|
|
3567
|
-
const transaction = client.transaction();
|
|
3568
|
-
for (const id of draftIds)
|
|
3569
|
-
transaction.delete(id);
|
|
3570
|
-
await transaction.commit(), devLog.info("Successfully discarded stale drafts", {
|
|
3571
|
-
count: draftIds.length,
|
|
3572
|
-
component: "PreflightView"
|
|
3573
|
-
}), await runDraftCheck();
|
|
3574
|
-
} catch (error) {
|
|
3575
|
-
devLog.error("Failed to discard drafts", error instanceof Error ? error : new Error(String(error)), {
|
|
3576
|
-
component: "PreflightView"
|
|
3577
|
-
});
|
|
3578
|
-
} finally {
|
|
3579
|
-
setIsDraftActionInProgress(!1);
|
|
3580
|
-
}
|
|
3581
|
-
}, [staleDrafts, client, runDraftCheck]), handlePublishDrafts = useCallback(async () => {
|
|
3582
|
-
setIsDraftActionInProgress(!0);
|
|
3583
|
-
try {
|
|
3584
|
-
devLog.info("Publishing stale drafts", {
|
|
3585
|
-
count: staleDrafts.length,
|
|
3586
|
-
component: "PreflightView"
|
|
3587
|
-
});
|
|
3588
|
-
for (const draft of staleDrafts)
|
|
3589
|
-
await client.request({
|
|
3590
|
-
method: "POST",
|
|
3591
|
-
uri: "/data/actions/mutate",
|
|
3592
|
-
body: {
|
|
3593
|
-
actions: [
|
|
3594
|
-
{
|
|
3595
|
-
actionType: "sanity.action.document.publish",
|
|
3596
|
-
draftId: draft.draftId,
|
|
3597
|
-
publishedId: draft.targetId
|
|
3598
|
-
}
|
|
3599
|
-
]
|
|
3600
|
-
}
|
|
3601
|
-
});
|
|
3602
|
-
devLog.info("Successfully published stale drafts", {
|
|
3603
|
-
count: staleDrafts.length,
|
|
3604
|
-
component: "PreflightView"
|
|
3605
|
-
}), await runDraftCheck();
|
|
3606
|
-
} catch (error) {
|
|
3607
|
-
devLog.error("Failed to publish drafts", error instanceof Error ? error : new Error(String(error)), {
|
|
3608
|
-
component: "PreflightView"
|
|
3609
|
-
});
|
|
3610
|
-
} finally {
|
|
3611
|
-
setIsDraftActionInProgress(!1);
|
|
3612
|
-
}
|
|
3613
|
-
}, [staleDrafts, client, runDraftCheck]), startTranslation = useCallback(
|
|
3310
|
+
const startTranslation = useCallback(
|
|
3614
3311
|
(replaceExisting, overrideReleaseId, skipConflicts) => {
|
|
3615
3312
|
translateDocumentSet({
|
|
3616
3313
|
sourceDocument: document,
|
|
@@ -3656,9 +3353,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3656
3353
|
}, []), sourceRefDraftIds = useMemo(
|
|
3657
3354
|
() => new Set(sourceRefsWithDrafts.map((d) => d.publishedId)),
|
|
3658
3355
|
[sourceRefsWithDrafts]
|
|
3659
|
-
), isLoading = someStillValidating || isSourceDraftCheckLoading || isDraftCheckLoading || isCheckingConflicts, hasStaleDrafts = staleDrafts.length > 0, draftsDismissed = draftAction === "continue", canStartTranslation = !isLoading && !hasValidationErrors && !conflictContext && (!hasStaleDrafts || draftsDismissed),
|
|
3660
|
-
setConflictContext(null);
|
|
3661
|
-
}, []), handleReplaceConflict = useCallback(() => {
|
|
3356
|
+
), isLoading = someStillValidating || isSourceDraftCheckLoading || isDraftCheckLoading || isCheckingConflicts, hasStaleDrafts = staleDrafts.length > 0, draftsDismissed = draftAction === "continue", canStartTranslation = !isLoading && !hasValidationErrors && !conflictContext && (!hasStaleDrafts || draftsDismissed), handleReplaceConflict = useCallback(() => {
|
|
3662
3357
|
conflictContext && startTranslation(!0, conflictContext.releaseId);
|
|
3663
3358
|
}, [startTranslation, conflictContext]), handleSkipConflicts = useCallback(() => {
|
|
3664
3359
|
conflictContext && startTranslation(!1, conflictContext.releaseId, conflictContext.conflicts);
|
|
@@ -3668,7 +3363,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3668
3363
|
{
|
|
3669
3364
|
conflicts: conflictContext.conflicts,
|
|
3670
3365
|
totalTranslations,
|
|
3671
|
-
onCancel:
|
|
3366
|
+
onCancel: handleBack,
|
|
3672
3367
|
onReplace: handleReplaceConflict,
|
|
3673
3368
|
onSkipConflicts: handleSkipConflicts
|
|
3674
3369
|
}
|
|
@@ -3750,10 +3445,10 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3750
3445
|
(isDraftCheckLoading || hasStaleDrafts) && /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, radius: 2, tone: hasStaleDrafts && !draftsDismissed ? "caution" : void 0, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
3751
3446
|
/* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
3752
3447
|
isDraftCheckLoading ? /* @__PURE__ */ jsx(Spinner, { muted: !0 }) : draftsDismissed ? /* @__PURE__ */ jsx(Text, { size: 2, children: /* @__PURE__ */ jsx(CheckmarkIcon, {}) }) : /* @__PURE__ */ jsx(Text, { size: 2, children: /* @__PURE__ */ jsx(WarningOutlineIcon, {}) }),
|
|
3753
|
-
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: isDraftCheckLoading ? "Checking for stale drafts on target documents\u2026" : draftsDismissed ? "Stale drafts acknowledged" : `${staleDrafts.length}
|
|
3448
|
+
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: isDraftCheckLoading ? "Checking for stale drafts on target documents\u2026" : draftsDismissed ? "Stale drafts acknowledged" : `${staleDrafts.length} draft${staleDrafts.length !== 1 ? "s" : ""} found on existing translated document${staleDrafts.length !== 1 ? "s" : ""}` })
|
|
3754
3449
|
] }),
|
|
3755
3450
|
hasStaleDrafts && !draftsDismissed && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3756
|
-
/* @__PURE__ */ jsx(Text, { size: 1, children: "These
|
|
3451
|
+
/* @__PURE__ */ jsx(Text, { size: 1, children: "These existing translations have unpublished changes that will be out-of-date after this translation is completed. Use the links below to either publish each draft or discard the changes." }),
|
|
3757
3452
|
/* @__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: staleDrafts.map((draft) => /* @__PURE__ */ jsxs(Flex, { align: "center", justify: "space-between", gap: 2, children: [
|
|
3758
3453
|
/* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, flex: 1, style: { minWidth: 0 }, children: [
|
|
3759
3454
|
/* @__PURE__ */ jsx(Text, { size: 1, children: draft.title || draft.targetId }),
|
|
@@ -3762,42 +3457,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
3762
3457
|
] }),
|
|
3763
3458
|
/* @__PURE__ */ jsx(DocumentOpenerButton, { documentId: draft.targetId, documentType: draft._type })
|
|
3764
3459
|
] }, draft.draftId)) }) }) }),
|
|
3765
|
-
/* @__PURE__ */
|
|
3766
|
-
/* @__PURE__ */ jsx(
|
|
3767
|
-
Button,
|
|
3768
|
-
{
|
|
3769
|
-
mode: "ghost",
|
|
3770
|
-
tone: "caution",
|
|
3771
|
-
text: "Discard All Drafts",
|
|
3772
|
-
icon: CloseIcon,
|
|
3773
|
-
onClick: handleDiscardDrafts,
|
|
3774
|
-
disabled: isDraftActionInProgress
|
|
3775
|
-
}
|
|
3776
|
-
),
|
|
3777
|
-
/* @__PURE__ */ jsx(
|
|
3778
|
-
Button,
|
|
3779
|
-
{
|
|
3780
|
-
mode: "ghost",
|
|
3781
|
-
tone: "caution",
|
|
3782
|
-
text: "Publish All Drafts",
|
|
3783
|
-
onClick: handlePublishDrafts,
|
|
3784
|
-
disabled: isDraftActionInProgress
|
|
3785
|
-
}
|
|
3786
|
-
),
|
|
3787
|
-
/* @__PURE__ */ jsx(
|
|
3788
|
-
Button,
|
|
3789
|
-
{
|
|
3790
|
-
mode: "ghost",
|
|
3791
|
-
text: "Continue Anyway",
|
|
3792
|
-
onClick: () => setDraftAction("continue"),
|
|
3793
|
-
disabled: isDraftActionInProgress
|
|
3794
|
-
}
|
|
3795
|
-
)
|
|
3796
|
-
] }),
|
|
3797
|
-
isDraftActionInProgress && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
|
|
3798
|
-
/* @__PURE__ */ jsx(Spinner, { muted: !0 }),
|
|
3799
|
-
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Processing\u2026" })
|
|
3800
|
-
] })
|
|
3460
|
+
/* @__PURE__ */ jsx(Button, { mode: "ghost", text: "Continue Anyway", onClick: () => setDraftAction("continue") })
|
|
3801
3461
|
] })
|
|
3802
3462
|
] }) }),
|
|
3803
3463
|
/* @__PURE__ */ jsxs(Flex, { justify: "space-between", gap: 2, children: [
|
|
@@ -4286,12 +3946,12 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
|
|
|
4286
3946
|
] }) });
|
|
4287
3947
|
}, TranslationModePanel = () => {
|
|
4288
3948
|
const { config, translationMode, openaiValidated, dispatch } = useTranslationContext();
|
|
4289
|
-
if (!config?.openai
|
|
3949
|
+
if (!isOpenAIConfigured(config?.openai))
|
|
4290
3950
|
return null;
|
|
4291
3951
|
if (!openaiValidated)
|
|
4292
3952
|
return /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
|
|
4293
|
-
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "OpenAI
|
|
4294
|
-
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "
|
|
3953
|
+
/* @__PURE__ */ jsx(Text, { weight: "semibold", children: "OpenAI Unavailable" }),
|
|
3954
|
+
/* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "This Studio's Sanity client cannot invoke the translation function. OpenAI mode requires @sanity/client 8.1 or newer." })
|
|
4295
3955
|
] }) });
|
|
4296
3956
|
const handleModeChange = (mode) => {
|
|
4297
3957
|
dispatch({
|