@parhelia/localization 0.1.12112 → 0.1.12175
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/translation-center/BatchTranslationView.d.ts +1 -1
- package/dist/translation-center/BatchTranslationView.d.ts.map +1 -1
- package/dist/translation-center/BatchTranslationView.js +175 -67
- package/dist/translation-center/RecentTranslations.d.ts.map +1 -1
- package/dist/translation-center/RecentTranslations.js +18 -9
- package/package.json +1 -1
|
@@ -3,6 +3,6 @@ interface BatchTranslationViewProps {
|
|
|
3
3
|
batchId: string;
|
|
4
4
|
onBack?: () => void;
|
|
5
5
|
}
|
|
6
|
-
export declare function BatchTranslationView({ batchId, onBack }: BatchTranslationViewProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export declare function BatchTranslationView({ batchId, onBack, }: BatchTranslationViewProps): import("react/jsx-runtime").JSX.Element;
|
|
7
7
|
export {};
|
|
8
8
|
//# sourceMappingURL=BatchTranslationView.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BatchTranslationView.d.ts","sourceRoot":"","sources":["../../src/translation-center/BatchTranslationView.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"BatchTranslationView.d.ts","sourceRoot":"","sources":["../../src/translation-center/BatchTranslationView.tsx"],"names":[],"mappings":"AAmCA,OAAO,qCAAqC,CAAC;AAyC7C,UAAU,yBAAyB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAkCD,wBAAgB,oBAAoB,CAAC,EACnC,OAAO,EACP,MAAM,GACP,EAAE,yBAAyB,2CAw0C3B"}
|
|
@@ -1,8 +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
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";
|
|
4
|
+
import { listBatchTranslationJobs, subscribeToBatch, unsubscribeFromBatch, getBatchInfo, getTranslationProviders, } from "../services/translationService";
|
|
5
|
+
import { ChevronDown, ChevronUp, RefreshCw, ArrowLeft, Info, Loader2, Languages, } from "lucide-react";
|
|
6
6
|
import { JsonView, defaultStyles } from "react-json-view-lite";
|
|
7
7
|
import "react-json-view-lite/dist/index.css";
|
|
8
8
|
// Wrapper so React 19 sees a plain function component instead of a forwardRef exotic component.
|
|
@@ -10,6 +10,11 @@ const Progress = (props) => React.createElement(CoreProgress, props);
|
|
|
10
10
|
// Wrappers for lucide-react icons to work with React 19
|
|
11
11
|
const ChevronUpIcon = (props) => React.createElement(ChevronUp, props);
|
|
12
12
|
const ChevronDownIcon = (props) => React.createElement(ChevronDown, props);
|
|
13
|
+
const RefreshIcon = (props) => React.createElement(RefreshCw, props);
|
|
14
|
+
const ArrowLeftIcon = (props) => React.createElement(ArrowLeft, props);
|
|
15
|
+
const InfoIcon = (props) => React.createElement(Info, props);
|
|
16
|
+
const LoaderIcon = (props) => React.createElement(Loader2, props);
|
|
17
|
+
const LanguagesIcon = (props) => React.createElement(Languages, props);
|
|
13
18
|
const MemoizedJsonView = memo(({ data }) => (_jsx(JsonView, { data: data, shouldExpandNode: (level) => level < 2, style: defaultStyles })), (prevProps, nextProps) => {
|
|
14
19
|
return JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data);
|
|
15
20
|
});
|
|
@@ -18,7 +23,7 @@ function normalizeBatchInfo(raw) {
|
|
|
18
23
|
if (!raw)
|
|
19
24
|
return null;
|
|
20
25
|
const pick = (a, b) => (a !== undefined ? a : b);
|
|
21
|
-
const toNum = (v) =>
|
|
26
|
+
const toNum = (v) => typeof v === "number" ? v : v != null ? parseInt(v, 10) : undefined;
|
|
22
27
|
const info = {
|
|
23
28
|
batchId: pick(raw?.batchId, raw?.BatchId),
|
|
24
29
|
createdAtUtc: pick(raw?.createdAtUtc, raw?.CreatedAtUtc),
|
|
@@ -37,7 +42,7 @@ function normalizeBatchInfo(raw) {
|
|
|
37
42
|
};
|
|
38
43
|
return info.batchId ? info : null;
|
|
39
44
|
}
|
|
40
|
-
export function BatchTranslationView({ batchId, onBack }) {
|
|
45
|
+
export function BatchTranslationView({ batchId, onBack, }) {
|
|
41
46
|
const editContext = useEditContext();
|
|
42
47
|
const [batchJobs, setBatchJobs] = useState([]);
|
|
43
48
|
const [batchInfo, setBatchInfo] = useState(null);
|
|
@@ -56,8 +61,8 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
56
61
|
// Helper function to get display name from service name
|
|
57
62
|
const getProviderDisplayName = useCallback((serviceName) => {
|
|
58
63
|
if (!serviceName)
|
|
59
|
-
return
|
|
60
|
-
const provider = providers.find(p => p.name === serviceName);
|
|
64
|
+
return "Unknown";
|
|
65
|
+
const provider = providers.find((p) => p.name === serviceName);
|
|
61
66
|
return provider?.displayName || serviceName;
|
|
62
67
|
}, [providers]);
|
|
63
68
|
// Parse metadata for display
|
|
@@ -78,7 +83,9 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
78
83
|
const loadProviders = useCallback(async () => {
|
|
79
84
|
try {
|
|
80
85
|
const res = await getTranslationProviders();
|
|
81
|
-
const providerData = (res?.data ??
|
|
86
|
+
const providerData = (res?.data ??
|
|
87
|
+
res ??
|
|
88
|
+
[]);
|
|
82
89
|
setProviders(providerData);
|
|
83
90
|
}
|
|
84
91
|
catch (error) {
|
|
@@ -90,9 +97,11 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
90
97
|
try {
|
|
91
98
|
// Use efficient batch-specific endpoint
|
|
92
99
|
const res = await listBatchTranslationJobs(batchId);
|
|
93
|
-
const apiJobs = (res?.data ??
|
|
100
|
+
const apiJobs = (res?.data ??
|
|
101
|
+
res ??
|
|
102
|
+
[]);
|
|
94
103
|
// Merge API response with existing state to prevent flickering
|
|
95
|
-
setBatchJobs(prevJobs => {
|
|
104
|
+
setBatchJobs((prevJobs) => {
|
|
96
105
|
// Create a map of existing jobs keyed by itemId-targetLanguage
|
|
97
106
|
const existingMap = new Map();
|
|
98
107
|
for (const job of prevJobs) {
|
|
@@ -105,13 +114,18 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
105
114
|
const existingJob = existingMap.get(key);
|
|
106
115
|
if (existingJob) {
|
|
107
116
|
// Prefer API data if timestamp is newer or status changed
|
|
108
|
-
const apiTimestamp = apiJob.timestamp
|
|
109
|
-
|
|
110
|
-
|
|
117
|
+
const apiTimestamp = apiJob.timestamp
|
|
118
|
+
? new Date(apiJob.timestamp).getTime()
|
|
119
|
+
: 0;
|
|
120
|
+
const existingTimestamp = existingJob.timestamp
|
|
121
|
+
? new Date(existingJob.timestamp).getTime()
|
|
122
|
+
: 0;
|
|
123
|
+
if (apiTimestamp >= existingTimestamp ||
|
|
124
|
+
apiJob.status !== existingJob.status) {
|
|
111
125
|
// Preserve sourceLanguage from API if available, otherwise keep existing
|
|
112
126
|
existingMap.set(key, {
|
|
113
127
|
...apiJob,
|
|
114
|
-
sourceLanguage: apiJob.sourceLanguage || existingJob.sourceLanguage || ""
|
|
128
|
+
sourceLanguage: apiJob.sourceLanguage || existingJob.sourceLanguage || "",
|
|
115
129
|
});
|
|
116
130
|
}
|
|
117
131
|
}
|
|
@@ -119,7 +133,7 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
119
133
|
// New job from API - ensure sourceLanguage is set
|
|
120
134
|
existingMap.set(key, {
|
|
121
135
|
...apiJob,
|
|
122
|
-
sourceLanguage: apiJob.sourceLanguage || ""
|
|
136
|
+
sourceLanguage: apiJob.sourceLanguage || "",
|
|
123
137
|
});
|
|
124
138
|
}
|
|
125
139
|
}
|
|
@@ -131,7 +145,7 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
131
145
|
return mergedJobs;
|
|
132
146
|
}
|
|
133
147
|
// Deep comparison: check if any job changed
|
|
134
|
-
const prevMap = new Map(prevJobs.map(j => [`${j.itemId}-${j.targetLanguage}`, j]));
|
|
148
|
+
const prevMap = new Map(prevJobs.map((j) => [`${j.itemId}-${j.targetLanguage}`, j]));
|
|
135
149
|
let hasChanges = false;
|
|
136
150
|
for (const mergedJob of mergedJobs) {
|
|
137
151
|
const key = `${mergedJob.itemId}-${mergedJob.targetLanguage}`;
|
|
@@ -181,7 +195,7 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
181
195
|
const info = normalizeBatchInfo(raw);
|
|
182
196
|
if (!cancelled && info)
|
|
183
197
|
setBatchInfo(info);
|
|
184
|
-
const isTerminal = info?.status ===
|
|
198
|
+
const isTerminal = info?.status === "Completed" || info?.status === "Error";
|
|
185
199
|
// Conditionally subscribe only for non-terminal batches
|
|
186
200
|
if (!isTerminal && editContext?.sessionId) {
|
|
187
201
|
await subscribeToBatch(batchId, editContext.sessionId);
|
|
@@ -204,7 +218,10 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
204
218
|
init();
|
|
205
219
|
return () => {
|
|
206
220
|
// Cleanup: unsubscribe when component unmounts or batch changes
|
|
207
|
-
if (isSubscribedRef.current &&
|
|
221
|
+
if (isSubscribedRef.current &&
|
|
222
|
+
editContext &&
|
|
223
|
+
editContext.sessionId &&
|
|
224
|
+
batchId) {
|
|
208
225
|
unsubscribeFromBatch(batchId, editContext.sessionId).catch(console.error);
|
|
209
226
|
}
|
|
210
227
|
};
|
|
@@ -226,8 +243,8 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
226
243
|
// Only update if this message is for our batch
|
|
227
244
|
if (message.payload.batchId === batchId) {
|
|
228
245
|
// Update the specific job in our local state instead of refetching all jobs
|
|
229
|
-
setBatchJobs(prevJobs => {
|
|
230
|
-
const jobIndex = prevJobs.findIndex(job => job.itemId === message.payload.itemId &&
|
|
246
|
+
setBatchJobs((prevJobs) => {
|
|
247
|
+
const jobIndex = prevJobs.findIndex((job) => job.itemId === message.payload.itemId &&
|
|
231
248
|
job.targetLanguage === message.payload.language);
|
|
232
249
|
if (jobIndex >= 0) {
|
|
233
250
|
// Update existing job
|
|
@@ -240,7 +257,9 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
240
257
|
timestamp: message.payload.timestamp || existingJob.timestamp,
|
|
241
258
|
message: message.payload.message || existingJob.message,
|
|
242
259
|
// Preserve sourceLanguage from existing job if not provided in message
|
|
243
|
-
sourceLanguage: message.payload.sourceLanguage ||
|
|
260
|
+
sourceLanguage: message.payload.sourceLanguage ||
|
|
261
|
+
existingJob.sourceLanguage ||
|
|
262
|
+
"",
|
|
244
263
|
};
|
|
245
264
|
}
|
|
246
265
|
return updatedJobs;
|
|
@@ -254,7 +273,7 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
254
273
|
status: message.payload.status,
|
|
255
274
|
timestamp: message.payload.timestamp || new Date().toISOString(),
|
|
256
275
|
batchId: batchId,
|
|
257
|
-
message: message.payload.message || ""
|
|
276
|
+
message: message.payload.message || "",
|
|
258
277
|
};
|
|
259
278
|
return [...prevJobs, newJob];
|
|
260
279
|
}
|
|
@@ -265,12 +284,14 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
265
284
|
if (message.type === "translation-progress") {
|
|
266
285
|
// Only track progress for jobs in this batch
|
|
267
286
|
if (message.payload.batchId === batchId) {
|
|
268
|
-
const itemId = (message.payload.itemId ?? "")
|
|
287
|
+
const itemId = (message.payload.itemId ?? "")
|
|
288
|
+
.toString()
|
|
289
|
+
.toLowerCase();
|
|
269
290
|
const language = message.payload.language;
|
|
270
291
|
const progressKey = `${itemId}-${language}`;
|
|
271
292
|
// Ensure a row exists; if not, add a placeholder "In Progress" job
|
|
272
|
-
setBatchJobs(prevJobs => {
|
|
273
|
-
const idx = prevJobs.findIndex(j => j.itemId === itemId && j.targetLanguage === language);
|
|
293
|
+
setBatchJobs((prevJobs) => {
|
|
294
|
+
const idx = prevJobs.findIndex((j) => j.itemId === itemId && j.targetLanguage === language);
|
|
274
295
|
if (idx >= 0)
|
|
275
296
|
return prevJobs;
|
|
276
297
|
const newJob = {
|
|
@@ -280,7 +301,7 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
280
301
|
status: "In Progress",
|
|
281
302
|
timestamp: new Date().toISOString(),
|
|
282
303
|
batchId: batchId,
|
|
283
|
-
message: message.payload.message || ""
|
|
304
|
+
message: message.payload.message || "",
|
|
284
305
|
};
|
|
285
306
|
return [...prevJobs, newJob];
|
|
286
307
|
});
|
|
@@ -288,14 +309,15 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
288
309
|
const next = new Map(prev);
|
|
289
310
|
next.set(progressKey, {
|
|
290
311
|
progress: message.payload.progress || 0,
|
|
291
|
-
message: message.payload.message || ""
|
|
312
|
+
message: message.payload.message || "",
|
|
292
313
|
});
|
|
293
314
|
return next;
|
|
294
315
|
});
|
|
295
316
|
}
|
|
296
317
|
}
|
|
297
318
|
// Handle batch-completed messages for this specific batch
|
|
298
|
-
if (message.type === "batch-completed" &&
|
|
319
|
+
if (message.type === "batch-completed" &&
|
|
320
|
+
message.payload.batchId === batchId) {
|
|
299
321
|
// Refresh header info only (jobs are already updated via websocket)
|
|
300
322
|
(async () => {
|
|
301
323
|
try {
|
|
@@ -318,7 +340,7 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
318
340
|
}, [editContext, batchId]);
|
|
319
341
|
// Adaptive polling while batch is in progress - only as fallback when websockets are silent
|
|
320
342
|
useEffect(() => {
|
|
321
|
-
const isRunning = batchInfo?.status ===
|
|
343
|
+
const isRunning = batchInfo?.status === "In Progress";
|
|
322
344
|
if (!isRunning)
|
|
323
345
|
return;
|
|
324
346
|
const pollBatchInfo = async () => {
|
|
@@ -356,7 +378,7 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
356
378
|
}
|
|
357
379
|
const fetchItemNames = async () => {
|
|
358
380
|
// Extract unique item IDs from batchJobs
|
|
359
|
-
const uniqueItemIds = Array.from(new Set(batchJobs.map(job => job.itemId)));
|
|
381
|
+
const uniqueItemIds = Array.from(new Set(batchJobs.map((job) => job.itemId)));
|
|
360
382
|
if (uniqueItemIds.length === 0) {
|
|
361
383
|
return;
|
|
362
384
|
}
|
|
@@ -364,7 +386,7 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
364
386
|
const language = editContext.currentItemDescriptor?.language || "en";
|
|
365
387
|
try {
|
|
366
388
|
// Create ItemDescriptors for fetching
|
|
367
|
-
const itemDescriptors = uniqueItemIds.map(itemId => ({
|
|
389
|
+
const itemDescriptors = uniqueItemIds.map((itemId) => ({
|
|
368
390
|
id: itemId,
|
|
369
391
|
language: language,
|
|
370
392
|
version: 0, // Latest version
|
|
@@ -378,7 +400,7 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
378
400
|
namesMap.set(stub.id.toLowerCase(), stub.name);
|
|
379
401
|
}
|
|
380
402
|
}
|
|
381
|
-
setItemNames(prevNames => {
|
|
403
|
+
setItemNames((prevNames) => {
|
|
382
404
|
// Merge with existing names, only update if there are changes
|
|
383
405
|
let hasChanges = false;
|
|
384
406
|
const merged = new Map(prevNames);
|
|
@@ -397,7 +419,11 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
397
419
|
}
|
|
398
420
|
};
|
|
399
421
|
fetchItemNames();
|
|
400
|
-
}, [
|
|
422
|
+
}, [
|
|
423
|
+
batchJobs,
|
|
424
|
+
editContext?.itemsRepository,
|
|
425
|
+
editContext?.currentItemDescriptor?.language,
|
|
426
|
+
]);
|
|
401
427
|
// Group jobs by item for better organization
|
|
402
428
|
const itemGroups = useMemo(() => {
|
|
403
429
|
const groups = {};
|
|
@@ -415,9 +441,9 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
415
441
|
return entries.sort(([, jobsA], [, jobsB]) => {
|
|
416
442
|
// Calculate status priority for each item group
|
|
417
443
|
const getStatusPriority = (jobs) => {
|
|
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");
|
|
444
|
+
const hasInProgress = jobs.some((j) => j.status === "In Progress");
|
|
445
|
+
const hasError = jobs.some((j) => j.status === "Error");
|
|
446
|
+
const allCompleted = jobs.every((j) => j.status === "Completed");
|
|
421
447
|
if (hasInProgress)
|
|
422
448
|
return 1; // Highest priority - show first
|
|
423
449
|
if (hasError)
|
|
@@ -434,7 +460,7 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
434
460
|
// Calculate overall progress segments including in-progress job progress.
|
|
435
461
|
// Incorporate server-side expected/completed/error counts so pending server jobs are reflected.
|
|
436
462
|
const progressSegments = useMemo(() => {
|
|
437
|
-
const expectedTotal = typeof batchInfo?.expectedJobs ===
|
|
463
|
+
const expectedTotal = typeof batchInfo?.expectedJobs === "number" && batchInfo.expectedJobs > 0
|
|
438
464
|
? batchInfo.expectedJobs
|
|
439
465
|
: batchJobs.length;
|
|
440
466
|
if (expectedTotal === 0) {
|
|
@@ -448,18 +474,22 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
448
474
|
errorCount: 0,
|
|
449
475
|
pendingCount: 0,
|
|
450
476
|
total: 0,
|
|
451
|
-
actualProgress: 0
|
|
477
|
+
actualProgress: 0,
|
|
452
478
|
};
|
|
453
479
|
}
|
|
454
|
-
const localCompleted = batchJobs.filter(job => job.status ===
|
|
455
|
-
const localErrors = batchJobs.filter(job => job.status ===
|
|
456
|
-
const serverCompleted = typeof batchInfo?.completedJobs ===
|
|
457
|
-
|
|
480
|
+
const localCompleted = batchJobs.filter((job) => job.status === "Completed").length;
|
|
481
|
+
const localErrors = batchJobs.filter((job) => job.status === "Error").length;
|
|
482
|
+
const serverCompleted = typeof batchInfo?.completedJobs === "number"
|
|
483
|
+
? batchInfo.completedJobs
|
|
484
|
+
: undefined;
|
|
485
|
+
const serverErrors = typeof batchInfo?.errorJobs === "number"
|
|
486
|
+
? batchInfo.errorJobs
|
|
487
|
+
: undefined;
|
|
458
488
|
// Prefer the larger of local/server so UI can progress smoothly with local updates
|
|
459
489
|
const completedCount = Math.max(0, Math.min(expectedTotal, Math.max(localCompleted, serverCompleted ?? 0)));
|
|
460
490
|
const errorCount = Math.max(0, Math.min(expectedTotal - completedCount, Math.max(localErrors, serverErrors ?? 0)));
|
|
461
491
|
// Known in-progress from client state
|
|
462
|
-
const knownInProgressCount = batchJobs.filter(job => job.status ===
|
|
492
|
+
const knownInProgressCount = batchJobs.filter((job) => job.status === "In Progress").length;
|
|
463
493
|
// Cap to not exceed remaining after completed+error
|
|
464
494
|
const inProgressCount = Math.max(0, Math.min(knownInProgressCount, expectedTotal - completedCount - errorCount));
|
|
465
495
|
const pendingCount = Math.max(0, expectedTotal - completedCount - errorCount - inProgressCount);
|
|
@@ -467,7 +497,7 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
467
497
|
// Actual progress: completed=100 each, in-progress use live progress if available, others 0
|
|
468
498
|
let totalActualProgress = completedCount * 100;
|
|
469
499
|
for (const job of batchJobs) {
|
|
470
|
-
if (job.status ===
|
|
500
|
+
if (job.status === "In Progress") {
|
|
471
501
|
const progressKey = `${job.itemId}-${job.targetLanguage}`;
|
|
472
502
|
const jobProgress = translationProgress.get(progressKey);
|
|
473
503
|
totalActualProgress += jobProgress?.progress || 0;
|
|
@@ -489,6 +519,7 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
489
519
|
return segments;
|
|
490
520
|
}, [batchJobs, batchInfo, translationProgress]);
|
|
491
521
|
const overallProgress = progressSegments.actualProgress;
|
|
522
|
+
const isMobile = editContext?.isMobile ?? false;
|
|
492
523
|
// Treat the batch as Completed if all jobs are completed and there are no errors/in-progress,
|
|
493
524
|
// even if the backend status is still "In Progress".
|
|
494
525
|
const effectiveStatus = batchInfo?.status === "In Progress" &&
|
|
@@ -517,14 +548,38 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
517
548
|
catch { }
|
|
518
549
|
})();
|
|
519
550
|
}, [effectiveStatus, batchId, loadBatchJobs]);
|
|
520
|
-
return (_jsxs("div", { className: "flex h-full flex-col min-h-0 bg-
|
|
521
|
-
?
|
|
522
|
-
: effectiveStatus ===
|
|
523
|
-
?
|
|
524
|
-
: effectiveStatus ===
|
|
525
|
-
?
|
|
526
|
-
:
|
|
527
|
-
|
|
551
|
+
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: `flex ${isMobile ? "flex-col items-start gap-4" : "items-center justify-between"}`, children: [_jsxs("div", { className: `flex ${isMobile ? "w-full" : "items-center"} gap-3`, children: [onBack && (_jsxs(Button, { size: "sm", variant: "outline", onClick: onBack, className: isMobile ? "shrink-0 self-start p-0 w-8 h-8" : "", children: [_jsx(ArrowLeftIcon, { className: "h-4 w-4" }), isMobile ? "" : "Back"] })), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("h1", { className: "text-lg md:text-xl font-semibold text-(--color-dark) truncate", children: "Translation Batch" }), _jsxs("p", { className: "text-xs md:text-sm text-gray-2 mt-0.5 md:mt-1", children: ["Batch ID:", " ", _jsx("span", { className: "font-mono text-[10px] md:text-xs bg-gray-4 px-2 py-0.5 rounded-md break-all", children: batchId })] }), batchInfo && !isMobile && (_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"
|
|
552
|
+
? "bg-green-100 text-green-700"
|
|
553
|
+
: effectiveStatus === "In Progress"
|
|
554
|
+
? ""
|
|
555
|
+
: effectiveStatus === "Error"
|
|
556
|
+
? "bg-red-100 text-red-600"
|
|
557
|
+
: "bg-gray-4 text-(--color-gray-1)"}`, style: effectiveStatus === "In Progress"
|
|
558
|
+
? { backgroundColor: "#f6eeff", color: "#9650fb" }
|
|
559
|
+
: 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 ||
|
|
560
|
+
batchInfo.completedAtUtc ||
|
|
561
|
+
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 &&
|
|
562
|
+
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"
|
|
563
|
+
? "bg-green-100 text-green-700"
|
|
564
|
+
: effectiveStatus === "In Progress"
|
|
565
|
+
? ""
|
|
566
|
+
: effectiveStatus === "Error"
|
|
567
|
+
? "bg-red-100 text-red-600"
|
|
568
|
+
: "bg-gray-4 text-(--color-gray-1)"}`, style: effectiveStatus === "In Progress"
|
|
569
|
+
? { backgroundColor: "#f6eeff", color: "#9650fb" }
|
|
570
|
+
: 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
|
|
571
|
+
? `, ${progressSegments.errorCount} errors`
|
|
572
|
+
: ""] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: loadBatchJobs, disabled: isLoading, 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: {
|
|
573
|
+
width: `${progressSegments.completed}%`,
|
|
574
|
+
backgroundColor: "#8ae048",
|
|
575
|
+
} })), progressSegments.inProgress > 0 && (_jsx("div", { className: "h-full transition-all duration-300", style: {
|
|
576
|
+
width: `${progressSegments.inProgress}%`,
|
|
577
|
+
backgroundColor: "#9650fb",
|
|
578
|
+
} })), 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: {
|
|
579
|
+
width: `${progressSegments.pending}%`,
|
|
580
|
+
backgroundColor: "var(--color-gray-4)",
|
|
581
|
+
} }))] }), _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]) => {
|
|
582
|
+
const itemCompleted = jobs.filter((j) => j.status === "Completed").length;
|
|
528
583
|
// Calculate item progress including in-progress job progress
|
|
529
584
|
// Performance: item-level calculation is always fast (typically 2-10 jobs per item)
|
|
530
585
|
let totalProgress = 0;
|
|
@@ -543,18 +598,62 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
543
598
|
}
|
|
544
599
|
}
|
|
545
600
|
const itemProgress = Math.round(totalProgress / jobs.length);
|
|
546
|
-
const itemInProgress = jobs.some(j => j.status === "In Progress");
|
|
547
|
-
const itemError = jobs.some(j => j.status === "Error");
|
|
548
|
-
return (_jsxs("div", { className:
|
|
601
|
+
const itemInProgress = jobs.some((j) => j.status === "In Progress");
|
|
602
|
+
const itemError = jobs.some((j) => j.status === "Error");
|
|
603
|
+
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: (() => {
|
|
549
604
|
const itemName = itemNames.get(itemId.toLowerCase());
|
|
550
|
-
return itemName ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "text-base font-
|
|
551
|
-
})() }),
|
|
552
|
-
|
|
553
|
-
|
|
605
|
+
return itemName ? (_jsxs(_Fragment, { children: [_jsx("div", { className: `${isMobile ? "text-sm" : "text-base"} font-bold text-(--color-dark) mb-1`, children: itemName }), _jsxs("div", { className: `text-[10px] md:text-xs text-gray-2 font-mono break-all`, children: ["Item ID: ", itemId] })] })) : (_jsxs("div", { className: `text-[10px] md:text-xs text-gray-2 font-mono break-all`, children: ["Item ID: ", itemId] }));
|
|
606
|
+
})() }), _jsxs("div", { className: "flex items-center justify-between mb-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("span", { className: `${isMobile ? "text-xs" : "text-sm"} text-gray-2`, children: [itemCompleted, " / ", jobs.length, " languages"] }), itemInProgress && (_jsx("span", { className: `${isMobile ? "text-[10px]" : "text-xs"} px-2 py-0.5 md:px-2.5 md:py-1 rounded-full font-medium`, style: {
|
|
607
|
+
backgroundColor: "#f6eeff",
|
|
608
|
+
color: "#9650fb",
|
|
609
|
+
}, 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: [itemProgress, "%"] })] }), _jsx("div", { className: "relative", children: _jsx(Progress, { value: itemProgress, className: `${isMobile ? "h-2" : "h-3"} mb-2`, indicatorClassName: itemError ? "bg-destructive" : undefined, indicatorStyle: itemError
|
|
610
|
+
? undefined
|
|
611
|
+
: itemProgress === 100
|
|
612
|
+
? { backgroundColor: "#8ae048" }
|
|
613
|
+
: { backgroundColor: "#9650fb" }, showValue: false }) })] }), isMobile ? (_jsx("div", { className: "space-y-3", children: jobs
|
|
614
|
+
.sort((a, b) => {
|
|
615
|
+
const getJobPriority = (job) => {
|
|
616
|
+
if (job.status === "In Progress")
|
|
617
|
+
return 1;
|
|
618
|
+
if (job.status === "Error")
|
|
619
|
+
return 2;
|
|
620
|
+
if (job.status === "Completed")
|
|
621
|
+
return 4;
|
|
622
|
+
return 3;
|
|
623
|
+
};
|
|
624
|
+
const priorityA = getJobPriority(a);
|
|
625
|
+
const priorityB = getJobPriority(b);
|
|
626
|
+
if (priorityA === priorityB) {
|
|
627
|
+
return a.targetLanguage.localeCompare(b.targetLanguage);
|
|
628
|
+
}
|
|
629
|
+
return priorityA - priorityB;
|
|
630
|
+
})
|
|
631
|
+
.map((job) => {
|
|
632
|
+
const progressKey = `${job.itemId}-${job.targetLanguage}`;
|
|
633
|
+
const progress = translationProgress.get(progressKey);
|
|
634
|
+
const date = new Date(job.timestamp);
|
|
635
|
+
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"
|
|
636
|
+
? "bg-green-100 text-green-700"
|
|
637
|
+
: job.status === "In Progress"
|
|
638
|
+
? ""
|
|
639
|
+
: job.status === "Error"
|
|
640
|
+
? "bg-red-100 text-red-600"
|
|
641
|
+
: "bg-gray-4 text-(--color-gray-1)"}`, style: job.status === "In Progress"
|
|
642
|
+
? {
|
|
643
|
+
backgroundColor: "#f6eeff",
|
|
644
|
+
color: "#9650fb",
|
|
645
|
+
}
|
|
646
|
+
: undefined, children: job.status })] }), job.message && (_jsx("div", { className: "text-xs text-gray-2 break-all whitespace-normal mb-2 leading-relaxed", children: job.message })), 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: {
|
|
647
|
+
backgroundColor: "#9650fb",
|
|
648
|
+
}, showValue: false })] })), _jsx("div", { className: "text-[10px] text-gray-2 text-right mt-1", children: date.toLocaleTimeString([], {
|
|
649
|
+
hour: "2-digit",
|
|
650
|
+
minute: "2-digit",
|
|
651
|
+
}) })] }, job.targetLanguage));
|
|
652
|
+
}) })) : (_jsx(SimpleTable, { columns: [
|
|
554
653
|
{
|
|
555
654
|
header: "Language",
|
|
556
655
|
className: "w-16", // Fixed width for language column
|
|
557
|
-
body: (job) => (_jsx("div", { className: "font-medium text-
|
|
656
|
+
body: (job) => (_jsx("div", { className: "font-medium text-(--color-dark)", children: job.targetLanguage })),
|
|
558
657
|
},
|
|
559
658
|
{
|
|
560
659
|
header: "Status",
|
|
@@ -568,26 +667,35 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
568
667
|
? ""
|
|
569
668
|
: job.status === "Error"
|
|
570
669
|
? "bg-red-100 text-red-600"
|
|
571
|
-
: "bg-
|
|
572
|
-
|
|
670
|
+
: "bg-gray-4 text-(--color-gray-1)"}`, style: job.status === "In Progress"
|
|
671
|
+
? {
|
|
672
|
+
backgroundColor: "#f6eeff",
|
|
673
|
+
color: "#9650fb",
|
|
674
|
+
}
|
|
675
|
+
: undefined, children: job.status }), job.status === "In Progress" &&
|
|
676
|
+
progress &&
|
|
677
|
+
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: {
|
|
678
|
+
backgroundColor: "#9650fb",
|
|
679
|
+
}, showValue: false, "data-testid": "translation-progress-bar" })] }))] }));
|
|
680
|
+
},
|
|
573
681
|
},
|
|
574
682
|
{
|
|
575
683
|
header: "Source",
|
|
576
684
|
className: "w-16", // Fixed width for source column
|
|
577
|
-
body: (job) => (_jsx("div", { className: "text-sm text-
|
|
685
|
+
body: (job) => (_jsx("div", { className: "text-sm text-(--color-gray-1)", children: job.sourceLanguage || "—" })),
|
|
578
686
|
},
|
|
579
687
|
{
|
|
580
688
|
header: "Message",
|
|
581
|
-
className: "w-
|
|
582
|
-
body: (job) => (_jsx("div", { className: "text-sm text-
|
|
689
|
+
className: "w-auto max-w-[300px]", // Use max-width instead of fixed width
|
|
690
|
+
body: (job) => (_jsx("div", { className: "text-sm text-gray-2 break-all whitespace-normal leading-relaxed", children: job.message || "—" })),
|
|
583
691
|
},
|
|
584
692
|
{
|
|
585
693
|
header: "Updated",
|
|
586
694
|
className: "w-24", // Fixed width for updated column
|
|
587
695
|
body: (job) => {
|
|
588
696
|
const date = new Date(job.timestamp);
|
|
589
|
-
return (_jsx("div", { className: "text-sm text-
|
|
590
|
-
}
|
|
697
|
+
return (_jsx("div", { className: "text-sm text-gray-2 whitespace-nowrap", children: date.toLocaleTimeString() }));
|
|
698
|
+
},
|
|
591
699
|
},
|
|
592
700
|
], items: jobs.sort((a, b) => {
|
|
593
701
|
// Sort by status first (in-progress first, then errors, then completed)
|
|
@@ -607,6 +715,6 @@ export function BatchTranslationView({ batchId, onBack }) {
|
|
|
607
715
|
return a.targetLanguage.localeCompare(b.targetLanguage);
|
|
608
716
|
}
|
|
609
717
|
return priorityA - priorityB;
|
|
610
|
-
}) })] }, itemId));
|
|
718
|
+
}) }))] }, itemId));
|
|
611
719
|
}) })) })] }));
|
|
612
720
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RecentTranslations.d.ts","sourceRoot":"","sources":["../../src/translation-center/RecentTranslations.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"RecentTranslations.d.ts","sourceRoot":"","sources":["../../src/translation-center/RecentTranslations.tsx"],"names":[],"mappings":"AAmEA,wBAAgB,kBAAkB,4CAyhBjC"}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState, useMemo } from "react";
|
|
3
|
+
import React from "react";
|
|
3
4
|
import { useRouter } from "next/navigation";
|
|
4
5
|
import { usePathname, useSearchParams } from "next/navigation";
|
|
5
6
|
import { Button, Select, useEditContext } from "@parhelia/core";
|
|
6
7
|
import { listBatches, getTranslationProviders, } from "../services/translationService";
|
|
7
8
|
import { useDebouncedCallback } from "use-debounce";
|
|
9
|
+
import { RefreshCw, X, ChevronDown, Languages, Loader2 } from "lucide-react";
|
|
10
|
+
// Wrappers for lucide-react icons to work with React 19
|
|
11
|
+
const RefreshIcon = (props) => React.createElement(RefreshCw, props);
|
|
12
|
+
const XIcon = (props) => React.createElement(X, props);
|
|
13
|
+
const ChevronDownIcon = (props) => React.createElement(ChevronDown, props);
|
|
14
|
+
const LanguagesIcon = (props) => React.createElement(Languages, props);
|
|
15
|
+
const LoaderIcon = (props) => React.createElement(Loader2, props);
|
|
8
16
|
const DATE_RANGE_OPTIONS = [
|
|
9
17
|
{ value: "lastHour", label: "Last Hour" },
|
|
10
18
|
{ value: "last24hours", label: "Last 24 Hours" },
|
|
@@ -258,7 +266,8 @@ export function RecentTranslations() {
|
|
|
258
266
|
router.push(`${pathname}?${current.toString()}`, { scroll: false });
|
|
259
267
|
};
|
|
260
268
|
const totalBatches = groupedBatches.reduce((sum, group) => sum + group.batches.length, 0);
|
|
261
|
-
|
|
269
|
+
const isMobile = editContext?.isMobile ?? false;
|
|
270
|
+
return (_jsxs("div", { className: "flex h-full flex-col min-h-0 bg-gray-5", "data-testid": "recent-translations", children: [_jsxs("div", { className: "shrink-0 border-b border-gray-3 bg-background p-4 md:p-6", children: [_jsxs("div", { className: "flex items-center justify-between mb-4", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold text-(--color-dark)", children: "Recent Translations" }), _jsx("p", { className: "text-sm text-gray-2 mt-1", children: "View and monitor translation jobs across all items" })] }), _jsx("div", { className: "flex gap-2", children: _jsxs(Button, { size: "sm", variant: "outline", onClick: () => loadRecentBatches(0, false), disabled: isLoading, 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" })] }) })] }), _jsxs("div", { className: "flex flex-wrap gap-4 items-center", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs font-medium text-(--color-gray-1)", children: "Date Range:" }), _jsx(Select, { className: "w-40", options: DATE_RANGE_OPTIONS, value: filters.dateRange, onValueChange: (value) => setFilters((prev) => ({ ...prev, dateRange: value })), placeholder: "Select date range" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs font-medium text-(--color-gray-1)", children: "Status:" }), _jsx(Select, { className: "w-36", options: STATUS_OPTIONS, value: filters.status, onValueChange: (value) => setFilters((prev) => ({ ...prev, status: value })), placeholder: "Select status" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs font-medium text-(--color-gray-1)", children: "User:" }), _jsx(Select, { className: "w-44", options: [
|
|
262
271
|
{ value: "all", label: "All Users" },
|
|
263
272
|
...(currentUserName !== "all"
|
|
264
273
|
? [{ value: currentUserName, label: `${currentUserName} (You)` }]
|
|
@@ -266,21 +275,21 @@ export function RecentTranslations() {
|
|
|
266
275
|
...uniqueUsers
|
|
267
276
|
.filter((u) => u !== currentUserName)
|
|
268
277
|
.map((u) => ({ value: u, label: u })),
|
|
269
|
-
], value: isLimitedPreviewUser ? currentUserName : filters.user, onValueChange: (value) => setFilters((prev) => ({ ...prev, user: value })), disabled: isLimitedPreviewUser, placeholder: "Select user" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs font-medium text-
|
|
278
|
+
], value: isLimitedPreviewUser ? currentUserName : filters.user, onValueChange: (value) => setFilters((prev) => ({ ...prev, user: value })), disabled: isLimitedPreviewUser, placeholder: "Select user" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs font-medium text-(--color-gray-1)", children: "Provider:" }), _jsx(Select, { className: "w-44", options: [
|
|
270
279
|
{ value: "all", label: "All Providers" },
|
|
271
280
|
...providers.map((p) => ({
|
|
272
281
|
value: p.name,
|
|
273
282
|
label: p.displayName || p.name,
|
|
274
283
|
})),
|
|
275
|
-
], value: filters.provider, onValueChange: (value) => setFilters((prev) => ({ ...prev, provider: value })), placeholder: "Select provider" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs font-medium text-
|
|
284
|
+
], value: filters.provider, onValueChange: (value) => setFilters((prev) => ({ ...prev, provider: value })), placeholder: "Select provider" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs font-medium text-(--color-gray-1)", children: "Target Language:" }), _jsx(Select, { className: "w-44", options: [
|
|
276
285
|
{ value: "all", label: "All Languages" },
|
|
277
286
|
...uniqueLanguages.map((lang) => ({
|
|
278
287
|
value: lang,
|
|
279
288
|
label: lang,
|
|
280
289
|
})),
|
|
281
|
-
], value: filters.targetLanguage, onValueChange: (value) => setFilters((prev) => ({ ...prev, targetLanguage: value })), placeholder: "Select language" })] }), (filters.dateRange !== "last30days" || filters.status !== "all" || filters.user !== currentUserName || filters.provider !== "all" || filters.targetLanguage !== "all") && (_jsxs(Button, { size: "sm", variant: "ghost", onClick: () => setFilters({ dateRange: "last30days", status: "all", user: currentUserName, provider: "all", targetLanguage: "all" }), children: [_jsx("i", { className: "pi pi-times" }), "Clear Filters"] }))] }), _jsxs("div", { className: "mt-3 text-xs text-
|
|
290
|
+
], value: filters.targetLanguage, onValueChange: (value) => setFilters((prev) => ({ ...prev, targetLanguage: value })), placeholder: "Select language" })] }), (filters.dateRange !== "last30days" || filters.status !== "all" || filters.user !== currentUserName || filters.provider !== "all" || filters.targetLanguage !== "all") && (_jsxs(Button, { size: "sm", variant: "ghost", onClick: () => setFilters({ dateRange: "last30days", status: "all", user: currentUserName, provider: "all", targetLanguage: "all" }), children: [_jsx("i", { className: "pi pi-times" }), "Clear Filters"] }))] }), _jsxs("div", { className: "mt-3 text-xs text-gray-2", children: ["Showing ", totalBatches, " batch", totalBatches !== 1 ? 'es' : '', batches.length !== totalBatches && (_jsxs("span", { children: [" (filtered from ", batches.length, " total batches)"] }))] })] }), _jsx("div", { className: "flex-1 overflow-auto p-4 md:p-6 min-h-0", children: isLoading && batches.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 recent translations..."] }) })) : groupedBatches.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-3" }), _jsx("p", { className: "font-medium text-(--color-gray-1)", children: "No translations found" }), _jsx("p", { className: "text-sm mt-1", children: filteredBatches.length !== batches.length
|
|
282
291
|
? "Try adjusting your filters or start a translation to see it appear here"
|
|
283
|
-
: "Start a translation to see it appear here" })] }) })) : (_jsxs("div", { className: "space-y-6", children: [groupedBatches.map((dateGroup) => (_jsxs("div", { children: [_jsxs("h2", { className: "text-lg font-semibold text-
|
|
292
|
+
: "Start a translation to see it appear here" })] }) })) : (_jsxs("div", { className: "space-y-6", children: [groupedBatches.map((dateGroup) => (_jsxs("div", { children: [_jsxs("h2", { className: "text-lg font-semibold text-(--color-dark) mb-3 border-b border-gray-3 pb-2", children: [dateGroup.label, _jsxs("span", { className: "ml-2 text-sm font-normal text-gray-2", children: ["(", dateGroup.batches.length, " batch", dateGroup.batches.length !== 1 ? 'es' : '', ")"] })] }), _jsx("div", { className: "space-y-3", children: dateGroup.batches.map((b) => {
|
|
284
293
|
const batchDate = new Date(b.timestamp);
|
|
285
294
|
const now = new Date();
|
|
286
295
|
const isToday = batchDate.toDateString() === now.toDateString();
|
|
@@ -295,8 +304,8 @@ export function RecentTranslations() {
|
|
|
295
304
|
const completedJobs = info?.completedJobs ?? 0;
|
|
296
305
|
const anyError = (info?.errorJobs ?? 0) > 0;
|
|
297
306
|
const anyInProgress = info?.status === "In Progress";
|
|
298
|
-
return (_jsx("div", { className: "border border-
|
|
299
|
-
info.status === 'In Progress' ? 'bg-
|
|
300
|
-
info.status === 'Error' ? 'bg-red-100 text-red-600' : 'bg-
|
|
301
|
-
}) })] }, dateGroup.label))), hasMore && (_jsx("div", { className: "flex justify-center pt-6", children: _jsx(Button, { variant: "outline", size: "sm", onClick: loadMore, disabled: isLoadingMore, children: isLoadingMore ? (_jsxs(_Fragment, { children: [_jsx(
|
|
307
|
+
return (_jsx("div", { className: "border border-gray-3 rounded-lg bg-background shadow-sm overflow-hidden transition-shadow hover:shadow-md", children: _jsx("button", { className: "w-full px-4 py-3 md:px-6 md:py-4 hover:bg-gray-5 transition-colors text-left", onClick: () => navigateToBatch(b.id), title: "View batch details", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "text-left", children: [_jsx("div", { className: "font-medium text-(--color-dark)", children: `Translation Batch - ${timeDisplay}` }), _jsxs("div", { className: "text-sm text-gray-2 mt-1 flex flex-wrap items-center gap-x-2", children: [_jsxs("span", { className: "font-medium text-(--color-gray-1)", children: [totalJobs, " translations"] }), typeof itemsCount === 'number' && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-gray-3", children: "\u2022" }), _jsxs("span", { children: [itemsCount, " items"] })] })), languages.length ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-gray-3", children: "\u2022" }), _jsx("span", { className: "truncate max-w-[200px]", title: languages.join(", "), children: languages.join(", ") })] })) : null, totalJobs > 0 && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-gray-3", children: "\u2022" }), _jsxs("span", { className: "text-xs", children: [completedJobs, "/", totalJobs, " completed"] })] }))] }), _jsx("div", { className: "mt-2 block", children: _jsxs("span", { className: "inline-flex text-[10px] bg-gray-4 text-(--color-gray-1) px-2 py-0.5 rounded font-mono whitespace-nowrap overflow-hidden", children: ["ID: ", b.id] }) }), _jsxs("div", { className: "mt-2 flex flex-wrap gap-2 items-center text-xs", children: [info?.status && (_jsx("span", { className: `inline-flex items-center rounded-full px-2.5 py-1 font-medium ${info.status === 'Completed' ? 'bg-green-100 text-green-700' :
|
|
308
|
+
info.status === 'In Progress' ? 'bg-theme-secondary-light text-theme-secondary' :
|
|
309
|
+
info.status === 'Error' ? 'bg-red-100 text-red-600' : 'bg-gray-4 text-(--color-gray-1)'}`, children: info.status })), info?.provider && (_jsxs("span", { className: "text-(--color-gray-1)", children: ["Provider: ", _jsx("span", { className: "font-medium", children: info.provider })] })), info?.initiatedByUser && (_jsxs("span", { className: "text-(--color-gray-1)", children: ["By: ", _jsx("span", { className: "font-medium", children: info.initiatedByUser })] })), info?.lastUpdatedUtc && (_jsxs("span", { className: "text-gray-2", children: ["Updated: ", new Date(info.lastUpdatedUtc).toLocaleString()] }))] })] }), _jsxs("div", { className: "flex gap-2", children: [anyInProgress && (_jsx("span", { className: "rounded-full bg-theme-secondary-light px-3 py-1 text-xs font-medium text-theme-secondary", children: "In Progress" })), anyError && (_jsx("span", { className: "rounded-full bg-red-100 px-3 py-1 text-xs font-medium text-red-600", children: "Error" })), !anyInProgress && !anyError && (_jsx("span", { className: "rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-700", children: "Completed" }))] })] }) }) }, b.id));
|
|
310
|
+
}) })] }, dateGroup.label))), hasMore && (_jsx("div", { className: "flex justify-center pt-6", children: _jsx(Button, { variant: "outline", size: "sm", onClick: loadMore, disabled: isLoadingMore, children: isLoadingMore ? (_jsxs(_Fragment, { children: [_jsx(LoaderIcon, { className: "h-4 w-4 animate-spin text-theme-secondary" }), "Loading more..."] })) : (_jsxs(_Fragment, { children: [_jsx(ChevronDownIcon, { className: "h-4 w-4" }), "Load More Batches"] })) }) }))] })) })] }));
|
|
302
311
|
}
|