@parhelia/page-wizard 0.1.10745

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +28 -0
  3. package/dist/PageWizard.d.ts +78 -0
  4. package/dist/PageWizard.js +57 -0
  5. package/dist/PageWizard.js.map +1 -0
  6. package/dist/SplashScreen.d.ts +1 -0
  7. package/dist/SplashScreen.js +110 -0
  8. package/dist/SplashScreen.js.map +1 -0
  9. package/dist/WizardBoxConnector.d.ts +4 -0
  10. package/dist/WizardBoxConnector.js +6 -0
  11. package/dist/WizardBoxConnector.js.map +1 -0
  12. package/dist/WizardSteps.d.ts +8 -0
  13. package/dist/WizardSteps.js +174 -0
  14. package/dist/WizardSteps.js.map +1 -0
  15. package/dist/config.d.ts +2 -0
  16. package/dist/config.js +86 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.js +2 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/service.d.ts +15 -0
  22. package/dist/service.js +29 -0
  23. package/dist/service.js.map +1 -0
  24. package/dist/startPageWizardCommand.d.ts +13 -0
  25. package/dist/startPageWizardCommand.js +31 -0
  26. package/dist/startPageWizardCommand.js.map +1 -0
  27. package/dist/steps/BuildPageStep.d.ts +2 -0
  28. package/dist/steps/BuildPageStep.js +138 -0
  29. package/dist/steps/BuildPageStep.js.map +1 -0
  30. package/dist/steps/CollectStep.d.ts +2 -0
  31. package/dist/steps/CollectStep.js +115 -0
  32. package/dist/steps/CollectStep.js.map +1 -0
  33. package/dist/steps/ComponentTypesSelector.d.ts +13 -0
  34. package/dist/steps/ComponentTypesSelector.js +155 -0
  35. package/dist/steps/ComponentTypesSelector.js.map +1 -0
  36. package/dist/steps/Components.d.ts +9 -0
  37. package/dist/steps/Components.js +89 -0
  38. package/dist/steps/Components.js.map +1 -0
  39. package/dist/steps/ContentStep.d.ts +2 -0
  40. package/dist/steps/ContentStep.js +809 -0
  41. package/dist/steps/ContentStep.js.map +1 -0
  42. package/dist/steps/EditButton.d.ts +8 -0
  43. package/dist/steps/EditButton.js +5 -0
  44. package/dist/steps/EditButton.js.map +1 -0
  45. package/dist/steps/FieldEditor.d.ts +5 -0
  46. package/dist/steps/FieldEditor.js +27 -0
  47. package/dist/steps/FieldEditor.js.map +1 -0
  48. package/dist/steps/FindItemsStep.d.ts +2 -0
  49. package/dist/steps/FindItemsStep.js +294 -0
  50. package/dist/steps/FindItemsStep.js.map +1 -0
  51. package/dist/steps/Generate.d.ts +6 -0
  52. package/dist/steps/Generate.js +31 -0
  53. package/dist/steps/Generate.js.map +1 -0
  54. package/dist/steps/ImagesStep.d.ts +2 -0
  55. package/dist/steps/ImagesStep.js +178 -0
  56. package/dist/steps/ImagesStep.js.map +1 -0
  57. package/dist/steps/LayoutStep.d.ts +2 -0
  58. package/dist/steps/LayoutStep.js +128 -0
  59. package/dist/steps/LayoutStep.js.map +1 -0
  60. package/dist/steps/MetaDataStep.d.ts +2 -0
  61. package/dist/steps/MetaDataStep.js +112 -0
  62. package/dist/steps/MetaDataStep.js.map +1 -0
  63. package/dist/steps/SchottSelectImagesStep.d.ts +2 -0
  64. package/dist/steps/SchottSelectImagesStep.js +55 -0
  65. package/dist/steps/SchottSelectImagesStep.js.map +1 -0
  66. package/dist/steps/SelectStep.d.ts +2 -0
  67. package/dist/steps/SelectStep.js +151 -0
  68. package/dist/steps/SelectStep.js.map +1 -0
  69. package/dist/steps/StructureStep.d.ts +2 -0
  70. package/dist/steps/StructureStep.js +205 -0
  71. package/dist/steps/StructureStep.js.map +1 -0
  72. package/dist/steps/TranslateStep.d.ts +2 -0
  73. package/dist/steps/TranslateStep.js +613 -0
  74. package/dist/steps/TranslateStep.js.map +1 -0
  75. package/dist/steps/schema.d.ts +13 -0
  76. package/dist/steps/schema.js +139 -0
  77. package/dist/steps/schema.js.map +1 -0
  78. package/dist/steps/usePageCreator.d.ts +7 -0
  79. package/dist/steps/usePageCreator.js +285 -0
  80. package/dist/steps/usePageCreator.js.map +1 -0
  81. package/dist/types.d.ts +27 -0
  82. package/dist/types.js +2 -0
  83. package/dist/types.js.map +1 -0
  84. package/dist/usePageWizard.d.ts +22 -0
  85. package/dist/usePageWizard.js +69 -0
  86. package/dist/usePageWizard.js.map +1 -0
  87. package/dist/utils/dataAccessor.d.ts +57 -0
  88. package/dist/utils/dataAccessor.js +323 -0
  89. package/dist/utils/dataAccessor.js.map +1 -0
  90. package/package.json +48 -0
  91. package/styles.css +1 -0
