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