@braze/sanity-plugin-ai-translation 0.2.76 → 0.2.78

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 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, CheckmarkIcon, ErrorOutlineIcon, CircleIcon, ArrowRightIcon, ResetIcon, PanelRightIcon, TranslateIcon } from "@sanity/icons";
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) : "");
@@ -1596,7 +1597,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1596
1597
  reasoning: result.usage?.output_tokens_details?.reasoning_tokens || 0,
1597
1598
  total: result.usage?.total_tokens || 0,
1598
1599
  cached: result.usage?.input_tokens_details?.cached_tokens || 0
1599
- }, cost = calculateCost(tokensUsed, model);
1600
+ };
1600
1601
  let content = "";
1601
1602
  if (result.output && Array.isArray(result.output)) {
1602
1603
  const messageItem = result.output.find((item) => item.type === "message");
@@ -1604,8 +1605,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1604
1605
  }
1605
1606
  return content || (content = result.output_text || result.text || ""), {
1606
1607
  content,
1607
- tokensUsed,
1608
- cost
1608
+ tokensUsed
1609
1609
  };
1610
1610
  }, callChatCompletionsAPI = async (apiKey, request) => {
1611
1611
  const model = request.model || "gpt-4o", requestBody = {
@@ -1657,56 +1657,11 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1657
1657
  reasoning: usage.reasoning_tokens || 0,
1658
1658
  total: usage.total_tokens || 0,
1659
1659
  cached: usage.prompt_tokens_details?.cached_tokens || 0
1660
- }, cost = calculateCost(tokensUsed, model);
1660
+ };
1661
1661
  return {
1662
1662
  content: result.choices[0].message.content || "",
1663
- tokensUsed,
1664
- cost
1663
+ tokensUsed
1665
1664
  };
1666
- }, calculateCost = (tokens, model) => {
1667
- const pricing = {
1668
- "gpt-5": {
1669
- input: 125e-8,
1670
- output: 1e-5,
1671
- reasoning: 0
1672
- },
1673
- "gpt-5.1": {
1674
- input: 125e-8,
1675
- output: 1e-5,
1676
- reasoning: 0
1677
- },
1678
- "gpt-5-mini": {
1679
- input: 25e-8,
1680
- output: 2e-6,
1681
- reasoning: 0
1682
- },
1683
- "gpt-5-nano": {
1684
- input: 50000000000000004e-24,
1685
- output: 40000000000000003e-23,
1686
- reasoning: 0
1687
- },
1688
- "gpt-5-pro": {
1689
- input: 15e-6,
1690
- output: 12e-5,
1691
- reasoning: 0
1692
- },
1693
- "gpt-4o": {
1694
- input: 25e-7,
1695
- output: 1e-5,
1696
- reasoning: 0
1697
- },
1698
- "gpt-4-turbo": {
1699
- input: 1e-5,
1700
- output: 3e-5,
1701
- reasoning: 0
1702
- },
1703
- "gpt-4": {
1704
- input: 3e-5,
1705
- output: 6e-5,
1706
- reasoning: 0
1707
- }
1708
- }, modelPricing = pricing[model] || pricing["gpt-5"], cachedInputRate = model === "gpt-5.1" ? 0.125 / 1e6 : modelPricing.input;
1709
- return Math.max(0, tokens.prompt - tokens.cached) * modelPricing.input + tokens.cached * cachedInputRate + tokens.completion * modelPricing.output + tokens.reasoning * modelPricing.reasoning;
1710
1665
  }, deserializeDocument = (originalDoc, reviewedText) => {
1711
1666
  devLog.debug("Raw GPT-5 response received", {
1712
1667
  length: reviewedText.length,
@@ -2349,7 +2304,6 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2349
2304
  openaiResponseContent = response.content, devLog.info("OpenAI translation response received", {
2350
2305
  ...logContext,
2351
2306
  tokensUsed: response.tokensUsed,
2352
- cost: response.cost,
2353
2307
  responseLength: response.content.length,
2354
2308
  responsePreview: response.content.substring(0, 500)
2355
2309
  });
@@ -2381,13 +2335,8 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
2381
2335
  originalValue: field.originalValue,
2382
2336
  translatedValue: field.translatedValue
2383
2337
  }))
2384
- })), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai", {
2385
- tokensUsed: response.tokensUsed.total,
2386
- estimatedCost: response.cost
2387
- }), {
2388
- document: result,
2389
- tokensUsed: response.tokensUsed.total,
2390
- cost: response.cost
2338
+ })), setJobStatus(dispatch, sourceDocument._id, targetLanguage, "translating_with_openai"), {
2339
+ document: result
2391
2340
  };