@@ -0,0 +1,613 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState, useCallback } from "react";
3
+ import { Card, Textarea, ActionButton, LanguageSelector, SimpleRichTextEditor, useEditContext, executePrompt } from "@parhelia/core";
4
+ import { createWizardAiContext } from "../service";
5
+ import { Languages, Upload, Wand2, CheckCircle, AlertCircle, Sparkles, RefreshCw, Edit, Save, X, } from "lucide-react";
6
+ export function TranslateStep({ step, data, setData, setStepCompleted, internalState, setInternalState, parentItem, }) {
7
+ const editContext = useEditContext();
8
+ const [translatableFields, setTranslatableFields] = useState({});
9
+ const [translatedFields, setTranslatedFields] = useState({});
10
+ const [isLoading, setIsLoading] = useState(false);
11
+ const [isTranslating, setIsTranslating] = useState(false);
12
+ const [isDetectingLanguage, setIsDetectingLanguage] = useState(false);
13
+ const [translatingFields, setTranslatingFields] = useState(new Set());
14
+ const [checkingFields, setCheckingFields] = useState(new Set());
15
+ const [targetLanguage, setTargetLanguage] = useState(data.targetLanguage || null);
16
+ const [detectedLanguageName, setDetectedLanguageName] = useState(data.detectedLanguageName || "");
17
+ const [componentNames, setComponentNames] = useState({});
18
+ const [editingFields, setEditingFields] = useState(new Set());
19
+ // Get the current item from data (set by previous wizard steps)
20
+ const currentItem = data.currentItem || parentItem;
21
+ // Add source language state - initialize from current item's language
22
+ const [sourceLanguage, setSourceLanguage] = useState(data.sourceLanguage || currentItem?.language || "en");
23
+ // Get document content from previous wizard step
24
+ const documentContent = data.document;
25
+ const [isSaving, setIsSaving] = useState(false);
26
+ const [saveResults, setSaveResults] = useState({ success: [], errors: [] });
27
+ const fetchComponentNames = useCallback(async (itemIds, sourceLanguageParam) => {
28
+ if (!editContext || itemIds.length === 0)
29
+ return;
30
+ const names = {};
31
+ for (const itemId of itemIds) {
32
+ try {
33
+ const item = await editContext.itemsRepository.getItem({
34
+ id: itemId,
35
+ language: sourceLanguageParam,
36
+ version: 0,
37
+ });
38
+ if (item?.name) {
39
+ names[itemId] = item.name;
40
+ }
41
+ else {
42
+ names[itemId] = itemId; // Fallback to ID if name not found
43
+ }
44
+ }
45
+ catch (error) {
46
+ console.error(`Error fetching item ${itemId}:`, error);
47
+ names[itemId] = itemId; // Fallback to ID on error
48
+ }
49
+ }
50
+ setComponentNames(names);
51
+ }, [editContext]);
52
+ const fetchTranslatableFields = useCallback(async (sourceLanguageParam) => {
53
+ if (!currentItem || !editContext)
54
+ return;
55
+ setIsLoading(true);
56
+ try {
57
+ const response = await fetch("/parhelia/page-wizard/getTranslatableFields", {
58
+ method: "POST",
59
+ headers: {
60
+ "Content-Type": "application/json",
61
+ },
62
+ body: JSON.stringify({
63
+ id: currentItem.id,
64
+ language: sourceLanguageParam,
65
+ version: currentItem.version,
66
+ }),
67
+ });
68
+ if (response.ok) {
69
+ const fieldsData = await response.json();
70
+ setTranslatableFields(fieldsData);
71
+ // Initialize translated fields structure
72
+ const initialTranslatedFields = {};
73
+ Object.keys(fieldsData).forEach((itemId) => {
74
+ if (fieldsData[itemId]) {
75
+ initialTranslatedFields[itemId] = fieldsData[itemId].map((field) => ({
76
+ fieldId: field.fieldId,
77
+ translatedValue: "",
78
+ isComplete: undefined,
79
+ missingSuggestions: undefined,
80
+ }));
81
+ }
82
+ });
83
+ setTranslatedFields(initialTranslatedFields);
84
+ // Fetch component names for all item IDs
85
+ const itemIds = Object.keys(fieldsData);
86
+ if (itemIds.length > 0) {
87
+ fetchComponentNames(itemIds, sourceLanguageParam);
88
+ }
89
+ }
90
+ else {
91
+ console.error("Failed to fetch translatable fields");
92
+ }
93
+ }
94
+ catch (error) {
95
+ console.error("Error fetching translatable fields:", error);
96
+ }
97
+ finally {
98
+ setIsLoading(false);
99
+ }
100
+ }, [currentItem, editContext, fetchComponentNames]);
101
+ // Track the last source language we fetched for to prevent unnecessary re-fetching
102
+ const [lastFetchedSourceLanguage, setLastFetchedSourceLanguage] = useState("");
103
+ // Reset the last fetched source language when the current item changes
104
+ useEffect(() => {
105
+ setLastFetchedSourceLanguage("");
106
+ }, [currentItem]);
107
+ useEffect(() => {
108
+ // Only fetch translatable fields if:
109
+ // 1. We have a source language
110
+ // 2. It's different from the last one we fetched for
111
+ // 3. We're not currently saving (to avoid conflicts)
112
+ // 4. We're not currently loading or translating
113
+ if (sourceLanguage &&
114
+ sourceLanguage !== lastFetchedSourceLanguage &&
115
+ !isSaving &&
116
+ !isLoading &&
117
+ !isTranslating) {
118
+ fetchTranslatableFields(sourceLanguage);
119
+ setLastFetchedSourceLanguage(sourceLanguage);
120
+ }
121
+ }, [
122
+ fetchTranslatableFields,
123
+ sourceLanguage,
124
+ lastFetchedSourceLanguage,
125
+ isSaving,
126
+ isLoading,
127
+ isTranslating,
128
+ ]);
129
+ const detectLanguage = async () => {
130
+ if (!editContext) {
131
+ console.warn("Edit context not available for language detection");
132
+ return;
133
+ }
134
+ setIsDetectingLanguage(true);
135
+ try {
136
+ const prompt = `Analyze the following text and identify the language it is written in. Return only the language name in English (e.g., "Spanish", "French", "German", "Italian", etc.). Do not include any other text or explanation.
137
+
138
+ Text to analyze:
139
+ ${documentContent.substring(0, 1000)}`;
140
+ const abortController = new AbortController();
141
+ const result = await executePrompt([
142
+ {
143
+ content: prompt,
144
+ role: "user",
145
+ name: "user",
146
+ id: crypto.randomUUID(),
147
+ },
148
+ ], { editContext, createAiContext: createWizardAiContext }, { model: step.fields.aiModel || undefined }, { signal: abortController.signal }, (response) => {
149
+ try {
150
+ const content = response.content ||
151
+ response.messages?.[response.messages.length - 1]?.content;
152
+ if (content) {
153
+ const languageName = content.trim();
154
+ setDetectedLanguageName(languageName);
155
+ // Try to find matching Language object from available languages
156
+ const availableLanguages = editContext?.itemLanguages || [];
157
+ const matchingLanguage = availableLanguages.find((lang) => lang.name.toLowerCase().includes(languageName.toLowerCase()));
158
+ if (matchingLanguage) {
159
+ setTargetLanguage(matchingLanguage);
160
+ }
161
+ }
162
+ }
163
+ catch (parseError) {
164
+ console.error("Error parsing language detection response:", parseError);
165
+ }
166
+ });
167
+ if (result && result.messages && result.messages.length > 0) {
168
+ const lastMessage = result.messages[result.messages.length - 1];
169
+ if (lastMessage && lastMessage.content) {
170
+ const languageName = lastMessage.content.trim();
171
+ setDetectedLanguageName(languageName);
172
+ // Try to find matching Language object from available languages
173
+ const availableLanguages = editContext?.itemLanguages || [];
174
+ const matchingLanguage = availableLanguages.find((lang) => lang.name.toLowerCase().includes(languageName.toLowerCase()));
175
+ if (matchingLanguage) {
176
+ setTargetLanguage(matchingLanguage);
177
+ }
178
+ console.log(`Detected target language: ${languageName}`);
179
+ }
180
+ }
181
+ }
182
+ catch (error) {
183
+ console.error("Language detection error:", error);
184
+ }
185
+ finally {
186
+ setIsDetectingLanguage(false);
187
+ }
188
+ };
189
+ // Keep track of the last document content we detected for
190
+ const [lastDetectedDocument, setLastDetectedDocument] = useState("");
191
+ // Auto-detect language when document content is available or changes
192
+ useEffect(() => {
193
+ if (documentContent &&
194
+ !isDetectingLanguage &&
195
+ documentContent !== lastDetectedDocument) {
196
+ // Reset detected language when document changes, then detect again
197
+ if (detectedLanguageName && lastDetectedDocument) {
198
+ setDetectedLanguageName("");
199
+ setTargetLanguage(null);
200
+ }
201
+ setLastDetectedDocument(documentContent);
202
+ detectLanguage();
203
+ }
204
+ }, [documentContent]);
205
+ const handleTranslate = async () => {
206
+ if (!targetLanguage?.languageCode) {
207
+ console.warn("Missing target language");
208
+ return;
209
+ }
210
+ if (!editContext) {
211
+ console.error("Edit context not available");
212
+ return;
213
+ }
214
+ setIsTranslating(true);
215
+ try {
216
+ // Prepare the fields data for the AI prompt
217
+ const fieldsForTranslation = Object.entries(translatableFields).flatMap(([itemId, fields]) => fields.map((field) => ({
218
+ itemId,
219
+ fieldId: field.fieldId,
220
+ fieldName: field.fieldName,
221
+ originalValue: field.value,
222
+ })));
223
+ const prompt = `You are a translation assistant. You have been given a document in ${targetLanguage.name} and a list of fields that need to be translated from ${sourceLanguage} to ${targetLanguage.name}.
224
+
225
+ Your task is to:
226
+ 1. Match the content from the provided document to the fields that need translation
227
+ 2. Return ONLY the translated text from the document - do not change, modify, or create any new text
228
+ 3. If you cannot find a suitable translation in the document for a field, leave the translatedValue empty
229
+
230
+ Document content:
231
+ ${documentContent}
232
+
233
+ Fields to translate:
234
+ ${JSON.stringify(fieldsForTranslation, null, 2)}
235
+
236
+ Return your response as a JSON object in this exact format:
237
+ {
238
+ "translations": [
239
+ {
240
+ "itemId": "item-id",
241
+ "fieldId": "field-id",
242
+ "translatedValue": "exact text from document"
243
+ }
244
+ ]
245
+ }`;
246
+ const abortController = new AbortController();
247
+ const result = await executePrompt([
248
+ {
249
+ content: prompt,
250
+ role: "user",
251
+ name: "user",
252
+ id: crypto.randomUUID(),
253
+ },
254
+ ], { editContext, createAiContext: createWizardAiContext }, { model: step.fields.aiModel || undefined, stream: false }, { signal: abortController.signal });
255
+ if (result && result.messages && result.messages.length > 0) {
256
+ const lastMessage = result.messages[result.messages.length - 1];
257
+ if (lastMessage && lastMessage.content) {
258
+ try {
259
+ const parsedResponse = JSON.parse(lastMessage.content);
260
+ const translations = parsedResponse.translations || [];
261
+ // Update translated fields with AI response
262
+ const updatedTranslatedFields = {
263
+ ...translatedFields,
264
+ };
265
+ translations.forEach((translation) => {
266
+ const { itemId, fieldId, translatedValue } = translation;
267
+ if (updatedTranslatedFields[itemId]) {
268
+ const fieldIndex = updatedTranslatedFields[itemId].findIndex((f) => f.fieldId === fieldId);
269
+ if (fieldIndex !== -1 &&
270
+ updatedTranslatedFields[itemId][fieldIndex]) {
271
+ updatedTranslatedFields[itemId][fieldIndex].translatedValue =
272
+ translatedValue || "";
273
+ updatedTranslatedFields[itemId][fieldIndex].missingSuggestions = undefined;
274
+ }
275
+ }
276
+ });
277
+ setTranslatedFields(updatedTranslatedFields);
278
+ console.log(`Translation complete: ${translations.length} fields translated`);
279
+ // Check translation completeness for all translated fields
280
+ translations.forEach((translation) => {
281
+ const { itemId, fieldId, translatedValue } = translation;
282
+ if (translatedValue && translatedValue.trim()) {
283
+ const originalField = fieldsForTranslation.find((f) => f.itemId === itemId && f.fieldId === fieldId);
284
+ if (originalField) {
285
+ checkTranslationCompleteness(itemId, fieldId, originalField.originalValue, translatedValue, originalField.fieldName);
286
+ }
287
+ }
288
+ });
289
+ }
290
+ catch (parseError) {
291
+ console.error("Failed to parse AI response:", parseError);
292
+ }
293
+ }
294
+ }
295
+ }
296
+ catch (error) {
297
+ console.error("Translation error:", error);
298
+ }
299
+ finally {
300
+ setIsTranslating(false);
301
+ }
302
+ };
303
+ const handleSingleFieldTranslation = async (itemId, fieldId, field) => {
304
+ if (!targetLanguage?.languageCode || !editContext) {
305
+ console.warn("Missing target language or edit context");
306
+ return;
307
+ }
308
+ const fieldKey = `${itemId}-${fieldId}`;
309
+ setTranslatingFields((prev) => new Set(prev).add(fieldKey));
310
+ try {
311
+ // Create a prompt for this specific field
312
+ const prompt = `You are a translation assistant. You have been given a document in ${targetLanguage.name} and a single field that needs to be translated from ${sourceLanguage} to ${targetLanguage.name}.
313
+
314
+ Your task is to:
315
+ 1. Find content from the provided document that matches this field
316
+ 2. Return ONLY the translated text from the document - do not change, modify, or create any new text
317
+ 3. If you cannot find a suitable translation in the document for this field, return an empty string
318
+
319
+ Document content:
320
+ ${documentContent}
321
+
322
+ Field to translate:
323
+ - Field Name: ${field.fieldName}
324
+ - Value (${sourceLanguage}): ${field.value}
325
+
326
+ Return your response as a JSON object in this exact format:
327
+ {
328
+ "translatedValue": "exact text from document or empty string if no match found"
329
+ }`;
330
+ const abortController = new AbortController();
331
+ const result = await executePrompt([
332
+ {
333
+ content: prompt,
334
+ role: "user",
335
+ name: "user",
336
+ id: crypto.randomUUID(),
337
+ },
338
+ ], { editContext, createAiContext: createWizardAiContext }, { model: step.fields.aiModel || undefined, stream: false }, { signal: abortController.signal });
339
+ if (result && result.messages && result.messages.length > 0) {
340
+ const lastMessage = result.messages[result.messages.length - 1];
341
+ if (lastMessage && lastMessage.content) {
342
+ try {
343
+ const parsedResponse = JSON.parse(lastMessage.content);
344
+ const translatedValue = parsedResponse.translatedValue || "";
345
+ // Update this specific field's translation
346
+ setTranslatedFields((prev) => ({
347
+ ...prev,
348
+ [itemId]: prev[itemId]?.map((f) => f.fieldId === fieldId
349
+ ? { ...f, translatedValue, missingSuggestions: undefined }
350
+ : f) || [],
351
+ }));
352
+ console.log(`Single field translation complete for ${field.fieldName}`);
353
+ // Check translation completeness after translation
354
+ if (translatedValue.trim()) {
355
+ checkTranslationCompleteness(itemId, fieldId, field.value, translatedValue, field.fieldName);
356
+ }
357
+ }
358
+ catch (parseError) {
359
+ console.error("Failed to parse AI response:", parseError);
360
+ }
361
+ }
362
+ }
363
+ }
364
+ catch (error) {
365
+ console.error("Single field translation error:", error);
366
+ }
367
+ finally {
368
+ setTranslatingFields((prev) => {
369
+ const newSet = new Set(prev);
370
+ newSet.delete(fieldKey);
371
+ return newSet;
372
+ });
373
+ }
374
+ };
375
+ const checkTranslationCompleteness = async (itemId, fieldId, originalText, translatedText, fieldName) => {
376
+ if (!editContext || !translatedText.trim()) {
377
+ return false;
378
+ }
379
+ const fieldKey = `${itemId}-${fieldId}`;
380
+ setCheckingFields((prev) => new Set(prev).add(fieldKey));
381
+ try {
382
+ const prompt = `You are a translation quality checker. Your task is to evaluate if a translation is complete and accurate.
383
+
384
+ Original text (${sourceLanguage}):
385
+ ${originalText}
386
+
387
+ Translated text (${targetLanguage?.name || "target language"}):
388
+ ${translatedText}
389
+
390
+ Field context: ${fieldName}
391
+
392
+ Is the translation complete (no missing content)? Do not assess the quality of the translation, just if it is complete. Only report missing sentences missing completely.
393
+ Do not report missing words, phrases, or content that is present in the original text. We just want to make sure we copied all content from the translation document.
394
+
395
+ If the translation is incomplete, identify specific content, phrases, or meaning that exists in the original but is missing in the translation, and suggest the exact text that should be added to make the translation complete.
396
+
397
+ Return your response as a JSON object in this exact format:
398
+ {
399
+ "isComplete": true/false,
400
+ "reason": "Brief explanation of your assessment",
401
+ "missingSuggestions": "If isComplete is false, provide specific suggestions for what needs to be added to complete the translation. Include the actual text that should be added. If isComplete is true, leave this empty."
402
+ }`;
403
+ const abortController = new AbortController();
404
+ const result = await executePrompt([
405
+ {
406
+ content: prompt,
407
+ role: "user",
408
+ name: "user",
409
+ id: crypto.randomUUID(),
410
+ },
411
+ ], { editContext, createAiContext: createWizardAiContext }, { model: step.fields.aiModel || undefined, stream: false }, { signal: abortController.signal });
412
+ if (result && result.messages && result.messages.length > 0) {
413
+ const lastMessage = result.messages[result.messages.length - 1];
414
+ if (lastMessage && lastMessage.content) {
415
+ try {
416
+ const parsedResponse = JSON.parse(lastMessage.content);
417
+ const isComplete = parsedResponse.isComplete || false;
418
+ const missingSuggestions = parsedResponse.missingSuggestions || "";
419
+ // Update the field's completeness status and missing suggestions
420
+ setTranslatedFields((prev) => ({
421
+ ...prev,
422
+ [itemId]: prev[itemId]?.map((f) => f.fieldId === fieldId
423
+ ? {
424
+ ...f,
425
+ isComplete,
426
+ missingSuggestions: isComplete
427
+ ? undefined
428
+ : missingSuggestions,
429
+ }
430
+ : f) || [],
431
+ }));
432
+ console.log(`Translation completeness check for ${fieldName}: ${isComplete ? "Complete" : "Incomplete"} - ${parsedResponse.reason}`);
433
+ if (!isComplete && missingSuggestions) {
434
+ console.log(`Missing content suggestions for ${fieldName}: ${missingSuggestions}`);
435
+ }
436
+ return isComplete;
437
+ }
438
+ catch (parseError) {
439
+ console.error("Failed to parse completeness check response:", parseError);
440
+ return false;
441
+ }
442
+ }
443
+ }
444
+ return false;
445
+ }
446
+ catch (error) {
447
+ console.error("Translation completeness check error:", error);
448
+ return false;
449
+ }
450
+ finally {
451
+ setCheckingFields((prev) => {
452
+ const newSet = new Set(prev);
453
+ newSet.delete(fieldKey);
454
+ return newSet;
455
+ });
456
+ }
457
+ };
458
+ const isRichTextField = (fieldType) => {
459
+ return (fieldType?.toLowerCase() === "rich text" ||
460
+ fieldType?.toLowerCase() === "html");
461
+ };
462
+ const toggleFieldEdit = (itemId, fieldId) => {
463
+ const fieldKey = `${itemId}-${fieldId}`;
464
+ setEditingFields((prev) => {
465
+ const newSet = new Set(prev);
466
+ if (newSet.has(fieldKey)) {
467
+ newSet.delete(fieldKey);
468
+ }
469
+ else {
470
+ newSet.add(fieldKey);
471
+ }
472
+ return newSet;
473
+ });
474
+ };
475
+ const handleFieldChange = (itemId, fieldId, value) => {
476
+ setTranslatedFields((prev) => ({
477
+ ...prev,
478
+ [itemId]: prev[itemId]?.map((field) => field.fieldId === fieldId
479
+ ? {
480
+ ...field,
481
+ translatedValue: value,
482
+ isComplete: undefined,
483
+ missingSuggestions: undefined,
484
+ }
485
+ : field) || [],
486
+ }));
487
+ };
488
+ const handleSaveTranslations = async () => {
489
+ if (!editContext || !currentItem || !targetLanguage?.languageCode) {
490
+ console.error("Missing required data for saving translations");
491
+ return;
492
+ }
493
+ setIsSaving(true);
494
+ setSaveResults({ success: [], errors: [] });
495
+ try {
496
+ // Get all items that have translations
497
+ const itemsToTranslate = Object.entries(translatedFields).filter(([itemId, fields]) => fields.some((field) => field.translatedValue.trim() !== ""));
498
+ console.log(`Saving translations for ${itemsToTranslate.length} items to language: ${targetLanguage.languageCode}`);
499
+ for (const [itemId, translatedFieldsList] of itemsToTranslate) {
500
+ try {
501
+ // Create ItemDescriptor for the component item
502
+ const componentItemDescriptor = {
503
+ id: itemId,
504
+ language: targetLanguage.languageCode,
505
+ version: 0,
506
+ };
507
+ // Pre-flight version creation removed; server-side or subsequent flows will ensure version existence as needed
508
+ // Update field values with translations
509
+ const fieldsWithTranslations = translatedFieldsList.filter((field) => field.translatedValue.trim() !== "");
510
+ for (const translatedField of fieldsWithTranslations) {
511
+ const fieldDescriptor = {
512
+ fieldId: translatedField.fieldId,
513
+ item: {
514
+ id: itemId,
515
+ language: targetLanguage.languageCode,
516
+ version: 0,
517
+ },
518
+ };
519
+ console.log(`Updating field ${translatedField.fieldId} for item ${itemId}`);
520
+ await editContext.operations.editField({
521
+ field: fieldDescriptor,
522
+ value: translatedField.translatedValue,
523
+ rawValue: translatedField.translatedValue,
524
+ refresh: "none",
525
+ });
526
+ }
527
+ setSaveResults((prev) => ({
528
+ ...prev,
529
+ success: [
530
+ ...prev.success,
531
+ `${itemId} (${fieldsWithTranslations.length} fields)`,
532
+ ],
533
+ }));
534
+ }
535
+ catch (error) {
536
+ console.error(`Error translating item ${itemId}:`, error);
537
+ setSaveResults((prev) => ({
538
+ ...prev,
539
+ errors: [
540
+ ...prev.errors,
541
+ `${itemId}: ${error instanceof Error ? error.message : "Unknown error"}`,
542
+ ],
543
+ }));
544
+ }
545
+ }
546
+ console.log("Translation saving completed");
547
+ }
548
+ catch (error) {
549
+ console.error("Error during translation saving:", error);
550
+ setSaveResults((prev) => ({
551
+ ...prev,
552
+ errors: [
553
+ ...prev.errors,
554
+ `General error: ${error instanceof Error ? error.message : "Unknown error"}`,
555
+ ],
556
+ }));
557
+ }
558
+ finally {
559
+ setIsSaving(false);
560
+ }
561
+ };
562
+ // Check if step is completed (has translations and they've been saved successfully)
563
+ useEffect(() => {
564
+ const hasTranslations = Object.values(translatedFields).some((fields) => fields.some((field) => field.translatedValue.trim() !== ""));
565
+ const hasSuccessfulSave = saveResults.success.length > 0;
566
+ // Step is completed if we have translations and at least some have been saved successfully
567
+ setStepCompleted(hasTranslations && hasSuccessfulSave);
568
+ }, [translatedFields, saveResults, setStepCompleted]);
569
+ // Save translated fields and save results to wizard data
570
+ useEffect(() => {
571
+ setData((prev) => ({
572
+ ...prev,
573
+ translatedFields,
574
+ saveResults,
575
+ targetLanguage,
576
+ detectedLanguageName,
577
+ sourceLanguage,
578
+ }));
579
+ }, [
580
+ translatedFields,
581
+ saveResults,
582
+ targetLanguage,
583
+ detectedLanguageName,
584
+ sourceLanguage,
585
+ setData,
586
+ ]);
587
+ const totalFields = Object.values(translatableFields).reduce((total, fields) => total + fields.length, 0);
588
+ const translatedCount = Object.values(translatedFields).reduce((total, fields) => total +
589
+ fields.filter((field) => field.translatedValue.trim() !== "").length, 0);
590
+ return (_jsxs("div", { className: "mx-auto flex flex-col gap-6 p-6", children: [_jsxs(Card, { title: "Translation Setup", description: "Select source and target languages, target language will be auto-detected from the document", icon: _jsx(Languages, { className: "h-5 w-5" }), children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { children: [_jsx("label", { className: "mb-2 block text-sm font-medium", children: "Source Language" }), _jsx(LanguageSelector, { selectedLanguage: sourceLanguage, onLanguageSelected: (language) => setSourceLanguage(language.languageCode), disabled: false, showAllLanguagesSwitch: false, showAllLanguages: true }), _jsx("p", { className: "mt-1 text-xs text-gray-600", children: "Select the language of the content to translate from" })] }), _jsxs("div", { children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsx("label", { className: "block text-sm font-medium", children: "Target Language" }), isDetectingLanguage && (_jsxs("div", { className: "flex items-center gap-1 text-xs text-blue-600", children: [_jsx(RefreshCw, { className: "h-3 w-3 animate-spin" }), "Detecting..."] })), detectedLanguageName && !isDetectingLanguage && (_jsxs("div", { className: "flex items-center gap-1 text-xs text-green-600", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "Auto-detected"] }))] }), _jsxs("div", { className: "relative", children: [_jsx(LanguageSelector, { selectedLanguage: targetLanguage?.languageCode || "", onLanguageSelected: (language) => setTargetLanguage(language), disabled: false, showAllLanguagesSwitch: false, showAllLanguages: true }), detectedLanguageName && (_jsx("button", { type: "button", onClick: detectLanguage, className: "absolute top-1/2 right-2 -translate-y-1/2 transform text-blue-600 hover:text-blue-800", title: "Re-detect language", children: _jsx(RefreshCw, { className: "h-4 w-4" }) }))] }), detectedLanguageName && (_jsxs("p", { className: "mt-1 text-xs text-green-600", children: ["Detected: ", detectedLanguageName, targetLanguage && ` → Selected: ${targetLanguage.name}`] }))] })] }), _jsxs(ActionButton, { onClick: handleTranslate, isLoading: isTranslating, disabled: !targetLanguage?.languageCode, className: "mt-4", variant: "outline", children: [_jsx(Wand2, { className: "mr-2 h-4 w-4" }), "Translate Fields"] })] }), _jsx(Card, { title: "Translation Progress", description: `${translatedCount} of ${totalFields} fields translated`, icon: _jsx(Upload, { className: "h-5 w-5" }), children: isLoading ? (_jsx("div", { className: "py-8 text-center", children: _jsxs("div", { className: "flex items-center justify-center gap-2", children: [_jsx(RefreshCw, { className: "h-4 w-4 animate-spin" }), "Loading translatable fields..."] }) })) : isTranslating ? (_jsx("div", { className: "py-8 text-center", children: _jsxs("div", { className: "flex items-center justify-center gap-2", children: [_jsx(RefreshCw, { className: "h-4 w-4 animate-spin" }), "Translating fields..."] }) })) : isSaving ? (_jsx("div", { className: "py-8 text-center", children: _jsxs("div", { className: "flex items-center justify-center gap-2", children: [_jsx(RefreshCw, { className: "h-4 w-4 animate-spin" }), "Saving translations..."] }) })) : saveResults.success.length > 0 || saveResults.errors.length > 0 ? (_jsxs("div", { className: "space-y-4", children: [saveResults.success.length > 0 && (_jsxs("div", { className: "rounded-lg border border-green-200 bg-green-50 p-4", children: [_jsxs("div", { className: "mb-2 flex items-center gap-2 font-medium text-green-800", children: [_jsx(CheckCircle, { className: "h-4 w-4" }), "Successfully Saved (", saveResults.success.length, " items)"] }), _jsx("ul", { className: "list-inside list-disc text-sm text-green-700", children: saveResults.success.map((item, index) => (_jsx("li", { children: item }, index))) })] })), saveResults.errors.length > 0 && (_jsxs("div", { className: "rounded-lg border border-red-200 bg-red-50 p-4", children: [_jsxs("div", { className: "mb-2 flex items-center gap-2 font-medium text-red-800", children: [_jsx(AlertCircle, { className: "h-4 w-4" }), "Errors (", saveResults.errors.length, ")"] }), _jsx("ul", { className: "list-inside list-disc text-sm text-red-700", children: saveResults.errors.map((error, index) => (_jsx("li", { children: error }, index))) })] }))] })) : translatedCount > 0 ? (_jsx("div", { className: "space-y-6", children: Object.entries(translatableFields).map(([itemId, fields]) => (_jsxs("div", { className: "rounded-lg p-4", children: [_jsx("h4", { className: "mb-4 font-medium", children: componentNames[itemId] || "Component" }), _jsx("div", { className: "space-y-4", children: fields.map((field) => {
591
+ const translatedField = translatedFields[itemId]?.find((f) => f.fieldId === field.fieldId);
592
+ return (_jsxs("div", { children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsx("div", { children: _jsxs("label", { className: "block text-xs font-medium", children: [field.fieldName, " (", field.fieldType, ")"] }) }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("label", { className: "block text-xs font-medium", children: field.fieldName }), translatedField?.translatedValue && (_jsx("div", { className: "flex items-center", children: checkingFields.has(`${itemId}-${field.fieldId}`) ? (_jsx(RefreshCw, { className: "h-3 w-3 animate-spin text-gray-500" })) : translatedField.isComplete === true ? (_jsx("span", { title: "Translation is complete", children: _jsx(CheckCircle, { className: "h-3 w-3 text-green-600" }) })) : translatedField.isComplete === false ? (_jsx("span", { title: "Translation may be incomplete", children: _jsx(AlertCircle, { className: "h-3 w-3 text-red-600" }) })) : (_jsx("div", { className: "h-3 w-3" })) }))] }), _jsxs("div", { className: "flex items-center gap-1", children: [!editingFields.has(`${itemId}-${field.fieldId}`) &&
593
+ translatedField?.translatedValue && (_jsxs("button", { type: "button", onClick: () => checkTranslationCompleteness(itemId, field.fieldId, field.value, translatedField.translatedValue, field.fieldName), disabled: checkingFields.has(`${itemId}-${field.fieldId}`) ||
594
+ !translatedField.translatedValue.trim(), className: "flex items-center gap-1 rounded px-2 py-1 text-xs text-gray-600 hover:bg-gray-50 hover:text-gray-800 disabled:cursor-not-allowed disabled:text-gray-400 disabled:hover:bg-transparent", title: "Check translation completeness", children: [checkingFields.has(`${itemId}-${field.fieldId}`) ? (_jsx(RefreshCw, { className: "h-3 w-3 animate-spin" })) : (_jsx(CheckCircle, { className: "h-3 w-3" })), "Check"] })), !editingFields.has(`${itemId}-${field.fieldId}`) && (_jsxs("button", { type: "button", onClick: () => handleSingleFieldTranslation(itemId, field.fieldId, field), disabled: !targetLanguage?.languageCode ||
595
+ translatingFields.has(`${itemId}-${field.fieldId}`) ||
596
+ isTranslating, className: "flex items-center gap-1 rounded px-2 py-1 text-xs text-blue-600 hover:bg-blue-50 hover:text-blue-800 disabled:cursor-not-allowed disabled:text-gray-400 disabled:hover:bg-transparent", title: "Retranslate this field", children: [translatingFields.has(`${itemId}-${field.fieldId}`) ? (_jsx(RefreshCw, { className: "h-3 w-3 animate-spin" })) : (_jsx(Wand2, { className: "h-3 w-3" })), "Retranslate"] })), editingFields.has(`${itemId}-${field.fieldId}`) ? (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", onClick: () => toggleFieldEdit(itemId, field.fieldId), className: "flex items-center gap-1 rounded px-2 py-1 text-xs text-green-600 hover:bg-green-50 hover:text-green-800", title: "Save changes", children: [_jsx(Save, { className: "h-3 w-3", strokeWidth: 1 }), "Save"] }), _jsxs("button", { type: "button", onClick: () => toggleFieldEdit(itemId, field.fieldId), className: "flex items-center gap-1 rounded px-2 py-1 text-xs text-gray-600 hover:bg-gray-50 hover:text-gray-800", title: "Cancel edit", children: [_jsx(X, { className: "h-3 w-3", strokeWidth: 1 }), "Cancel"] })] })) : (_jsxs("button", { type: "button", onClick: () => toggleFieldEdit(itemId, field.fieldId), className: "flex items-center gap-1 rounded px-2 py-1 text-xs text-gray-600 hover:bg-gray-50 hover:text-gray-800", title: "Edit translation", children: [_jsx(Edit, { className: "h-3 w-3", strokeWidth: 1 }), "Edit"] }))] })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsx("div", { className: "rounded border bg-gray-50 p-1 text-xs", children: isRichTextField(field.fieldType) ? (_jsx("div", { className: "rich-text-content", dangerouslySetInnerHTML: {
597
+ __html: field.value,
598
+ } })) : (field.value) }), _jsxs("div", { className: "space-y-2", children: [translatedField?.isComplete === false &&
599
+ translatedField?.missingSuggestions && (_jsxs("div", { className: "rounded border border-orange-200 bg-orange-50 p-2", children: [_jsxs("div", { className: "mb-1 flex items-center gap-1 text-xs font-medium text-orange-800", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "AI Suggestions for Missing Content:"] }), _jsx("p", { className: "text-xs text-orange-700 select-text", children: translatedField.missingSuggestions })] })), editingFields.has(`${itemId}-${field.fieldId}`) ? (_jsx("div", { children: isRichTextField(field.fieldType) ? (_jsx(SimpleRichTextEditor, { value: translatedField?.translatedValue || "", onChange: (value) => handleFieldChange(itemId, field.fieldId, value), readOnly: false, placeholder: "Enter translation here...", className: "min-h-[70px] text-xs" })) : (_jsx(Textarea, { value: translatedField?.translatedValue || "", onChange: (e) => handleFieldChange(itemId, field.fieldId, e.target.value), placeholder: "Enter translation here...", rows: 4, className: "h-full min-h-[70px] w-full text-xs" })) })) : (_jsx("div", { className: "h-full min-h-[70px] rounded border bg-gray-50 p-1 text-xs", children: translatedField?.translatedValue ? (isRichTextField(field.fieldType) ? (_jsx("div", { className: "rich-text-content", dangerouslySetInnerHTML: {
600
+ __html: translatedField.translatedValue,
601
+ } })) : (translatedField.translatedValue)) : (_jsx("span", { className: "text-gray-400", children: "Translation will appear here..." })) }))] })] })] }, field.fieldId));
602
+ }) })] }, itemId))) })) : (_jsx("div", { className: "py-8 text-center text-sm text-gray-500", children: Object.keys(translatableFields).length > 0
603
+ ? "Click 'Translate Fields' to start the translation process."
604
+ : "No translatable fields found." })) }), translatedCount > 0 &&
605
+ saveResults.success.length === 0 &&
606
+ saveResults.errors.length === 0 && (_jsx(Card, { title: "Save Translations", description: "Create language versions and apply translations", icon: _jsx(CheckCircle, { className: "h-5 w-5" }), children: _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "rounded-lg border border-blue-200 bg-blue-50 p-4", children: [_jsx("h4", { className: "mb-2 font-medium text-blue-900", children: "Ready to Save" }), _jsxs("p", { className: "text-sm text-blue-800", children: [translatedCount, " fields ready to be saved to", " ", _jsx("strong", { children: targetLanguage?.name }), " language", detectedLanguageName &&
607
+ targetLanguage?.name
608
+ ?.toLowerCase()
609
+ .includes(detectedLanguageName.toLowerCase()) && (_jsx("span", { className: "text-green-700", children: " (auto-detected)" })), ". This will update translated field values for all selected items."] })] }), _jsxs(ActionButton, { onClick: handleSaveTranslations, isLoading: isSaving, disabled: translatedCount === 0 || !targetLanguage?.languageCode, className: "w-full", variant: "outline", children: [_jsx(CheckCircle, { className: "mr-2 h-4 w-4" }), isSaving
610
+ ? "Saving Translations..."
611
+ : `Save ${translatedCount} Translations to ${targetLanguage?.name || "Target Language"}`] })] }) }))] }));
612
+ }
613
+ //# sourceMappingURL=TranslateStep.js.map