@parhelia/localization 0.1.12601 → 0.1.12602
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/LocalizeItemDialog.d.ts.map +1 -1
- package/dist/LocalizeItemDialog.js +17 -13
- package/dist/api/discovery.d.ts +4 -1
- package/dist/api/discovery.d.ts.map +1 -1
- package/dist/api/discovery.js +4 -3
- package/dist/hooks/useTranslationWizard.d.ts.map +1 -1
- package/dist/hooks/useTranslationWizard.js +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -13
- package/dist/services/translationService.d.ts +7 -48
- package/dist/services/translationService.d.ts.map +1 -1
- package/dist/services/translationService.js +0 -3
- package/dist/settings/TranslationServicesPanel.d.ts.map +1 -1
- package/dist/settings/TranslationServicesPanel.js +42 -111
- package/dist/setup/LocalizationSetupStep.d.ts.map +1 -1
- package/dist/setup/LocalizationSetupStep.js +7 -8
- package/dist/steps/ServiceLanguageSelectionStep.d.ts.map +1 -1
- package/dist/steps/ServiceLanguageSelectionStep.js +1 -18
- package/dist/steps/SubitemDiscoveryStep.d.ts.map +1 -1
- package/dist/steps/SubitemDiscoveryStep.js +181 -95
- package/dist/translation-center/BatchTranslationView.d.ts +1 -1
- package/dist/translation-center/BatchTranslationView.d.ts.map +1 -1
- package/dist/translation-center/BatchTranslationView.js +98 -356
- package/dist/translation-center/RecentTranslations.d.ts.map +1 -1
- package/dist/translation-center/RecentTranslations.js +13 -20
- package/dist/translation-center/TranslationManagement.d.ts.map +1 -1
- package/dist/translation-center/TranslationManagement.js +4 -2
- package/package.json +11 -10
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React, { useEffect, useState, useMemo, useRef, useCallback, memo, } from "react";
|
|
3
|
-
import { useEditContext, SimpleTable, Progress as CoreProgress, Button,
|
|
4
|
-
import { listBatchTranslationJobs,
|
|
5
|
-
import { ChevronDown, ChevronUp
|
|
6
|
-
import { toast } from "sonner";
|
|
3
|
+
import { useEditContext, SimpleTable, Progress as CoreProgress, Button, } from "@parhelia/core";
|
|
4
|
+
import { listBatchTranslationJobs, subscribeToBatch, unsubscribeFromBatch, getBatchInfo, getTranslationProviders } from "../services/translationService";
|
|
5
|
+
import { ChevronDown, ChevronUp } from "lucide-react";
|
|
7
6
|
import { JsonView, defaultStyles } from "react-json-view-lite";
|
|
8
7
|
import "react-json-view-lite/dist/index.css";
|
|
9
8
|
// Wrapper so React 19 sees a plain function component instead of a forwardRef exotic component.
|
|
@@ -11,12 +10,6 @@ const Progress = (props) => React.createElement(CoreProgress, props);
|
|
|
11
10
|
// Wrappers for lucide-react icons to work with React 19
|
|
12
11
|
const ChevronUpIcon = (props) => React.createElement(ChevronUp, props);
|
|
13
12
|
const ChevronDownIcon = (props) => React.createElement(ChevronDown, props);
|
|
14
|
-
const RefreshIcon = (props) => React.createElement(RefreshCw, props);
|
|
15
|
-
const ArrowLeftIcon = (props) => React.createElement(ArrowLeft, props);
|
|
16
|
-
const InfoIcon = (props) => React.createElement(Info, props);
|
|
17
|
-
const LoaderIcon = (props) => React.createElement(Loader2, props);
|
|
18
|
-
const LanguagesIcon = (props) => React.createElement(Languages, props);
|
|
19
|
-
const ExternalLinkIcon = (props) => React.createElement(ExternalLink, props);
|
|
20
13
|
const MemoizedJsonView = memo(({ data }) => (_jsx(JsonView, { data: data, shouldExpandNode: (level) => level < 2, style: defaultStyles })), (prevProps, nextProps) => {
|
|
21
14
|
return JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data);
|
|
22
15
|
});
|
|
@@ -24,66 +17,32 @@ const MemoizedJsonView = memo(({ data }) => (_jsx(JsonView, { data: data, should
|
|
|
24
17
|
function normalizeBatchInfo(raw) {
|
|
25
18
|
if (!raw)
|
|
26
19
|
return null;
|
|
27
|
-
const
|
|
20
|
+
const pick = (a, b) => (a !== undefined ? a : b);
|
|
21
|
+
const toNum = (v) => (typeof v === "number" ? v : v != null ? parseInt(v, 10) : undefined);
|
|
28
22
|
const info = {
|
|
29
|
-
batchId: raw?.batchId,
|
|
30
|
-
createdAtUtc: raw?.createdAtUtc,
|
|
31
|
-
startedAtUtc: raw?.startedAtUtc,
|
|
32
|
-
completedAtUtc: raw?.completedAtUtc,
|
|
33
|
-
initiatedByUser: raw?.initiatedByUser,
|
|
34
|
-
initiatorSessionId: raw?.initiatorSessionId,
|
|
35
|
-
provider: raw?.provider,
|
|
36
|
-
status: raw?.status,
|
|
37
|
-
expectedJobs: toNum(raw?.expectedJobs),
|
|
38
|
-
completedJobs: toNum(raw?.completedJobs),
|
|
39
|
-
errorJobs: toNum(raw?.errorJobs),
|
|
40
|
-
metadata: raw?.metadata,
|
|
41
|
-
lastUpdatedUtc: raw?.lastUpdatedUtc,
|
|
42
|
-
exists: raw?.exists,
|
|
23
|
+
batchId: pick(raw?.batchId, raw?.BatchId),
|
|
24
|
+
createdAtUtc: pick(raw?.createdAtUtc, raw?.CreatedAtUtc),
|
|
25
|
+
startedAtUtc: pick(raw?.startedAtUtc, raw?.StartedAtUtc),
|
|
26
|
+
completedAtUtc: pick(raw?.completedAtUtc, raw?.CompletedAtUtc),
|
|
27
|
+
initiatedByUser: pick(raw?.initiatedByUser, raw?.InitiatedByUser),
|
|
28
|
+
initiatorSessionId: pick(raw?.initiatorSessionId, raw?.InitiatorSessionId),
|
|
29
|
+
provider: pick(raw?.provider, raw?.Provider),
|
|
30
|
+
status: pick(raw?.status, raw?.Status),
|
|
31
|
+
expectedJobs: toNum(pick(raw?.expectedJobs, raw?.ExpectedJobs)),
|
|
32
|
+
completedJobs: toNum(pick(raw?.completedJobs, raw?.CompletedJobs)),
|
|
33
|
+
errorJobs: toNum(pick(raw?.errorJobs, raw?.ErrorJobs)),
|
|
34
|
+
metadata: pick(raw?.metadata, raw?.Metadata),
|
|
35
|
+
lastUpdatedUtc: pick(raw?.lastUpdatedUtc, raw?.LastUpdatedUtc),
|
|
36
|
+
exists: pick(raw?.exists, raw?.Exists),
|
|
43
37
|
};
|
|
44
38
|
return info.batchId ? info : null;
|
|
45
39
|
}
|
|
46
|
-
function
|
|
47
|
-
return (itemId ?? "").toString().toLowerCase();
|
|
48
|
-
}
|
|
49
|
-
function getJobKey(itemId, language) {
|
|
50
|
-
return `${normalizeJobItemId(itemId)}-${(language ?? "").toString()}`;
|
|
51
|
-
}
|
|
52
|
-
function getSocketPayloadMessage(payload) {
|
|
53
|
-
return payload?.message || payload?.error || "";
|
|
54
|
-
}
|
|
55
|
-
function normalizeJobRecord(raw) {
|
|
56
|
-
return {
|
|
57
|
-
id: typeof raw?.id === "number"
|
|
58
|
-
? raw.id
|
|
59
|
-
: typeof raw?.Id === "number"
|
|
60
|
-
? raw.Id
|
|
61
|
-
: undefined,
|
|
62
|
-
itemId: normalizeJobItemId(raw?.itemId ?? raw?.ItemId),
|
|
63
|
-
targetLanguage: raw?.targetLanguage ?? raw?.TargetLanguage ?? "",
|
|
64
|
-
sourceLanguage: raw?.sourceLanguage ?? raw?.SourceLanguage ?? "",
|
|
65
|
-
status: raw?.status ?? raw?.Status ?? "",
|
|
66
|
-
timestamp: raw?.timestamp ?? raw?.Timestamp ?? "",
|
|
67
|
-
hash: raw?.hash ?? raw?.Hash,
|
|
68
|
-
message: raw?.message ?? raw?.Message,
|
|
69
|
-
batchId: raw?.batchId ?? raw?.BatchId,
|
|
70
|
-
metadata: raw?.metadata ?? raw?.Metadata ?? null,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
function getRetryJobKey(job) {
|
|
74
|
-
return `${job.id ?? "no-id"}|${normalizeJobItemId(job.itemId)}|${job.sourceLanguage}|${job.targetLanguage}`;
|
|
75
|
-
}
|
|
76
|
-
export function BatchTranslationView({ batchId, onBack, }) {
|
|
40
|
+
export function BatchTranslationView({ batchId, onBack }) {
|
|
77
41
|
const editContext = useEditContext();
|
|
78
|
-
const router = useRouter();
|
|
79
|
-
const pathname = usePathname();
|
|
80
|
-
const searchParams = useSearchParams();
|
|
81
42
|
const [batchJobs, setBatchJobs] = useState([]);
|
|
82
43
|
const [batchInfo, setBatchInfo] = useState(null);
|
|
83
44
|
const [providers, setProviders] = useState([]);
|
|
84
45
|
const [isLoading, setIsLoading] = useState(false);
|
|
85
|
-
const [isRetryingAll, setIsRetryingAll] = useState(false);
|
|
86
|
-
const [retryingJobKeys, setRetryingJobKeys] = useState(new Set());
|
|
87
46
|
const [translationProgress, setTranslationProgress] = useState(new Map());
|
|
88
47
|
const [itemNames, setItemNames] = useState(new Map());
|
|
89
48
|
const [isMetadataExpanded, setIsMetadataExpanded] = useState(false);
|
|
@@ -97,94 +56,10 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
97
56
|
// Helper function to get display name from service name
|
|
98
57
|
const getProviderDisplayName = useCallback((serviceName) => {
|
|
99
58
|
if (!serviceName)
|
|
100
|
-
return
|
|
101
|
-
const provider = providers.find(
|
|
59
|
+
return 'Unknown';
|
|
60
|
+
const provider = providers.find(p => p.name === serviceName);
|
|
102
61
|
return provider?.displayName || serviceName;
|
|
103
62
|
}, [providers]);
|
|
104
|
-
const openItemInEditor = useCallback((itemId, language) => {
|
|
105
|
-
editContext?.loadItem({
|
|
106
|
-
id: normalizeJobItemId(itemId),
|
|
107
|
-
language,
|
|
108
|
-
version: 0,
|
|
109
|
-
});
|
|
110
|
-
editContext?.switchWorkspace?.("editor");
|
|
111
|
-
}, [editContext]);
|
|
112
|
-
const openBatchInView = useCallback((nextBatchId) => {
|
|
113
|
-
const current = new URLSearchParams(searchParams.toString());
|
|
114
|
-
current.set("batchId", nextBatchId);
|
|
115
|
-
router.push(`${pathname}?${current.toString()}`, { scroll: false });
|
|
116
|
-
}, [pathname, router, searchParams]);
|
|
117
|
-
const buildRetryJobs = useCallback((jobs) => {
|
|
118
|
-
const uniqueJobs = new Map();
|
|
119
|
-
for (const job of jobs) {
|
|
120
|
-
if (job.status !== "Error")
|
|
121
|
-
continue;
|
|
122
|
-
const key = getRetryJobKey(job);
|
|
123
|
-
if (uniqueJobs.has(key))
|
|
124
|
-
continue;
|
|
125
|
-
uniqueJobs.set(key, {
|
|
126
|
-
sourceTranslationId: job.id,
|
|
127
|
-
itemId: normalizeJobItemId(job.itemId),
|
|
128
|
-
sourceLanguage: job.sourceLanguage,
|
|
129
|
-
targetLanguage: job.targetLanguage,
|
|
130
|
-
metadata: job.metadata ?? undefined,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
return Array.from(uniqueJobs.values());
|
|
134
|
-
}, []);
|
|
135
|
-
const handleRetryJobs = useCallback(async (jobs, retryAll) => {
|
|
136
|
-
const retryJobs = buildRetryJobs(jobs);
|
|
137
|
-
if (retryJobs.length === 0) {
|
|
138
|
-
toast.error("No failed translations available to retry.");
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (!editContext?.sessionId) {
|
|
142
|
-
toast.error("Cannot retry without an active session.");
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
if (!batchInfo?.provider) {
|
|
146
|
-
toast.error("Retry provider could not be resolved for this batch.");
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
if (retryAll) {
|
|
150
|
-
setIsRetryingAll(true);
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
setRetryingJobKeys(new Set(jobs.map((job) => getRetryJobKey(job)).filter((key) => retryJobs.some((retryJob) => key ===
|
|
154
|
-
`${retryJob.sourceTranslationId ?? "no-id"}|${retryJob.itemId}|${retryJob.sourceLanguage}|${retryJob.targetLanguage}`))));
|
|
155
|
-
}
|
|
156
|
-
try {
|
|
157
|
-
const result = await retryBatchTranslation({
|
|
158
|
-
sessionId: editContext.sessionId,
|
|
159
|
-
sourceBatchId: batchId,
|
|
160
|
-
provider: batchInfo.provider,
|
|
161
|
-
jobs: retryJobs,
|
|
162
|
-
});
|
|
163
|
-
if (result.type !== "success" || !result.data) {
|
|
164
|
-
toast.error(result.details ||
|
|
165
|
-
result.summary ||
|
|
166
|
-
"Failed to start retry translations.");
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
const response = result.data;
|
|
170
|
-
const startedCount = response.started?.length ?? 0;
|
|
171
|
-
const skippedCount = response.skipped?.length ?? 0;
|
|
172
|
-
if (startedCount === 0) {
|
|
173
|
-
toast.error(skippedCount > 0
|
|
174
|
-
? response.skipped[0]?.error || "No retries were started."
|
|
175
|
-
: "No retries were started.");
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
toast.success(retryAll
|
|
179
|
-
? `Started retry batch with ${startedCount} translation${startedCount === 1 ? "" : "s"}.`
|
|
180
|
-
: "Started retry batch for failed translation.");
|
|
181
|
-
openBatchInView(response.retryBatchId);
|
|
182
|
-
}
|
|
183
|
-
finally {
|
|
184
|
-
setIsRetryingAll(false);
|
|
185
|
-
setRetryingJobKeys(new Set());
|
|
186
|
-
}
|
|
187
|
-
}, [batchId, batchInfo?.provider, buildRetryJobs, editContext?.sessionId, openBatchInView]);
|
|
188
63
|
// Parse metadata for display
|
|
189
64
|
const parsedMetadata = useMemo(() => {
|
|
190
65
|
if (!batchInfo?.metadata)
|
|
@@ -203,9 +78,7 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
203
78
|
const loadProviders = useCallback(async () => {
|
|
204
79
|
try {
|
|
205
80
|
const res = await getTranslationProviders();
|
|
206
|
-
const providerData = (res?.data ??
|
|
207
|
-
res ??
|
|
208
|
-
[]);
|
|
81
|
+
const providerData = (res?.data ?? res ?? []);
|
|
209
82
|
setProviders(providerData);
|
|
210
83
|
}
|
|
211
84
|
catch (error) {
|
|
@@ -217,45 +90,36 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
217
90
|
try {
|
|
218
91
|
// Use efficient batch-specific endpoint
|
|
219
92
|
const res = await listBatchTranslationJobs(batchId);
|
|
220
|
-
const
|
|
221
|
-
const apiJobs = apiJobsRaw.map(normalizeJobRecord);
|
|
93
|
+
const apiJobs = (res?.data ?? res ?? []);
|
|
222
94
|
// Merge API response with existing state to prevent flickering
|
|
223
|
-
setBatchJobs(
|
|
95
|
+
setBatchJobs(prevJobs => {
|
|
224
96
|
// Create a map of existing jobs keyed by itemId-targetLanguage
|
|
225
97
|
const existingMap = new Map();
|
|
226
98
|
for (const job of prevJobs) {
|
|
227
|
-
const key =
|
|
99
|
+
const key = `${job.itemId}-${job.targetLanguage}`;
|
|
228
100
|
existingMap.set(key, job);
|
|
229
101
|
}
|
|
230
102
|
// Merge API jobs into the map (API data takes precedence)
|
|
231
103
|
for (const apiJob of apiJobs) {
|
|
232
|
-
const
|
|
233
|
-
const key = getJobKey(normalizedJob.itemId, normalizedJob.targetLanguage);
|
|
104
|
+
const key = `${apiJob.itemId}-${apiJob.targetLanguage}`;
|
|
234
105
|
const existingJob = existingMap.get(key);
|
|
235
106
|
if (existingJob) {
|
|
236
107
|
// Prefer API data if timestamp is newer or status changed
|
|
237
|
-
const apiTimestamp =
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const existingTimestamp = existingJob.timestamp
|
|
241
|
-
? new Date(existingJob.timestamp).getTime()
|
|
242
|
-
: 0;
|
|
243
|
-
if (apiTimestamp >= existingTimestamp ||
|
|
244
|
-
normalizedJob.status !== existingJob.status) {
|
|
108
|
+
const apiTimestamp = apiJob.timestamp ? new Date(apiJob.timestamp).getTime() : 0;
|
|
109
|
+
const existingTimestamp = existingJob.timestamp ? new Date(existingJob.timestamp).getTime() : 0;
|
|
110
|
+
if (apiTimestamp >= existingTimestamp || apiJob.status !== existingJob.status) {
|
|
245
111
|
// Preserve sourceLanguage from API if available, otherwise keep existing
|
|
246
112
|
existingMap.set(key, {
|
|
247
|
-
...
|
|
248
|
-
sourceLanguage:
|
|
249
|
-
existingJob.sourceLanguage ||
|
|
250
|
-
"",
|
|
113
|
+
...apiJob,
|
|
114
|
+
sourceLanguage: apiJob.sourceLanguage || existingJob.sourceLanguage || ""
|
|
251
115
|
});
|
|
252
116
|
}
|
|
253
117
|
}
|
|
254
118
|
else {
|
|
255
119
|
// New job from API - ensure sourceLanguage is set
|
|
256
120
|
existingMap.set(key, {
|
|
257
|
-
...
|
|
258
|
-
sourceLanguage:
|
|
121
|
+
...apiJob,
|
|
122
|
+
sourceLanguage: apiJob.sourceLanguage || ""
|
|
259
123
|
});
|
|
260
124
|
}
|
|
261
125
|
}
|
|
@@ -267,7 +131,7 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
267
131
|
return mergedJobs;
|
|
268
132
|
}
|
|
269
133
|
// Deep comparison: check if any job changed
|
|
270
|
-
const prevMap = new Map(prevJobs.map(
|
|
134
|
+
const prevMap = new Map(prevJobs.map(j => [`${j.itemId}-${j.targetLanguage}`, j]));
|
|
271
135
|
let hasChanges = false;
|
|
272
136
|
for (const mergedJob of mergedJobs) {
|
|
273
137
|
const key = `${mergedJob.itemId}-${mergedJob.targetLanguage}`;
|
|
@@ -317,7 +181,7 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
317
181
|
const info = normalizeBatchInfo(raw);
|
|
318
182
|
if (!cancelled && info)
|
|
319
183
|
setBatchInfo(info);
|
|
320
|
-
const isTerminal = info?.status ===
|
|
184
|
+
const isTerminal = info?.status === 'Completed' || info?.status === 'Error';
|
|
321
185
|
// Conditionally subscribe only for non-terminal batches
|
|
322
186
|
if (!isTerminal && editContext?.sessionId) {
|
|
323
187
|
await subscribeToBatch(batchId, editContext.sessionId);
|
|
@@ -340,10 +204,7 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
340
204
|
init();
|
|
341
205
|
return () => {
|
|
342
206
|
// Cleanup: unsubscribe when component unmounts or batch changes
|
|
343
|
-
if (isSubscribedRef.current &&
|
|
344
|
-
editContext &&
|
|
345
|
-
editContext.sessionId &&
|
|
346
|
-
batchId) {
|
|
207
|
+
if (isSubscribedRef.current && editContext && editContext.sessionId && batchId) {
|
|
347
208
|
unsubscribeFromBatch(batchId, editContext.sessionId).catch(console.error);
|
|
348
209
|
}
|
|
349
210
|
};
|
|
@@ -365,10 +226,8 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
365
226
|
// Only update if this message is for our batch
|
|
366
227
|
if (message.payload.batchId === batchId) {
|
|
367
228
|
// Update the specific job in our local state instead of refetching all jobs
|
|
368
|
-
setBatchJobs(
|
|
369
|
-
const
|
|
370
|
-
const payloadMessage = getSocketPayloadMessage(message.payload);
|
|
371
|
-
const jobIndex = prevJobs.findIndex((job) => normalizeJobItemId(job.itemId) === normalizedItemId &&
|
|
229
|
+
setBatchJobs(prevJobs => {
|
|
230
|
+
const jobIndex = prevJobs.findIndex(job => job.itemId === message.payload.itemId &&
|
|
372
231
|
job.targetLanguage === message.payload.language);
|
|
373
232
|
if (jobIndex >= 0) {
|
|
374
233
|
// Update existing job
|
|
@@ -377,44 +236,41 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
377
236
|
if (existingJob) {
|
|
378
237
|
updatedJobs[jobIndex] = {
|
|
379
238
|
...existingJob,
|
|
380
|
-
itemId: normalizedItemId,
|
|
381
239
|
status: message.payload.status,
|
|
382
240
|
timestamp: message.payload.timestamp || existingJob.timestamp,
|
|
383
|
-
message:
|
|
241
|
+
message: message.payload.message || existingJob.message,
|
|
384
242
|
// Preserve sourceLanguage from existing job if not provided in message
|
|
385
|
-
sourceLanguage: message.payload.sourceLanguage ||
|
|
386
|
-
existingJob.sourceLanguage ||
|
|
387
|
-
"",
|
|
243
|
+
sourceLanguage: message.payload.sourceLanguage || existingJob.sourceLanguage || ""
|
|
388
244
|
};
|
|
389
245
|
}
|
|
390
246
|
return updatedJobs;
|
|
391
247
|
}
|
|
392
|
-
else {
|
|
393
|
-
// Add new job if it doesn't exist
|
|
248
|
+
else if (message.type === "translation-started") {
|
|
249
|
+
// Add new job if it doesn't exist (shouldn't happen with batch subscriptions, but safe fallback)
|
|
394
250
|
const newJob = {
|
|
395
|
-
itemId:
|
|
251
|
+
itemId: message.payload.itemId,
|
|
396
252
|
targetLanguage: message.payload.language,
|
|
397
253
|
sourceLanguage: message.payload.sourceLanguage || "", // Keep this, but we'll try to populate from API
|
|
398
254
|
status: message.payload.status,
|
|
399
255
|
timestamp: message.payload.timestamp || new Date().toISOString(),
|
|
400
256
|
batchId: batchId,
|
|
401
|
-
message:
|
|
257
|
+
message: message.payload.message || ""
|
|
402
258
|
};
|
|
403
259
|
return [...prevJobs, newJob];
|
|
404
260
|
}
|
|
261
|
+
return prevJobs;
|
|
405
262
|
});
|
|
406
263
|
}
|
|
407
264
|
}
|
|
408
265
|
if (message.type === "translation-progress") {
|
|
409
266
|
// Only track progress for jobs in this batch
|
|
410
267
|
if (message.payload.batchId === batchId) {
|
|
411
|
-
const itemId =
|
|
268
|
+
const itemId = (message.payload.itemId ?? "").toString().toLowerCase();
|
|
412
269
|
const language = message.payload.language;
|
|
413
|
-
const progressKey =
|
|
270
|
+
const progressKey = `${itemId}-${language}`;
|
|
414
271
|
// Ensure a row exists; if not, add a placeholder "In Progress" job
|
|
415
|
-
setBatchJobs(
|
|
416
|
-
const idx = prevJobs.findIndex(
|
|
417
|
-
j.targetLanguage === language);
|
|
272
|
+
setBatchJobs(prevJobs => {
|
|
273
|
+
const idx = prevJobs.findIndex(j => j.itemId === itemId && j.targetLanguage === language);
|
|
418
274
|
if (idx >= 0)
|
|
419
275
|
return prevJobs;
|
|
420
276
|
const newJob = {
|
|
@@ -424,7 +280,7 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
424
280
|
status: "In Progress",
|
|
425
281
|
timestamp: new Date().toISOString(),
|
|
426
282
|
batchId: batchId,
|
|
427
|
-
message: message.payload.message || ""
|
|
283
|
+
message: message.payload.message || ""
|
|
428
284
|
};
|
|
429
285
|
return [...prevJobs, newJob];
|
|
430
286
|
});
|
|
@@ -432,15 +288,14 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
432
288
|
const next = new Map(prev);
|
|
433
289
|
next.set(progressKey, {
|
|
434
290
|
progress: message.payload.progress || 0,
|
|
435
|
-
message: message.payload.message || ""
|
|
291
|
+
message: message.payload.message || ""
|
|
436
292
|
});
|
|
437
293
|
return next;
|
|
438
294
|
});
|
|
439
295
|
}
|
|
440
296
|
}
|
|
441
297
|
// Handle batch-completed messages for this specific batch
|
|
442
|
-
if (message.type === "batch-completed" &&
|
|
443
|
-
message.payload.batchId === batchId) {
|
|
298
|
+
if (message.type === "batch-completed" && message.payload.batchId === batchId) {
|
|
444
299
|
// Refresh header info only (jobs are already updated via websocket)
|
|
445
300
|
(async () => {
|
|
446
301
|
try {
|
|
@@ -463,7 +318,7 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
463
318
|
}, [editContext, batchId]);
|
|
464
319
|
// Adaptive polling while batch is in progress - only as fallback when websockets are silent
|
|
465
320
|
useEffect(() => {
|
|
466
|
-
const isRunning = batchInfo?.status ===
|
|
321
|
+
const isRunning = batchInfo?.status === 'In Progress';
|
|
467
322
|
if (!isRunning)
|
|
468
323
|
return;
|
|
469
324
|
const pollBatchInfo = async () => {
|
|
@@ -501,7 +356,7 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
501
356
|
}
|
|
502
357
|
const fetchItemNames = async () => {
|
|
503
358
|
// Extract unique item IDs from batchJobs
|
|
504
|
-
const uniqueItemIds = Array.from(new Set(batchJobs.map(
|
|
359
|
+
const uniqueItemIds = Array.from(new Set(batchJobs.map(job => job.itemId)));
|
|
505
360
|
if (uniqueItemIds.length === 0) {
|
|
506
361
|
return;
|
|
507
362
|
}
|
|
@@ -509,7 +364,7 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
509
364
|
const language = editContext.currentItemDescriptor?.language || "en";
|
|
510
365
|
try {
|
|
511
366
|
// Create ItemDescriptors for fetching
|
|
512
|
-
const itemDescriptors = uniqueItemIds.map(
|
|
367
|
+
const itemDescriptors = uniqueItemIds.map(itemId => ({
|
|
513
368
|
id: itemId,
|
|
514
369
|
language: language,
|
|
515
370
|
version: 0, // Latest version
|
|
@@ -523,7 +378,7 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
523
378
|
namesMap.set(stub.id.toLowerCase(), stub.name);
|
|
524
379
|
}
|
|
525
380
|
}
|
|
526
|
-
setItemNames(
|
|
381
|
+
setItemNames(prevNames => {
|
|
527
382
|
// Merge with existing names, only update if there are changes
|
|
528
383
|
let hasChanges = false;
|
|
529
384
|
const merged = new Map(prevNames);
|
|
@@ -542,11 +397,7 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
542
397
|
}
|
|
543
398
|
};
|
|
544
399
|
fetchItemNames();
|
|
545
|
-
}, [
|
|
546
|
-
batchJobs,
|
|
547
|
-
editContext?.itemsRepository,
|
|
548
|
-
editContext?.currentItemDescriptor?.language,
|
|
549
|
-
]);
|
|
400
|
+
}, [batchJobs, editContext?.itemsRepository, editContext?.currentItemDescriptor?.language]);
|
|
550
401
|
// Group jobs by item for better organization
|
|
551
402
|
const itemGroups = useMemo(() => {
|
|
552
403
|
const groups = {};
|
|
@@ -564,9 +415,9 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
564
415
|
return entries.sort(([, jobsA], [, jobsB]) => {
|
|
565
416
|
// Calculate status priority for each item group
|
|
566
417
|
const getStatusPriority = (jobs) => {
|
|
567
|
-
const hasInProgress = jobs.some(
|
|
568
|
-
const hasError = jobs.some(
|
|
569
|
-
const allCompleted = jobs.every(
|
|
418
|
+
const hasInProgress = jobs.some(j => j.status === "In Progress");
|
|
419
|
+
const hasError = jobs.some(j => j.status === "Error");
|
|
420
|
+
const allCompleted = jobs.every(j => j.status === "Completed");
|
|
570
421
|
if (hasInProgress)
|
|
571
422
|
return 1; // Highest priority - show first
|
|
572
423
|
if (hasError)
|
|
@@ -583,7 +434,7 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
583
434
|
// Calculate overall progress segments including in-progress job progress.
|
|
584
435
|
// Incorporate server-side expected/completed/error counts so pending server jobs are reflected.
|
|
585
436
|
const progressSegments = useMemo(() => {
|
|
586
|
-
const expectedTotal = typeof batchInfo?.expectedJobs ===
|
|
437
|
+
const expectedTotal = typeof batchInfo?.expectedJobs === 'number' && batchInfo.expectedJobs > 0
|
|
587
438
|
? batchInfo.expectedJobs
|
|
588
439
|
: batchJobs.length;
|
|
589
440
|
if (expectedTotal === 0) {
|
|
@@ -597,22 +448,18 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
597
448
|
errorCount: 0,
|
|
598
449
|
pendingCount: 0,
|
|
599
450
|
total: 0,
|
|
600
|
-
actualProgress: 0
|
|
451
|
+
actualProgress: 0
|
|
601
452
|
};
|
|
602
453
|
}
|
|
603
|
-
const localCompleted = batchJobs.filter(
|
|
604
|
-
const localErrors = batchJobs.filter(
|
|
605
|
-
const serverCompleted = typeof batchInfo?.completedJobs ===
|
|
606
|
-
|
|
607
|
-
: undefined;
|
|
608
|
-
const serverErrors = typeof batchInfo?.errorJobs === "number"
|
|
609
|
-
? batchInfo.errorJobs
|
|
610
|
-
: undefined;
|
|
454
|
+
const localCompleted = batchJobs.filter(job => job.status === 'Completed').length;
|
|
455
|
+
const localErrors = batchJobs.filter(job => job.status === 'Error').length;
|
|
456
|
+
const serverCompleted = typeof batchInfo?.completedJobs === 'number' ? batchInfo.completedJobs : undefined;
|
|
457
|
+
const serverErrors = typeof batchInfo?.errorJobs === 'number' ? batchInfo.errorJobs : undefined;
|
|
611
458
|
// Prefer the larger of local/server so UI can progress smoothly with local updates
|
|
612
459
|
const completedCount = Math.max(0, Math.min(expectedTotal, Math.max(localCompleted, serverCompleted ?? 0)));
|
|
613
460
|
const errorCount = Math.max(0, Math.min(expectedTotal - completedCount, Math.max(localErrors, serverErrors ?? 0)));
|
|
614
461
|
// Known in-progress from client state
|
|
615
|
-
const knownInProgressCount = batchJobs.filter(
|
|
462
|
+
const knownInProgressCount = batchJobs.filter(job => job.status === 'In Progress').length;
|
|
616
463
|
// Cap to not exceed remaining after completed+error
|
|
617
464
|
const inProgressCount = Math.max(0, Math.min(knownInProgressCount, expectedTotal - completedCount - errorCount));
|
|
618
465
|
const pendingCount = Math.max(0, expectedTotal - completedCount - errorCount - inProgressCount);
|
|
@@ -620,8 +467,8 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
620
467
|
// Actual progress: completed=100 each, in-progress use live progress if available, others 0
|
|
621
468
|
let totalActualProgress = completedCount * 100;
|
|
622
469
|
for (const job of batchJobs) {
|
|
623
|
-
if (job.status ===
|
|
624
|
-
const progressKey =
|
|
470
|
+
if (job.status === 'In Progress') {
|
|
471
|
+
const progressKey = `${job.itemId}-${job.targetLanguage}`;
|
|
625
472
|
const jobProgress = translationProgress.get(progressKey);
|
|
626
473
|
totalActualProgress += jobProgress?.progress || 0;
|
|
627
474
|
}
|
|
@@ -642,7 +489,6 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
642
489
|
return segments;
|
|
643
490
|
}, [batchJobs, batchInfo, translationProgress]);
|
|
644
491
|
const overallProgress = progressSegments.actualProgress;
|
|
645
|
-
const isMobile = editContext?.isMobile ?? false;
|
|
646
492
|
// Treat the batch as Completed if all jobs are completed and there are no errors/in-progress,
|
|
647
493
|
// even if the backend status is still "In Progress".
|
|
648
494
|
const effectiveStatus = batchInfo?.status === "In Progress" &&
|
|
@@ -671,38 +517,14 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
671
517
|
catch { }
|
|
672
518
|
})();
|
|
673
519
|
}, [effectiveStatus, batchId, loadBatchJobs]);
|
|
674
|
-
return (_jsxs("div", { className: "flex h-full flex-col min-h-0 bg-gray-5", "data-testid": "batch-translation-view", children: [_jsxs("div", { className: "shrink-0 border-b border-gray-3 bg-background p-4 md:p-6", children: [_jsxs("div", { className:
|
|
675
|
-
?
|
|
676
|
-
: effectiveStatus ===
|
|
677
|
-
?
|
|
678
|
-
: effectiveStatus ===
|
|
679
|
-
?
|
|
680
|
-
:
|
|
681
|
-
|
|
682
|
-
: undefined, children: effectiveStatus })), batchInfo.provider && (_jsxs("span", { className: "text-(--color-gray-1)", children: ["Provider:", " ", _jsx("span", { className: "font-medium", children: getProviderDisplayName(batchInfo.provider) })] })), batchInfo.initiatedByUser && (_jsxs("span", { className: "text-(--color-gray-1)", children: ["By:", " ", _jsx("span", { className: "font-medium", children: batchInfo.initiatedByUser })] })), (batchInfo.startedAtUtc ||
|
|
683
|
-
batchInfo.completedAtUtc ||
|
|
684
|
-
batchInfo.lastUpdatedUtc) && (_jsxs("span", { className: "text-gray-2", children: [batchInfo.startedAtUtc && (_jsxs(_Fragment, { children: ["Started:", " ", new Date(batchInfo.startedAtUtc).toLocaleString()] })), batchInfo.completedAtUtc && (_jsxs(_Fragment, { children: [batchInfo.startedAtUtc ? " • " : "", "Completed:", " ", new Date(batchInfo.completedAtUtc).toLocaleString()] })), !batchInfo.completedAtUtc &&
|
|
685
|
-
batchInfo.lastUpdatedUtc && (_jsxs(_Fragment, { children: [batchInfo.startedAtUtc ? " • " : "", "Updated:", " ", new Date(batchInfo.lastUpdatedUtc).toLocaleString()] }))] }))] }))] })] }), isMobile && batchInfo && (_jsxs("div", { className: "grid grid-cols-2 gap-x-4 gap-y-3 w-full mt-1 border-t border-gray-3 pt-3", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-[10px] text-gray-2 uppercase font-bold tracking-wider", children: "Status" }), effectiveStatus && (_jsx("span", { "data-testid": "translation-job-status", className: `inline-flex items-center self-start rounded-full px-2 py-0.5 text-[10px] font-medium ${effectiveStatus === "Completed"
|
|
686
|
-
? "bg-green-100 text-green-700"
|
|
687
|
-
: effectiveStatus === "In Progress"
|
|
688
|
-
? ""
|
|
689
|
-
: effectiveStatus === "Error"
|
|
690
|
-
? "bg-red-100 text-red-600"
|
|
691
|
-
: "bg-gray-4 text-(--color-gray-1)"}`, style: effectiveStatus === "In Progress"
|
|
692
|
-
? { backgroundColor: "#f6eeff", color: "#9650fb" }
|
|
693
|
-
: undefined, children: effectiveStatus }))] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-[10px] text-gray-2 uppercase font-bold tracking-wider", children: "Provider" }), _jsx("span", { className: "text-xs font-medium text-(--color-gray-1) truncate", children: getProviderDisplayName(batchInfo.provider) })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-[10px] text-gray-2 uppercase font-bold tracking-wider", children: "Initiated By" }), _jsx("span", { className: "text-xs font-medium text-(--color-gray-1) truncate", children: batchInfo.initiatedByUser || "System" })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-[10px] text-gray-2 uppercase font-bold tracking-wider", children: "Activity" }), _jsx("span", { className: "text-[10px] text-gray-2 leading-tight", children: batchInfo.completedAtUtc ? (_jsxs(_Fragment, { children: ["Done:", " ", new Date(batchInfo.completedAtUtc).toLocaleTimeString()] })) : batchInfo.startedAtUtc ? (_jsxs(_Fragment, { children: ["Started:", " ", new Date(batchInfo.startedAtUtc).toLocaleTimeString()] })) : batchInfo.lastUpdatedUtc ? (_jsxs(_Fragment, { children: ["Updated:", " ", new Date(batchInfo.lastUpdatedUtc).toLocaleTimeString()] })) : (_jsx(_Fragment, { children: "Created" })) })] })] })), _jsxs("div", { className: `flex items-center ${isMobile ? "w-full justify-between border-t border-gray-3 pt-3 mt-1" : "gap-3"}`, children: [_jsxs("div", { className: "text-xs md:text-sm text-gray-2", children: [_jsx("span", { className: "font-semibold text-(--color-dark)", children: progressSegments.completedCount }), "/", progressSegments.total, " completed", progressSegments.errorCount
|
|
694
|
-
? `, ${progressSegments.errorCount} errors`
|
|
695
|
-
: ""] }), _jsxs("div", { className: "flex items-center gap-2", children: [progressSegments.errorCount > 0 && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => void handleRetryJobs(batchJobs.filter((job) => job.status === "Error"), true), disabled: isRetryingAll || isLoading, className: "md:w-auto h-8 px-3", children: [_jsx(RefreshIcon, { className: `h-4 w-4 ${isRetryingAll ? "animate-spin" : ""}` }), _jsx("span", { children: "Retry All Failed" })] })), _jsxs(Button, { size: "sm", variant: "outline", onClick: loadBatchJobs, disabled: isLoading || isRetryingAll, title: "Manual refresh - normally updates come through websocket subscriptions", className: "md:w-auto w-8 h-8 md:h-8 p-0 md:px-3", children: [_jsx(RefreshIcon, { className: `h-4 w-4 ${isLoading ? "animate-spin" : ""}` }), _jsx("span", { className: "hidden md:inline", children: "Refresh" })] })] })] })] }), parsedMetadata && (_jsxs("div", { className: `border-t border-gray-3 bg-gray-5 mt-4 rounded-b-lg ${isMobile ? "-mx-4 border-x-0" : ""}`, children: [_jsxs("button", { onClick: () => setIsMetadataExpanded(!isMetadataExpanded), className: `flex w-full cursor-pointer items-center justify-between ${isMobile ? "px-4 py-3" : "px-4 py-2.5"} text-left transition-colors hover:bg-gray-4`, children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(InfoIcon, { className: "h-4 w-4 text-gray-2" }), _jsx("span", { className: "text-xs font-medium text-(--color-gray-1)", children: "Batch Metadata" })] }), isMetadataExpanded ? (_jsx(ChevronUpIcon, { className: "h-4 w-4 text-gray-2", strokeWidth: 1 })) : (_jsx(ChevronDownIcon, { className: "h-4 w-4 text-gray-2", strokeWidth: 1 }))] }), isMetadataExpanded && (_jsx("div", { className: "max-h-96 overflow-y-auto px-4 pb-3", children: _jsx("div", { className: "rounded-lg border border-gray-3 bg-background p-3 text-xs shadow-sm mt-2", children: _jsx(MemoizedJsonView, { data: parsedMetadata }) }) }))] })), _jsxs("div", { className: "mt-6 md:mt-4", children: [_jsxs("div", { className: "flex items-center justify-between mb-2", children: [_jsx("span", { className: "text-sm font-medium text-(--color-dark)", children: "Overall Progress" }), _jsxs("span", { className: "text-sm text-gray-2", children: [_jsx("span", { className: "font-semibold text-(--color-dark)", children: progressSegments.completedCount }), " ", "/ ", progressSegments.total, " completed"] })] }), _jsxs("div", { className: `relative ${isMobile ? "h-5" : "h-4"} rounded-full overflow-hidden bg-gray-3`, children: [_jsxs("div", { className: "absolute inset-0 flex", children: [progressSegments.completed > 0 && (_jsx("div", { className: "h-full transition-all duration-300", style: {
|
|
696
|
-
width: `${progressSegments.completed}%`,
|
|
697
|
-
backgroundColor: "#8ae048",
|
|
698
|
-
} })), progressSegments.inProgress > 0 && (_jsx("div", { className: "h-full transition-all duration-300", style: {
|
|
699
|
-
width: `${progressSegments.inProgress}%`,
|
|
700
|
-
backgroundColor: "#9650fb",
|
|
701
|
-
} })), progressSegments.error > 0 && (_jsx("div", { className: "bg-destructive h-full transition-all duration-300", style: { width: `${progressSegments.error}%` } })), progressSegments.pending > 0 && (_jsx("div", { className: "h-full transition-all duration-300", style: {
|
|
702
|
-
width: `${progressSegments.pending}%`,
|
|
703
|
-
backgroundColor: "var(--color-gray-4)",
|
|
704
|
-
} }))] }), _jsx("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none", children: _jsxs("span", { className: `text-xs font-bold text-white drop-shadow-sm ${isMobile ? "-mt-1" : ""}`, children: [overallProgress, "%"] }) })] }), _jsxs("div", { className: `flex flex-wrap ${isMobile ? "gap-x-4 gap-y-2" : "gap-3"} mt-3 md:mt-2 text-[10px] md:text-xs`, children: [progressSegments.completedCount > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "w-2.5 h-2.5 rounded-sm", style: { backgroundColor: "#8ae048" } }), _jsxs("span", { className: "text-(--color-gray-1)", children: [progressSegments.completedCount, " completed"] })] })), progressSegments.inProgressCount > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "w-2.5 h-2.5 rounded-sm", style: { backgroundColor: "#9650fb" } }), _jsxs("span", { className: "text-(--color-gray-1)", children: [progressSegments.inProgressCount, " in progress"] })] })), progressSegments.errorCount > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "w-2.5 h-2.5 bg-destructive rounded-sm" }), _jsxs("span", { className: "text-(--color-gray-1)", children: [progressSegments.errorCount, " errors"] })] })), progressSegments.pendingCount > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "w-2.5 h-2.5 bg-gray-4 rounded-sm" }), _jsxs("span", { className: "text-(--color-gray-1)", children: [progressSegments.pendingCount, " pending"] })] }))] })] })] }), _jsx("div", { className: "flex-1 overflow-auto p-4 md:p-6 min-h-0", children: isLoading && batchJobs.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-32", children: _jsxs("div", { className: "flex items-center gap-2 text-gray-2", children: [_jsx(LoaderIcon, { className: "h-5 w-5 animate-spin text-theme-secondary" }), "Loading batch translations..."] }) })) : batchJobs.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-32", children: _jsxs("div", { className: "text-center text-gray-2", children: [_jsx(LanguagesIcon, { className: "h-8 w-8 block mx-auto mb-2 text-gray-2" }), _jsx("p", { className: "font-medium text-(--color-gray-1)", children: "No translations found for this batch" }), _jsx("p", { className: "text-sm mt-1", children: "The batch may not exist or no translation jobs have started yet" })] }) })) : (_jsx("div", { className: "space-y-4", children: sortedItemGroups.map(([itemId, jobs]) => {
|
|
705
|
-
const itemCompleted = jobs.filter((j) => j.status === "Completed").length;
|
|
520
|
+
return (_jsxs("div", { className: "flex h-full flex-col min-h-0 bg-[var(--color-gray-5)]", "data-testid": "batch-translation-view", children: [_jsxs("div", { className: "flex-shrink-0 border-b border-[var(--color-gray-3)] bg-background p-4 md:p-6", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-3", children: [onBack && (_jsxs(Button, { size: "sm", variant: "outline", onClick: onBack, children: [_jsx("i", { className: "pi pi-arrow-left" }), "Back"] })), _jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold text-[var(--color-dark)]", children: "Translation Batch" }), _jsxs("p", { className: "text-sm text-[var(--color-gray-2)] mt-1", children: ["Batch ID: ", _jsx("span", { className: "font-mono text-xs bg-[var(--color-gray-4)] px-2 py-0.5 rounded-md", children: batchId })] }), batchInfo && (_jsxs("div", { className: "mt-2 flex flex-wrap gap-2 items-center text-xs", children: [effectiveStatus && (_jsx("span", { "data-testid": "translation-job-status", className: `inline-flex items-center rounded-full px-2.5 py-1 font-medium ${effectiveStatus === 'Completed'
|
|
521
|
+
? 'bg-green-100 text-green-700'
|
|
522
|
+
: effectiveStatus === 'In Progress'
|
|
523
|
+
? ''
|
|
524
|
+
: effectiveStatus === 'Error'
|
|
525
|
+
? 'bg-red-100 text-red-600'
|
|
526
|
+
: 'bg-[var(--color-gray-4)] text-[var(--color-gray-1)]'}`, style: effectiveStatus === 'In Progress' ? { backgroundColor: '#f6eeff', color: '#9650fb' } : undefined, children: effectiveStatus })), batchInfo.provider && (_jsxs("span", { className: "text-[var(--color-gray-1)]", children: ["Provider: ", _jsx("span", { className: "font-medium", children: getProviderDisplayName(batchInfo.provider) })] })), batchInfo.initiatedByUser && (_jsxs("span", { className: "text-[var(--color-gray-1)]", children: ["By: ", _jsx("span", { className: "font-medium", children: batchInfo.initiatedByUser })] })), (batchInfo.startedAtUtc || batchInfo.completedAtUtc || batchInfo.lastUpdatedUtc) && (_jsxs("span", { className: "text-[var(--color-gray-2)]", children: [batchInfo.startedAtUtc && (_jsxs(_Fragment, { children: ["Started: ", new Date(batchInfo.startedAtUtc).toLocaleString()] })), batchInfo.completedAtUtc && (_jsxs(_Fragment, { children: [batchInfo.startedAtUtc ? ' • ' : '', "Completed: ", new Date(batchInfo.completedAtUtc).toLocaleString()] })), !batchInfo.completedAtUtc && batchInfo.lastUpdatedUtc && (_jsxs(_Fragment, { children: [batchInfo.startedAtUtc ? ' • ' : '', "Updated: ", new Date(batchInfo.lastUpdatedUtc).toLocaleString()] }))] }))] }))] })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "text-sm text-[var(--color-gray-2)]", children: [`${progressSegments.completedCount}/${progressSegments.total} completed`, progressSegments.errorCount ? `, ${progressSegments.errorCount} errors` : ''] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: loadBatchJobs, disabled: isLoading, title: "Manual refresh - normally updates come through websocket subscriptions", children: [_jsx("i", { className: `pi pi-refresh ${isLoading ? 'pi-spin' : ''}` }), "Refresh"] })] })] }), parsedMetadata && (_jsxs("div", { className: "border-t border-[var(--color-gray-3)] bg-[var(--color-gray-5)] mt-4 rounded-b-lg", children: [_jsxs("button", { onClick: () => setIsMetadataExpanded(!isMetadataExpanded), className: "flex w-full cursor-pointer items-center justify-between px-4 py-2.5 text-left transition-colors hover:bg-[var(--color-gray-4)]", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("i", { className: "pi pi-info-circle text-[var(--color-gray-2)]" }), _jsx("span", { className: "text-xs font-medium text-[var(--color-gray-1)]", children: "Batch Metadata" })] }), isMetadataExpanded ? (_jsx(ChevronUpIcon, { className: "h-4 w-4 text-[var(--color-gray-2)]", strokeWidth: 1 })) : (_jsx(ChevronDownIcon, { className: "h-4 w-4 text-[var(--color-gray-2)]", strokeWidth: 1 }))] }), isMetadataExpanded && (_jsx("div", { className: "max-h-96 overflow-y-auto px-4 pb-3", children: _jsx("div", { className: "rounded-lg border border-[var(--color-gray-3)] bg-background p-3 text-xs shadow-sm mt-2", children: _jsx(MemoizedJsonView, { data: parsedMetadata }) }) }))] })), _jsxs("div", { className: "mt-4", children: [_jsxs("div", { className: "flex items-center justify-between mb-2", children: [_jsx("span", { className: "text-sm font-medium text-[var(--color-dark)]", children: "Overall Progress" }), _jsxs("span", { className: "text-sm text-[var(--color-gray-2)]", children: [progressSegments.completedCount, " / ", progressSegments.total, " completed"] })] }), _jsxs("div", { className: "relative h-4 rounded-full overflow-hidden", style: { backgroundColor: 'var(--color-gray-3)' }, children: [_jsxs("div", { className: "absolute inset-0 flex", children: [progressSegments.completed > 0 && (_jsx("div", { className: "h-full transition-all duration-300", style: { width: `${progressSegments.completed}%`, backgroundColor: '#8ae048' } })), progressSegments.inProgress > 0 && (_jsx("div", { className: "h-full transition-all duration-300", style: { width: `${progressSegments.inProgress}%`, backgroundColor: '#9650fb' } })), progressSegments.error > 0 && (_jsx("div", { className: "bg-destructive h-full transition-all duration-300", style: { width: `${progressSegments.error}%` } })), progressSegments.pending > 0 && (_jsx("div", { className: "h-full transition-all duration-300", style: { width: `${progressSegments.pending}%`, backgroundColor: 'var(--color-gray-4)' } }))] }), _jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: _jsxs("span", { className: "text-xs font-medium text-white drop-shadow-sm", children: [overallProgress, "%"] }) })] }), _jsxs("div", { className: "flex flex-wrap gap-3 mt-2 text-xs", children: [progressSegments.completedCount > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "w-3 h-3 rounded-sm", style: { backgroundColor: '#8ae048' } }), _jsxs("span", { className: "text-[var(--color-gray-1)]", children: [progressSegments.completedCount, " completed"] })] })), progressSegments.inProgressCount > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "w-3 h-3 rounded-sm", style: { backgroundColor: '#9650fb' } }), _jsxs("span", { className: "text-[var(--color-gray-1)]", children: [progressSegments.inProgressCount, " in progress"] })] })), progressSegments.errorCount > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "w-3 h-3 bg-destructive rounded-sm" }), _jsxs("span", { className: "text-[var(--color-gray-1)]", children: [progressSegments.errorCount, " errors"] })] })), progressSegments.pendingCount > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "w-3 h-3 bg-[var(--color-gray-4)] rounded-sm" }), _jsxs("span", { className: "text-[var(--color-gray-1)]", children: [progressSegments.pendingCount, " pending"] })] })), typeof batchInfo?.expectedJobs === 'number' && (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "w-3 h-3 bg-[var(--color-gray-4)] rounded-sm" }), _jsxs("span", { className: "text-[var(--color-gray-1)]", children: [(batchInfo.expectedJobs || 0) - (batchInfo.completedJobs || 0) - (batchInfo.errorJobs || 0), " pending (server)"] })] }))] })] })] }), _jsx("div", { className: "flex-1 overflow-auto p-4 md:p-6 min-h-0", children: isLoading && batchJobs.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-32", children: _jsxs("div", { className: "flex items-center gap-2 text-[var(--color-gray-2)]", children: [_jsx("i", { className: "pi pi-spin pi-spinner text-[#9650fb]" }), "Loading batch translations..."] }) })) : batchJobs.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-32", children: _jsxs("div", { className: "text-center text-[var(--color-gray-2)]", children: [_jsx("i", { className: "pi pi-language text-2xl block mb-2 text-[var(--color-gray-3)]" }), _jsx("p", { className: "font-medium text-[var(--color-gray-1)]", children: "No translations found for this batch" }), _jsx("p", { className: "text-sm mt-1", children: "The batch may not exist or no translation jobs have started yet" })] }) })) : (_jsx("div", { className: "space-y-4", children: sortedItemGroups.map(([itemId, jobs]) => {
|
|
527
|
+
const itemCompleted = jobs.filter(j => j.status === "Completed").length;
|
|
706
528
|
// Calculate item progress including in-progress job progress
|
|
707
529
|
// Performance: item-level calculation is always fast (typically 2-10 jobs per item)
|
|
708
530
|
let totalProgress = 0;
|
|
@@ -712,7 +534,7 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
712
534
|
}
|
|
713
535
|
else if (job.status === "In Progress") {
|
|
714
536
|
// Get progress for this specific job
|
|
715
|
-
const progressKey =
|
|
537
|
+
const progressKey = `${job.itemId}-${job.targetLanguage}`;
|
|
716
538
|
const jobProgress = translationProgress.get(progressKey);
|
|
717
539
|
totalProgress += jobProgress?.progress || 0; // Use actual progress or 0
|
|
718
540
|
}
|
|
@@ -721,131 +543,51 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
721
543
|
}
|
|
722
544
|
}
|
|
723
545
|
const itemProgress = Math.round(totalProgress / jobs.length);
|
|
724
|
-
const itemInProgress = jobs.some(
|
|
725
|
-
const itemError = jobs.some(
|
|
726
|
-
|
|
727
|
-
const itemVisualProgress = itemTerminalError ? 100 : itemProgress;
|
|
728
|
-
return (_jsxs("div", { className: `border border-gray-3 rounded-lg bg-background ${isMobile ? "p-4" : "p-6"} shadow-sm`, children: [_jsxs("div", { className: "mb-4", children: [_jsx("div", { className: "mb-3", children: (() => {
|
|
546
|
+
const itemInProgress = jobs.some(j => j.status === "In Progress");
|
|
547
|
+
const itemError = jobs.some(j => j.status === "Error");
|
|
548
|
+
return (_jsxs("div", { className: "border border-[var(--color-gray-3)] rounded-lg bg-background p-4 md:p-6 shadow-sm", children: [_jsxs("div", { className: "mb-4", children: [_jsx("div", { className: "mb-3", children: (() => {
|
|
729
549
|
const itemName = itemNames.get(itemId.toLowerCase());
|
|
730
|
-
return itemName ? (_jsxs(_Fragment, { children: [_jsx("div", { className:
|
|
731
|
-
})() }),
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}, children: "In Progress" })), itemError && (_jsx("span", { className: `${isMobile ? "text-[10px]" : "text-xs"} bg-red-100 text-red-600 px-2 py-0.5 md:px-2.5 md:py-1 rounded-full font-medium`, children: "Error" }))] }), _jsxs("span", { className: `${isMobile ? "text-xs" : "text-sm"} font-bold text-(--color-dark)`, children: [itemVisualProgress, "%"] })] }), _jsx("div", { className: "relative", children: _jsx(Progress, { value: itemVisualProgress, className: `${isMobile ? "h-2" : "h-3"} mb-2`, indicatorClassName: itemError ? "bg-destructive" : undefined, indicatorStyle: itemError
|
|
735
|
-
? undefined
|
|
736
|
-
: itemProgress === 100
|
|
737
|
-
? { backgroundColor: "#8ae048" }
|
|
738
|
-
: { backgroundColor: "#9650fb" }, showValue: false }) })] }), isMobile ? (_jsx("div", { className: "space-y-3", children: jobs
|
|
739
|
-
.sort((a, b) => {
|
|
740
|
-
const getJobPriority = (job) => {
|
|
741
|
-
if (job.status === "In Progress")
|
|
742
|
-
return 1;
|
|
743
|
-
if (job.status === "Error")
|
|
744
|
-
return 2;
|
|
745
|
-
if (job.status === "Completed")
|
|
746
|
-
return 4;
|
|
747
|
-
return 3;
|
|
748
|
-
};
|
|
749
|
-
const priorityA = getJobPriority(a);
|
|
750
|
-
const priorityB = getJobPriority(b);
|
|
751
|
-
if (priorityA === priorityB) {
|
|
752
|
-
return a.targetLanguage.localeCompare(b.targetLanguage);
|
|
753
|
-
}
|
|
754
|
-
return priorityA - priorityB;
|
|
755
|
-
})
|
|
756
|
-
.map((job) => {
|
|
757
|
-
const progressKey = getJobKey(job.itemId, job.targetLanguage);
|
|
758
|
-
const progress = translationProgress.get(progressKey);
|
|
759
|
-
const date = new Date(job.timestamp);
|
|
760
|
-
const displayMessage = job.message ||
|
|
761
|
-
(job.status === "Error"
|
|
762
|
-
? progress?.message || "Translation failed"
|
|
763
|
-
: "");
|
|
764
|
-
const jobRetryKey = getRetryJobKey(job);
|
|
765
|
-
const isRetryingJob = isRetryingAll || retryingJobKeys.has(jobRetryKey);
|
|
766
|
-
return (_jsxs("div", { className: "rounded-md border border-gray-4 bg-gray-5 p-3", children: [_jsxs("div", { className: "flex items-center justify-between mb-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "font-bold text-(--color-dark)", children: job.targetLanguage }), _jsx("span", { className: "text-gray-2", children: "\u2190" }), _jsx("span", { className: "text-xs text-(--color-gray-1)", children: job.sourceLanguage || "—" })] }), _jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-bold ${job.status === "Completed"
|
|
767
|
-
? "bg-green-100 text-green-700"
|
|
768
|
-
: job.status === "In Progress"
|
|
769
|
-
? ""
|
|
770
|
-
: job.status === "Error"
|
|
771
|
-
? "bg-red-100 text-red-600"
|
|
772
|
-
: "bg-gray-4 text-(--color-gray-1)"}`, style: job.status === "In Progress"
|
|
773
|
-
? {
|
|
774
|
-
backgroundColor: "#f6eeff",
|
|
775
|
-
color: "#9650fb",
|
|
776
|
-
}
|
|
777
|
-
: undefined, children: job.status })] }), displayMessage && (_jsxs("div", { className: "mb-2 flex items-start gap-1.5", children: [_jsx("div", { className: "min-w-0 flex-1 text-xs text-gray-2 break-words [overflow-wrap:anywhere] whitespace-normal leading-relaxed line-clamp-3", title: displayMessage, children: displayMessage }), _jsx(CopyButton, { textToCopy: displayMessage, iconOnly: true, className: "mt-0.5 shrink-0 text-gray-2 hover:text-(--color-gray-1)" })] })), job.status === "In Progress" && progress && (_jsxs("div", { className: "mb-2", children: [_jsxs("div", { className: "flex items-center justify-between mb-1", children: [_jsx("span", { className: "text-[10px] text-gray-2 truncate pr-2", children: progress.message }), _jsxs("span", { className: "text-[10px] font-bold text-(--color-gray-1) shrink-0", children: [Math.round(progress.progress), "%"] })] }), _jsx(Progress, { value: progress.progress, className: "h-2", indicatorStyle: {
|
|
778
|
-
backgroundColor: "#9650fb",
|
|
779
|
-
}, showValue: false })] })), job.status === "Error" && (_jsx("div", { className: "mb-2", children: _jsx(Progress, { value: 100, className: "h-2", indicatorClassName: "bg-destructive", showValue: false }) })), _jsx("div", { className: "text-[10px] text-gray-2 text-right mt-1", children: date.toLocaleTimeString([], {
|
|
780
|
-
hour: "2-digit",
|
|
781
|
-
minute: "2-digit",
|
|
782
|
-
}) }), _jsx("div", { className: "mt-2 flex justify-end", children: _jsxs("div", { className: "flex items-center gap-2", children: [job.status === "Error" && (_jsx(SimpleIconButton, { onClick: () => void handleRetryJobs([job], false), icon: _jsx(RefreshIcon, { className: `h-3.5 w-3.5 ${isRetryingJob ? "animate-spin" : ""}`, strokeWidth: 1 }), label: "Retry translation as new batch", disabled: isRetryingJob, className: "p-0! text-gray-2 hover:text-(--color-gray-1)" })), _jsx(SimpleIconButton, { onClick: () => openItemInEditor(job.itemId, job.targetLanguage), icon: _jsx(ExternalLinkIcon, { className: "h-3.5 w-3.5", strokeWidth: 1 }), label: `Open item in ${job.targetLanguage}`, disabled: isRetryingAll, className: "p-0! text-gray-2 hover:text-(--color-gray-1)" })] }) })] }, job.targetLanguage));
|
|
783
|
-
}) })) : (_jsx(SimpleTable, { columns: [
|
|
550
|
+
return itemName ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "text-base font-semibold text-[var(--color-dark)] mb-1", children: itemName }), _jsxs("div", { className: "text-xs text-[var(--color-gray-2)] font-mono", children: ["Item ID: ", itemId] })] })) : (_jsxs("div", { className: "text-xs text-[var(--color-gray-2)] font-mono", children: ["Item ID: ", itemId] }));
|
|
551
|
+
})() }), _jsx("div", { className: "flex items-center justify-between mb-2", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("span", { className: "text-sm text-[var(--color-gray-2)]", children: [itemCompleted, " / ", jobs.length, " languages"] }), itemInProgress && (_jsx("span", { className: "text-xs px-2.5 py-1 rounded-full font-medium", style: { backgroundColor: '#f6eeff', color: '#9650fb' }, children: "In Progress" })), itemError && (_jsx("span", { className: "text-xs bg-red-100 text-red-600 px-2.5 py-1 rounded-full font-medium", children: "Error" }))] }) }), _jsx("div", { className: "relative", children: _jsx(Progress, { value: itemProgress, className: "h-3 mb-2", indicatorClassName: itemError ? "bg-destructive" : undefined, indicatorStyle: itemError ? undefined :
|
|
552
|
+
itemProgress === 100 ? { backgroundColor: '#8ae048' } :
|
|
553
|
+
{ backgroundColor: '#9650fb' }, showValue: true }) })] }), _jsx(SimpleTable, { columns: [
|
|
784
554
|
{
|
|
785
555
|
header: "Language",
|
|
786
556
|
className: "w-16", // Fixed width for language column
|
|
787
|
-
body: (job) => (_jsx("div", { className: "font-medium text-(--color-dark)", children: job.targetLanguage }))
|
|
557
|
+
body: (job) => (_jsx("div", { className: "font-medium text-[var(--color-dark)]", children: job.targetLanguage }))
|
|
788
558
|
},
|
|
789
559
|
{
|
|
790
560
|
header: "Status",
|
|
791
|
-
className: "w-
|
|
561
|
+
className: "w-32", // Fixed width for status column
|
|
792
562
|
body: (job) => {
|
|
793
|
-
const progressKey =
|
|
563
|
+
const progressKey = `${job.itemId}-${job.targetLanguage}`;
|
|
794
564
|
const progress = translationProgress.get(progressKey);
|
|
795
|
-
|
|
796
|
-
(job.status === "Error"
|
|
797
|
-
? progress?.message || "Translation failed"
|
|
798
|
-
: "");
|
|
799
|
-
return (_jsxs("div", { className: "w-[14rem] min-w-[14rem]", children: [_jsx("span", { className: `inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium ${job.status === "Completed"
|
|
565
|
+
return (_jsxs("div", { children: [_jsx("span", { className: `inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium ${job.status === "Completed"
|
|
800
566
|
? "bg-green-100 text-green-700"
|
|
801
567
|
: job.status === "In Progress"
|
|
802
568
|
? ""
|
|
803
569
|
: job.status === "Error"
|
|
804
570
|
? "bg-red-100 text-red-600"
|
|
805
|
-
: "bg-gray-4 text-(--color-gray-1)"}`, style: job.status === "In Progress"
|
|
806
|
-
|
|
807
|
-
backgroundColor: "#f6eeff",
|
|
808
|
-
color: "#9650fb",
|
|
809
|
-
}
|
|
810
|
-
: undefined, children: job.status }), job.status === "In Progress" &&
|
|
811
|
-
progress &&
|
|
812
|
-
Math.round(progress.progress || 0) === 0 && (_jsx("div", { className: "mt-1 text-xs text-gray-2", children: "Version created" })), progress && job.status === "In Progress" && (_jsxs("div", { className: "mt-1.5", children: [_jsxs("div", { className: "flex items-center justify-between mb-1", children: [_jsx("span", { className: "text-xs text-gray-2 truncate", "data-testid": "translation-progress-text", children: progress.message }), _jsxs("span", { className: "text-xs font-medium text-(--color-gray-1) ml-1", children: [Math.round(progress.progress), "%"] })] }), _jsx(Progress, { value: progress.progress, className: "h-2", indicatorStyle: {
|
|
813
|
-
backgroundColor: "#9650fb",
|
|
814
|
-
}, showValue: false, "data-testid": "translation-progress-bar" })] })), job.status === "Error" && (_jsx("div", { className: "mt-1.5", children: _jsx(Progress, { value: 100, className: "h-2", indicatorClassName: "bg-destructive", showValue: false, "data-testid": "translation-progress-bar" }) }))] }));
|
|
815
|
-
},
|
|
571
|
+
: "bg-[var(--color-gray-4)] text-[var(--color-gray-1)]"}`, style: job.status === "In Progress" ? { backgroundColor: '#f6eeff', color: '#9650fb' } : undefined, children: job.status }), job.status === "In Progress" && progress && Math.round(progress.progress || 0) === 0 && (_jsx("div", { className: "mt-1 text-xs text-[var(--color-gray-2)]", children: "Version created" })), progress && job.status === "In Progress" && (_jsxs("div", { className: "mt-1.5", children: [_jsxs("div", { className: "flex items-center justify-between mb-1", children: [_jsx("span", { className: "text-xs text-[var(--color-gray-2)] truncate", "data-testid": "translation-progress-text", children: progress.message }), _jsxs("span", { className: "text-xs font-medium text-[var(--color-gray-1)] ml-1", children: [Math.round(progress.progress), "%"] })] }), _jsx(Progress, { value: progress.progress, className: "h-2", indicatorStyle: { backgroundColor: '#9650fb' }, showValue: false, "data-testid": "translation-progress-bar" })] }))] }));
|
|
572
|
+
}
|
|
816
573
|
},
|
|
817
574
|
{
|
|
818
575
|
header: "Source",
|
|
819
576
|
className: "w-16", // Fixed width for source column
|
|
820
|
-
body: (job) => (_jsx("div", { className: "text-sm text-(--color-gray-1)", children: job.sourceLanguage || "—" }))
|
|
577
|
+
body: (job) => (_jsx("div", { className: "text-sm text-[var(--color-gray-1)]", children: job.sourceLanguage || "—" }))
|
|
821
578
|
},
|
|
822
579
|
{
|
|
823
580
|
header: "Message",
|
|
824
|
-
className: "w-
|
|
825
|
-
body: (job) => {
|
|
826
|
-
const message = job.message ||
|
|
827
|
-
(job.status === "Error"
|
|
828
|
-
? "Translation failed"
|
|
829
|
-
: "—");
|
|
830
|
-
return (_jsxs("div", { className: "flex items-start gap-1.5", children: [_jsx("div", { className: "min-w-0 w-full flex-1 text-sm text-gray-2 break-words [overflow-wrap:anywhere] whitespace-normal leading-relaxed line-clamp-3", title: message, children: message }), message !== "—" && (_jsx(CopyButton, { textToCopy: message, iconOnly: true, className: "mt-0.5 shrink-0 text-gray-2 hover:text-(--color-gray-1)" }))] }));
|
|
831
|
-
},
|
|
832
|
-
},
|
|
833
|
-
{
|
|
834
|
-
header: "Actions",
|
|
835
|
-
className: "w-24",
|
|
836
|
-
body: (job) => {
|
|
837
|
-
const jobRetryKey = getRetryJobKey(job);
|
|
838
|
-
const isRetryingJob = isRetryingAll || retryingJobKeys.has(jobRetryKey);
|
|
839
|
-
return (_jsxs("div", { className: "flex items-center justify-center gap-2", children: [job.status === "Error" && (_jsx(SimpleIconButton, { onClick: () => void handleRetryJobs([job], false), icon: _jsx(RefreshIcon, { className: `h-3.5 w-3.5 ${isRetryingJob ? "animate-spin" : ""}`, strokeWidth: 1 }), label: "Retry translation as new batch", disabled: isRetryingJob, className: "p-0! text-gray-2 hover:text-(--color-gray-1)" })), _jsx(SimpleIconButton, { onClick: () => openItemInEditor(job.itemId, job.targetLanguage), icon: _jsx(ExternalLinkIcon, { className: "h-3.5 w-3.5", strokeWidth: 1 }), label: `Open item in ${job.targetLanguage}`, disabled: isRetryingAll, className: "p-0! text-gray-2 hover:text-(--color-gray-1)" })] }));
|
|
840
|
-
},
|
|
581
|
+
className: "w-64", // Fixed width with proper wrapping for message column
|
|
582
|
+
body: (job) => (_jsx("div", { className: "text-sm text-[var(--color-gray-2)] break-words whitespace-normal leading-relaxed", children: job.message || "—" }))
|
|
841
583
|
},
|
|
842
584
|
{
|
|
843
585
|
header: "Updated",
|
|
844
586
|
className: "w-24", // Fixed width for updated column
|
|
845
587
|
body: (job) => {
|
|
846
588
|
const date = new Date(job.timestamp);
|
|
847
|
-
return (_jsx("div", { className: "text-sm text-gray-2 whitespace-nowrap", children: date.toLocaleTimeString() }));
|
|
848
|
-
}
|
|
589
|
+
return (_jsx("div", { className: "text-sm text-[var(--color-gray-2)] whitespace-nowrap", children: date.toLocaleTimeString() }));
|
|
590
|
+
}
|
|
849
591
|
},
|
|
850
592
|
], items: jobs.sort((a, b) => {
|
|
851
593
|
// Sort by status first (in-progress first, then errors, then completed)
|
|
@@ -865,6 +607,6 @@ export function BatchTranslationView({ batchId, onBack, }) {
|
|
|
865
607
|
return a.targetLanguage.localeCompare(b.targetLanguage);
|
|
866
608
|
}
|
|
867
609
|
return priorityA - priorityB;
|
|
868
|
-
}) })
|
|
610
|
+
}) })] }, itemId));
|
|
869
611
|
}) })) })] }));
|
|
870
612
|
}
|