2392
2341
  } catch (error) {
2393
2342
  const err = error instanceof Error ? error : new Error(String(error));
@@ -2723,7 +2672,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2723
2672
  documentId: sourceDocument._id,
2724
2673
  documentType: sourceDocument._type
2725
2674
  }), documentToTranslate = filteredDocument;
2726
- let translatedDoc, translationMetrics = {}, previousError = null;
2675
+ let translatedDoc, previousError = null;
2727
2676
  const performTranslation = async (errorContext) => {
2728
2677
  switch (translationMode) {
2729
2678
  case "sanity-only": {
@@ -2783,19 +2732,14 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2783
2732
  telemetryConfig
2784
2733
  });
2785
2734
  return result ? {
2786
- document: result.document,
2787
- tokensUsed: result.tokensUsed,
2788
- cost: result.cost
2735
+ document: result.document
2789
2736
  } : { document: null };
2790
2737
  }
2791
2738
  default:
2792
2739
  throw new Error(`Invalid translation mode: ${translationMode}`);
2793
2740
  }
2794
- }, translationResult = await performTranslation();
2795
- if (translatedDoc = translationResult.document, translationMetrics = {
2796
- tokensUsed: translationResult.tokensUsed,
2797
- cost: translationResult.cost
2798
- }, !translatedDoc)
2741
+ };
2742
+ if (translatedDoc = (await performTranslation()).document, !translatedDoc)
2799
2743
  return null;
2800
2744
  let transformedResult;
2801
2745
  Object.keys(originalStructure).length > 0 ? (devLog.debug("Restoring field transforms after translation", {
@@ -2858,13 +2802,9 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2858
2802
  targetLanguage,
2859
2803
  error: error instanceof Error ? error.message : String(error)
2860
2804
  }), previousError = error instanceof Error ? error : new Error(String(error));
2861
- const errorContext = `Previous translation attempt failed with Sanity validation error: ${previousError.message}. Please ensure the translated document structure exactly matches the source document structure.`, retryResult = await performTranslation(errorContext);
2862
- if (translatedDoc = retryResult.document, !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)
2863
2807
  throw new Error("Translation retry failed: no document returned");
2864
- translationMetrics = {
2865
- tokensUsed: retryResult.tokensUsed,
2866
- cost: retryResult.cost
2867
- };
2868
2808
  let retryTransformedResult;
2869
2809
  Object.keys(originalStructure).length > 0 ? retryTransformedResult = restoreTransforms({
2870
2810
  translatedDocument: translatedDoc,
@@ -2896,17 +2836,13 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2896
2836
  }
2897
2837
  }
2898
2838
  ), {
2899
- document: retryNewDoc,
2900
- tokensUsed: translationMetrics.tokensUsed,
2901
- cost: translationMetrics.cost
2839
+ document: retryNewDoc
2902
2840
  };
2903
2841
  }
2904
2842
  throw error;
2905
2843
  }
2906
2844
  return {
2907
- document: newDoc,
2908
- tokensUsed: translationMetrics.tokensUsed,
2909
- cost: translationMetrics.cost
2845
+ document: newDoc
2910
2846
  };
2911
2847
  }, translateDocument = async ({
2912
2848
  dispatch,
@@ -2938,7 +2874,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2938
2874
  client,
2939
2875
  sourceDocument,
2940
2876
  sourceLanguage
2941
- }), existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), translationResult = await handleTranslation({
2877
+ }), existingTranslationId = getExistingTranslationId(translationMetadata, targetLanguage), newDocument = (await handleTranslation({
2942
2878
  dispatch,
2943
2879
  client,
2944
2880
  schemaId,
@@ -2955,7 +2891,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2955
2891
  replaceExisting,
2956
2892
  mainSourceDocumentId,
2957
2893
  telemetryConfig
2958
- }), newDocument = translationResult?.document;
2894
+ }))?.document;
2959
2895
  if (!newDocument) {
2960
2896
  const failureError = new Error("Translation returned null");
2961
2897
  devLog.error("Translation failed: no document returned", failureError, logContext), setJobStatus(
@@ -3006,9 +2942,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3006
2942
  targetLanguage,
3007
2943
  "completed",
3008
2944
  {
3009
- targetId: newDocument._id,
3010
- tokensUsed: translationResult?.tokensUsed,
3011
- estimatedCost: translationResult?.cost
2945
+ targetId: newDocument._id
3012
2946
  },
3013
2947
  mainSourceDocumentId
3014
2948
  ), devLog.info("Translation completed successfully", {
@@ -3023,9 +2957,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3023
2957
  sourceLanguage,
3024
2958
  targetLanguage,
3025
2959
  translationMode,
3026
- durationMs: Date.now() - jobStartTime,
3027
- tokensUsed: translationResult?.tokensUsed,
3028
- estimatedCost: translationResult?.cost
2960
+ durationMs: Date.now() - jobStartTime
3029
2961
  }
3030
2962
  }), devLog.timeEnd("translateDocument"), devLog.groupEnd(), newDocument._id;
3031
2963
  } catch (error) {
@@ -3343,7 +3275,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3343
3275
  ] })
3344
3276
  ] })
3345
3277
  ] }) });
3346
- }, ActionButtonBar = ({ setIsDialogOpen }) => {
3278
+ }, ActionButtonBar = ({ setIsDialogOpen, hasValidationErrors }) => {
3347
3279
  const {
3348
3280
  document,
3349
3281
  config,
@@ -3475,7 +3407,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3475
3407
  tone: "positive",
3476
3408
  size: 2,
3477
3409
  onClick: handleTranslate,
3478
- 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,
3479
3411
  children: isCheckingConflicts ? "Checking..." : "Translate"
3480
3412
  }
3481
3413
  )
@@ -3921,13 +3853,110 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3921
3853
  ] })
3922
3854
  }
3923
3855
  )
3856
+ ] })
3857
+ ] }) });
3858
+ }, DocumentValidator = ({ documentId, documentType, onResult }) => {
3859
+ const publishedId = getPublishedId(documentId), { isValidating, validation } = useValidationStatus(publishedId, documentType), hasReceivedRealResult = useRef(!1);
3860
+ return useEffect(() => {
3861
+ (isValidating || validation.length > 0) && (hasReceivedRealResult.current = !0);
3862
+ const stillWaitingForFirstResult = !hasReceivedRealResult.current, errors = validation.filter((v) => v.level === "error");
3863
+ onResult(documentId, {
3864
+ isValidating: isValidating || stillWaitingForFirstResult,
3865
+ errors
3866
+ });
3867
+ }, [isValidating, validation, documentId, onResult]), null;
3868
+ }, ValidationPanel = ({ onValidationChange }) => {
3869
+ const { document, references, selectedReferenceIds, shouldTranslateReferences } = useTranslationContext(), [results, setResults] = useState(/* @__PURE__ */ new Map()), documentsToValidate = useMemo(() => {
3870
+ const docs = [
3871
+ {
3872
+ _id: document._id,
3873
+ _type: document._type,
3874
+ title: document.title || document.name || document._id
3875
+ }
3876
+ ];
3877
+ if (shouldTranslateReferences)
3878
+ for (const ref of references)
3879
+ selectedReferenceIds.has(ref._id) && docs.push(ref);
3880
+ return docs;
3881
+ }, [document, references, selectedReferenceIds, shouldTranslateReferences]), handleResult = useCallback((id, result) => {
3882
+ setResults((prev) => {
3883
+ const next = new Map(prev);
3884
+ return next.set(id, result), next;
3885
+ });
3886
+ }, []), { shouldBlock, someStillValidating, documentsWithErrors } = useMemo(() => {
3887
+ const allValidated = documentsToValidate.every((doc) => results.has(doc._id)), validating = [...results.values()].some((r) => r.isValidating) || !allValidated, docsWithErrors = [];
3888
+ for (const doc of documentsToValidate) {
3889
+ const result = results.get(doc._id);
3890
+ result && result.errors.length > 0 && docsWithErrors.push({ ...doc, errors: result.errors });
3891
+ }
3892
+ const hasErrors = docsWithErrors.length > 0;
3893
+ return {
3894
+ shouldBlock: validating || hasErrors,
3895
+ someStillValidating: validating,
3896
+ documentsWithErrors: docsWithErrors
3897
+ };
3898
+ }, [documentsToValidate, results]);
3899
+ useEffect(() => {
3900
+ onValidationChange(shouldBlock);
3901
+ }, [shouldBlock, onValidationChange]), useEffect(() => {
3902
+ documentsWithErrors.length > 0 && devLog.warn("Documents have validation errors that block translation", {
3903
+ documentsWithErrors: documentsWithErrors.map((d) => ({
3904
+ id: d._id,
3905
+ type: d._type,
3906
+ errorCount: d.errors.length
3907
+ })),
3908
+ component: "ValidationPanel"
3909
+ });
3910
+ }, [documentsWithErrors]);
3911
+ const totalErrors = useMemo(
3912
+ () => documentsWithErrors.reduce((sum, doc) => sum + doc.errors.length, 0),
3913
+ [documentsWithErrors]
3914
+ );
3915
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
3916
+ documentsToValidate.map((doc) => /* @__PURE__ */ jsx(DocumentValidator, { documentId: doc._id, documentType: doc._type, onResult: handleResult }, doc._id)),
3917
+ someStillValidating && documentsWithErrors.length === 0 && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, padding: 2, children: [
3918
+ /* @__PURE__ */ jsx(Spinner, { muted: !0 }),
3919
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Validating documents\u2026" })
3924
3920
  ] }),
3925
- translationMode !== "sanity-only" && /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 3, border: !0, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
3926
- "\u{1F4B0} ",
3927
- /* @__PURE__ */ jsx("strong", { children: "OpenAI costs apply:" }),
3928
- " You'll be charged for API usage based on tokens processed."
3921
+ documentsWithErrors.length > 0 && /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 4, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
3922
+ /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
3923
+ /* @__PURE__ */ jsx(Text, { size: 2, children: /* @__PURE__ */ jsx(ErrorOutlineIcon, {}) }),
3924
+ /* @__PURE__ */ jsxs(Text, { weight: "semibold", children: [
3925
+ totalErrors,
3926
+ " validation error",
3927
+ totalErrors !== 1 ? "s" : "",
3928
+ " found in ",
3929
+ documentsWithErrors.length,
3930
+ " ",
3931
+ "document",
3932
+ documentsWithErrors.length !== 1 ? "s" : ""
3933
+ ] })
3934
+ ] }),
3935
+ /* @__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." }),
3936
+ /* @__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) => {
3937
+ const publishedId = getPublishedId(doc._id);
3938
+ return /* @__PURE__ */ jsxs(Flex, { align: "center", justify: "space-between", gap: 2, children: [
3939
+ /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, flex: 1, style: { minWidth: 0 }, children: [
3940
+ /* @__PURE__ */ jsx(Text, { size: 1, children: /* @__PURE__ */ jsx(
3941
+ IntentLink,
3942
+ {
3943
+ intent: "edit",
3944
+ params: { id: publishedId, type: doc._type },
3945
+ style: { color: "inherit" },
3946
+ children: doc.title || doc._id
3947
+ }
3948
+ ) }),
3949
+ /* @__PURE__ */ jsx(Badge, { tone: "primary", fontSize: 0, children: doc._type })
3950
+ ] }),
3951
+ /* @__PURE__ */ jsxs(Badge, { tone: "critical", fontSize: 0, children: [
3952
+ doc.errors.length,
3953
+ " error",
3954
+ doc.errors.length !== 1 ? "s" : ""
3955
+ ] })
3956
+ ] }, doc._id);
3957
+ }) }) }) })
3929
3958
  ] }) })
3930
- ] }) });
3959
+ ] });
3931
3960
  }, assembleReferences = async ({
3932
3961
  client,
3933
3962
  sourceDocument,
@@ -4181,32 +4210,34 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
4181
4210
  !isLoadingReferences && references.length > 0 && showSelectionPanel && /* @__PURE__ */ jsx(ReferenceSelectionPanel, {})
4182
4211
  ] }) : null
4183
4212
  ] });
4184
- }, WriteSettingsPanel = () => /* @__PURE__ */ jsx(ReferenceConfigPanel, {}), TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
4185
- /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
4186
- /* @__PURE__ */ jsx(Label, { children: "Languages" }),
4187
- /* @__PURE__ */ jsx(LanguageSelectionPanel, {})
4188
- ] }),
4189
- /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
4190
- /* @__PURE__ */ jsx(Label, { children: "Translation Method" }),
4191
- /* @__PURE__ */ jsx(TranslationModePanel, {})
4192
- ] }),
4193
- /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
4194
- /* @__PURE__ */ jsx(Label, { children: "Content Release" }),
4195
- /* @__PURE__ */ jsx(ReleaseSelectionPanel, {})
4196
- ] }),
4197
- /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
4198
- /* @__PURE__ */ jsx(Label, { children: "Settings" }),
4199
- /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, muted: !0, children: /* @__PURE__ */ jsx(WriteSettingsPanel, {}) })
4200
- ] }),
4201
- /* @__PURE__ */ jsx(ActionButtonBar, { setIsDialogOpen })
4202
- ] }), TERMINAL_STATUSES = ["completed", "skipped", "error"], TranslationStatusPanel = () => {
4203
- const { overallStatus, translationJobs, translationMode, 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(() => {
4213
+ }, WriteSettingsPanel = () => /* @__PURE__ */ jsx(ReferenceConfigPanel, {}), TranslationConfigView = ({ setIsDialogOpen }) => {
4214
+ const [hasValidationErrors, setHasValidationErrors] = useState(!0), handleValidationChange = useCallback((shouldBlock) => {
4215
+ setHasValidationErrors(shouldBlock);
4216
+ }, []);
4217
+ return /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
4218
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
4219
+ /* @__PURE__ */ jsx(Label, { children: "Languages" }),
4220
+ /* @__PURE__ */ jsx(LanguageSelectionPanel, {})
4221
+ ] }),
4222
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
4223
+ /* @__PURE__ */ jsx(Label, { children: "Translation Method" }),
4224
+ /* @__PURE__ */ jsx(TranslationModePanel, {})
4225
+ ] }),
4226
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
4227
+ /* @__PURE__ */ jsx(Label, { children: "Content Release" }),
4228
+ /* @__PURE__ */ jsx(ReleaseSelectionPanel, {})
4229
+ ] }),
4230
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
4231
+ /* @__PURE__ */ jsx(Label, { children: "Settings" }),
4232
+ /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, muted: !0, children: /* @__PURE__ */ jsx(WriteSettingsPanel, {}) })
4233
+ ] }),
4234
+ /* @__PURE__ */ jsx(ValidationPanel, { onValidationChange: handleValidationChange }),
4235
+ /* @__PURE__ */ jsx(ActionButtonBar, { setIsDialogOpen, hasValidationErrors })
4236
+ ] });
4237
+ }, TERMINAL_STATUSES = ["completed", "skipped", "error"], TranslationStatusPanel = () => {
4238
+ 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(() => {
4204
4239
  forceCleanupActiveTranslation(document._id), dispatch({ type: "RESET_TRANSLATION_STATE" });
4205
- }, [dispatch, document._id]), openaiStats = useMemo(() => {
4206
- if (translationMode === "sanity-only") return null;
4207
- 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);
4208
- return { totalTokens, totalCost, jobsWithUsage: completedJobs.filter((j) => j.tokensUsed).length };
4209
- }, [jobs, translationMode]);
4240
+ }, [dispatch, document._id]);
4210
4241
  return /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
4211
4242
  /* @__PURE__ */ jsx(Heading, { size: 2, children: headline }),
4212
4243
  /* @__PURE__ */ jsx(ProgressBar, { progress: jobs.filter((job) => TERMINAL_STATUSES.includes(job.status)).length, max: jobs.length }),
@@ -4227,12 +4258,6 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
4227
4258
  job._id?.slice(-6) || "unknown"
4228
4259
  ] }),
4229
4260
  !["completed", "skipped", "error"].includes(job.status) && /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: getStatusLabel(job.status) }),
4230
- job.status === "completed" && job.tokensUsed ? /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
4231
- job.tokensUsed.toLocaleString(),
4232
- " tokens (~$",
4233
- job.estimatedCost?.toFixed(4),
4234
- ")"
4235
- ] }) : null,
4236
4261
  job.status === "error" && job.errorMessage ? /* @__PURE__ */ jsx(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
4237
4262
  "Error: ",
4238
4263
  job.errorMessage
@@ -4254,22 +4279,6 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
4254
4279
  ] })
4255
4280
  ] }) }) }, job._id)) }) }) })
4256
4281
  ] }),
4257
- openaiStats && openaiStats.jobsWithUsage > 0 && /* @__PURE__ */ jsx(Card, { tone: "primary", padding: 4, border: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
4258
- /* @__PURE__ */ jsx(Text, { weight: "semibold", children: "OpenAI Usage Summary" }),
4259
- /* @__PURE__ */ jsxs(Inline, { space: 4, children: [
4260
- /* @__PURE__ */ jsxs(Text, { size: 1, children: [
4261
- "Total Tokens: ",
4262
- /* @__PURE__ */ jsx("strong", { children: openaiStats.totalTokens.toLocaleString() })
4263
- ] }),
4264
- /* @__PURE__ */ jsxs(Text, { size: 1, children: [
4265
- "Total Cost: ",
4266
- /* @__PURE__ */ jsxs("strong", { children: [
4267
- "$",
4268
- openaiStats.totalCost.toFixed(4)
4269
- ] })
4270
- ] })
4271
- ] })
4272
- ] }) }),
4273
4282
  isTerminalState && /* @__PURE__ */ jsx(Flex, { justify: "flex-end", children: /* @__PURE__ */ jsx(Button, { mode: "ghost", tone: "primary", text: "Translate More", icon: ResetIcon, onClick: handleReset }) })
4274
4283
  ] });
4275
4284
  }, ProgressBar = ({ progress, max = 100 }) => {