@parhelia/localization 0.1.12911 → 0.1.12917

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.
@@ -1,23 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useRef, useState, useMemo } from "react";
2
+ import { useCallback, useEffect, useRef, useState, useMemo, useSyncExternalStore, } from "react";
3
3
  import React from "react";
4
- import { Button, Badge, Select, Input, UserPicker, Popover, PopoverContent, PopoverTrigger, ContentTree, fetchItemStubs, searchUsers as searchBackendUsers, useEditContext, getLanguages, SimpleIconButton, DeleteIcon, } from "@parhelia/core";
5
- import { listBatches, getTranslationProviders, listBatchTranslationJobs, retryBatchTranslation, abortBatch, deleteBatch, } from "../services/translationService";
4
+ import { Button, Badge, Select, Input, UserPicker, Popover, PopoverContent, PopoverTrigger, Tooltip, TooltipContent, TooltipTrigger, ContentTree, PageHeader, fetchItemStubs, searchUsers as searchBackendUsers, useEditContext, getLanguages, SimpleIconButton, formatDateTime, } from "@parhelia/core";
5
+ import { listBatches, getTranslationProviders, listBatchTranslationJobs, retryBatchTranslation, abortJob, abortBatch, } from "../services/translationService";
6
6
  import { TRANSLATION_BATCH_STARTED_EVENT, } from "../translationEvents";
7
7
  import { useDebouncedCallback } from "use-debounce";
8
- import { X, ChevronDown, Languages, Loader2, Calendar, CheckCircle2, User as UserIcon2, Cloud, Globe, FileText, FolderTree, ExternalLink, Check, AlertCircle, CircleStop, Hourglass, Copy, Sparkles, RotateCcw, } from "lucide-react";
8
+ import { X, ChevronDown, Languages, Loader2, Globe, FolderTree, ExternalLink, Check, AlertCircle, CircleStop, Hourglass, Copy, Sparkles, RotateCcw, Plus, } from "lucide-react";
9
9
  import { toast } from "sonner";
10
+ import { openLocalizeItemDialog } from "../LocalizeItemCommand";
10
11
  // Wrappers for lucide-react icons to work with React 19
11
12
  const XIcon = (props) => React.createElement(X, props);
12
13
  const ChevronDownIcon = (props) => React.createElement(ChevronDown, props);
13
14
  const LanguagesIcon = (props) => React.createElement(Languages, props);
14
15
  const LoaderIcon = (props) => React.createElement(Loader2, props);
15
- const CalendarIcon = (props) => React.createElement(Calendar, props);
16
- const StatusIcon = (props) => React.createElement(CheckCircle2, props);
17
- const UserFilterIcon = (props) => React.createElement(UserIcon2, props);
18
- const ProviderIcon = (props) => React.createElement(Cloud, props);
19
16
  const GlobeIcon = (props) => React.createElement(Globe, props);
20
- const ItemIcon = (props) => React.createElement(FileText, props);
21
17
  const FolderTreeIcon = (props) => React.createElement(FolderTree, props);
22
18
  const ExternalLinkIcon = (props) => React.createElement(ExternalLink, props);
23
19
  const CheckIcon = (props) => React.createElement(Check, props);
@@ -27,6 +23,70 @@ const QueuedIcon = (props) => React.createElement(Hourglass, props);
27
23
  const CopyIcon = (props) => React.createElement(Copy, props);
28
24
  const SparklesIcon = (props) => React.createElement(Sparkles, props);
29
25
  const RetryIcon = (props) => React.createElement(RotateCcw, props);
26
+ const PlusIcon = (props) => React.createElement(Plus, props);
27
+ function renderTargetLanguageIcon(language) {
28
+ if (language.icon) {
29
+ return React.createElement("img", {
30
+ src: language.icon,
31
+ className: "h-4 w-5 object-cover",
32
+ alt: "",
33
+ "aria-hidden": true,
34
+ });
35
+ }
36
+ return React.createElement(GlobeIcon, {
37
+ className: "h-3.5 w-3.5 text-neutral-grey-50",
38
+ strokeWidth: 1.5,
39
+ "aria-hidden": true,
40
+ });
41
+ }
42
+ const DEFAULT_FILTERS = {
43
+ dateRange: "last30days",
44
+ status: "all",
45
+ user: "all",
46
+ provider: "all",
47
+ targetLanguage: "all",
48
+ itemIdOrPath: "",
49
+ itemIncludeSubitems: false,
50
+ };
51
+ let filterSnapshot = {
52
+ filters: DEFAULT_FILTERS,
53
+ itemIdOrPathInput: "",
54
+ };
55
+ let defaultUserInitialized = false;
56
+ const filterListeners = new Set();
57
+ function emitFilterSnapshot() {
58
+ filterListeners.forEach((listener) => listener());
59
+ }
60
+ function subscribeFilterSnapshot(listener) {
61
+ filterListeners.add(listener);
62
+ return () => filterListeners.delete(listener);
63
+ }
64
+ function getFilterSnapshot() {
65
+ return filterSnapshot;
66
+ }
67
+ function setTranslationBatchFilters(updater) {
68
+ const nextFilters = typeof updater === "function" ? updater(filterSnapshot.filters) : updater;
69
+ filterSnapshot = { ...filterSnapshot, filters: nextFilters };
70
+ emitFilterSnapshot();
71
+ }
72
+ function setTranslationBatchItemInput(value) {
73
+ filterSnapshot = { ...filterSnapshot, itemIdOrPathInput: value };
74
+ emitFilterSnapshot();
75
+ }
76
+ function ensureDefaultTranslationBatchUser(currentUserName) {
77
+ if (defaultUserInitialized || currentUserName === "all")
78
+ return;
79
+ defaultUserInitialized = true;
80
+ if (filterSnapshot.filters.user !== "all")
81
+ return;
82
+ setTranslationBatchFilters((previous) => ({
83
+ ...previous,
84
+ user: currentUserName,
85
+ }));
86
+ }
87
+ function useTranslationBatchFilterSnapshot() {
88
+ return useSyncExternalStore(subscribeFilterSnapshot, getFilterSnapshot, getFilterSnapshot);
89
+ }
30
90
  const DATE_RANGE_OPTIONS = [
31
91
  { value: "lastHour", label: "Last Hour" },
32
92
  { value: "last24hours", label: "Last 24 Hours" },
@@ -78,6 +138,9 @@ function normalizeGuid(value) {
78
138
  .replace(/[{}()]/g, "")
79
139
  .toLowerCase();
80
140
  }
141
+ function normalizeLanguageCode(value) {
142
+ return (value ?? "").toString().trim().toLowerCase();
143
+ }
81
144
  function dateRangeToFromUtc(range) {
82
145
  const now = new Date();
83
146
  const cutoff = new Date(now);
@@ -114,20 +177,17 @@ function dateRangeToFromUtc(range) {
114
177
  export function TranslationBatches() {
115
178
  const editContext = useEditContext();
116
179
  const [batches, setBatches] = useState([]);
117
- const [providers, setProviders] = useState([]);
118
180
  const [sitecoreLanguages, setSitecoreLanguages] = useState([]);
119
181
  const [isLoading, setIsLoading] = useState(false);
120
182
  const [isLoadingMore, setIsLoadingMore] = useState(false);
121
183
  const [hasMore, setHasMore] = useState(true);
122
184
  const [currentOffset, setCurrentOffset] = useState(0);
123
- const [itemIdOrPathInput, setItemIdOrPathInput] = useState("");
124
- const [itemPickerOpen, setItemPickerOpen] = useState(false);
125
185
  const [expandedBatchId, setExpandedBatchId] = useState(null);
126
186
  const [expandedItems, setExpandedItems] = useState(new Set());
127
187
  const [batchJobs, setBatchJobs] = useState({});
128
188
  const [loadingJobs, setLoadingJobs] = useState(new Set());
129
189
  const [abortingBatchIds, setAbortingBatchIds] = useState(new Set());
130
- const [deletingBatchIds, setDeletingBatchIds] = useState(new Set());
190
+ const [abortingJobIds, setAbortingJobIds] = useState(new Set());
131
191
  const [retryingKeys, setRetryingKeys] = useState(new Set());
132
192
  // Cache of userName → displayName so we can render full names on batch rows
133
193
  // without re-querying the user service every render.
@@ -138,21 +198,41 @@ export function TranslationBatches() {
138
198
  const requestIdRef = useRef(0);
139
199
  const currentUserName = editContext?.user?.name || "all";
140
200
  const isLimitedPreviewUser = editContext?.user?.isLimitedPreviewUser ?? false;
141
- const [filters, setFilters] = useState({
142
- dateRange: "last30days",
143
- status: "all",
144
- user: currentUserName,
145
- provider: "all",
146
- targetLanguage: "all",
147
- itemIdOrPath: "",
148
- itemIncludeSubitems: false,
149
- });
201
+ const isMobile = editContext?.isMobile ?? false;
202
+ const rootRef = useRef(null);
203
+ const [isCompactBatches, setIsCompactBatches] = useState(isMobile);
204
+ const { filters } = useTranslationBatchFilterSnapshot();
205
+ useEffect(() => {
206
+ const node = rootRef.current;
207
+ if (!node)
208
+ return;
209
+ const updateLayout = () => {
210
+ setIsCompactBatches(isMobile || node.clientWidth < 640);
211
+ };
212
+ updateLayout();
213
+ let observer;
214
+ if (typeof ResizeObserver !== "undefined") {
215
+ observer = new ResizeObserver(updateLayout);
216
+ observer.observe(node);
217
+ }
218
+ window.addEventListener("resize", updateLayout);
219
+ return () => {
220
+ observer?.disconnect();
221
+ window.removeEventListener("resize", updateLayout);
222
+ };
223
+ }, [isMobile]);
224
+ useEffect(() => {
225
+ ensureDefaultTranslationBatchUser(currentUserName);
226
+ }, [currentUserName]);
150
227
  // Enforce current user filter for limited preview users
151
228
  useEffect(() => {
152
229
  if (isLimitedPreviewUser &&
153
230
  currentUserName !== "all" &&
154
231
  filters.user !== currentUserName) {
155
- setFilters((prev) => ({ ...prev, user: currentUserName }));
232
+ setTranslationBatchFilters((prev) => ({
233
+ ...prev,
234
+ user: currentUserName,
235
+ }));
156
236
  }
157
237
  }, [isLimitedPreviewUser, currentUserName, filters.user]);
158
238
  const effectiveFilters = useMemo(() => {
@@ -220,29 +300,12 @@ export function TranslationBatches() {
220
300
  }
221
301
  }, []);
222
302
  const debouncedReload = useDebouncedCallback(() => loadRecentBatches(0, false, effectiveFilters), 800);
223
- const debouncedSetItemFilter = useDebouncedCallback((value) => setFilters((prev) => ({ ...prev, itemIdOrPath: value })), 400);
224
303
  const loadMore = () => {
225
304
  if (!isLoadingMore && hasMore) {
226
305
  loadRecentBatches(currentOffset, true, effectiveFilters);
227
306
  }
228
307
  };
229
- // Load providers
230
- useEffect(() => {
231
- const loadProviders = async () => {
232
- try {
233
- const res = await getTranslationProviders();
234
- const maybeProviders = res?.data ?? res;
235
- setProviders(Array.isArray(maybeProviders)
236
- ? maybeProviders
237
- : []);
238
- }
239
- catch (error) {
240
- console.error("Failed to load translation providers:", error);
241
- }
242
- };
243
- loadProviders();
244
- }, []);
245
- // Load Sitecore languages once for the Target Language dropdown
308
+ // Load Sitecore languages for language chips in expanded batches.
246
309
  useEffect(() => {
247
310
  const loadLanguages = async () => {
248
311
  try {
@@ -385,13 +448,6 @@ export function TranslationBatches() {
385
448
  // refresh that brings the UI to its final state.
386
449
  };
387
450
  }, [editContext]);
388
- const searchTranslationUsers = useCallback((query, limit) => searchBackendUsers(query, limit), []);
389
- // Languages available in the Target Language dropdown — sourced from Sitecore so
390
- // the list is stable regardless of the current filter selection.
391
- const languageOptions = useMemo(() => sitecoreLanguages
392
- .map((l) => l.languageCode || l.name)
393
- .filter((code) => !!code)
394
- .sort(), [sitecoreLanguages]);
395
451
  // Group by date. Filtering happens server-side, so we operate on `batches` directly.
396
452
  const groupedBatches = useMemo(() => {
397
453
  if (batches.length === 0)
@@ -547,41 +603,54 @@ export function TranslationBatches() {
547
603
  loadBatchJobs,
548
604
  loadRecentBatches,
549
605
  ]);
550
- const handleDeleteBatch = useCallback((batchId) => {
606
+ const handleAbortJob = useCallback((job) => {
607
+ if (!job.id || !job.batchId) {
608
+ toast.error("This translation job cannot be aborted.");
609
+ return;
610
+ }
611
+ const jobId = job.id;
612
+ const batchId = job.batchId;
613
+ const targetLabel = job.targetLanguage
614
+ ? `${job.targetLanguage} translation`
615
+ : "translation";
551
616
  editContext?.confirm({
552
- header: "Delete Translation History",
553
- message: "Delete this translation batch from history? This removes only translation job records; it does not delete Sitecore items or translated content.",
554
- acceptLabel: "Delete",
617
+ header: "Abort Translation Job",
618
+ message: `Abort this ${targetLabel} job? This stops the remaining translation work for this language, but it does not roll back content that was already written.`,
619
+ acceptLabel: "Abort",
555
620
  showCancel: true,
556
621
  accept: async () => {
557
- setDeletingBatchIds((prev) => new Set(prev).add(batchId));
622
+ setAbortingJobIds((prev) => new Set(prev).add(jobId));
558
623
  try {
559
- const result = await deleteBatch(batchId);
624
+ const result = await abortJob(jobId);
560
625
  if (result.type !== "success") {
561
626
  toast.error(result.details ||
562
627
  result.summary ||
563
- "Failed to delete translation batch.");
628
+ "Failed to abort translation job.");
564
629
  return;
565
630
  }
566
- toast.success("Translation batch deleted.");
567
- setBatches((prev) => prev.filter((batch) => batch.id !== batchId));
568
- setBatchJobs((prev) => {
569
- const next = { ...prev };
570
- delete next[batchId];
571
- return next;
572
- });
573
- setExpandedBatchId((prev) => (prev === batchId ? null : prev));
631
+ toast.success("Translation job aborted.");
632
+ await loadRecentBatches(0, false, effectiveFilters);
633
+ if (expandedBatchId === batchId || batchJobs[batchId]) {
634
+ await loadBatchJobs(batchId);
635
+ }
574
636
  }
575
637
  finally {
576
- setDeletingBatchIds((prev) => {
638
+ setAbortingJobIds((prev) => {
577
639
  const next = new Set(prev);
578
- next.delete(batchId);
640
+ next.delete(jobId);
579
641
  return next;
580
642
  });
581
643
  }
582
644
  },
583
645
  });
584
- }, [editContext]);
646
+ }, [
647
+ batchJobs,
648
+ editContext,
649
+ effectiveFilters,
650
+ expandedBatchId,
651
+ loadBatchJobs,
652
+ loadRecentBatches,
653
+ ]);
585
654
  const handleRetryJobs = useCallback(async (batchId, provider, jobs, retryKey) => {
586
655
  const sessionId = editContext?.sessionId;
587
656
  if (!sessionId) {
@@ -652,67 +721,302 @@ export function TranslationBatches() {
652
721
  const openItemInEditor = useCallback(async (itemId, language) => {
653
722
  const normalized = (itemId ?? "").toString().toLowerCase();
654
723
  editContext?.setShowAgentsWorkspaceEditor(true);
655
- await editContext?.loadItem({ id: normalized, language, version: 0 });
724
+ await editContext?.loadItem(language ? { id: normalized, language, version: 0 } : normalized);
656
725
  }, [editContext]);
657
- const totalBatches = groupedBatches.reduce((sum, group) => sum + group.batches.length, 0);
658
- const isMobile = editContext?.isMobile ?? false;
726
+ return (_jsxs("div", { ref: rootRef, className: "flex h-full min-h-0 flex-col bg-neutral-grey-5", style: { padding: isCompactBatches ? 12 : 40 }, "data-testid": "translation-batches", children: [_jsx(PageHeader, { title: "Translation Batches", description: isMobile
727
+ ? undefined
728
+ : "Review translation history and manage batch progress", variant: "workspace", className: isCompactBatches ? "mb-4" : "mb-5" }), _jsx("main", { className: "min-h-0 min-w-0 flex-1 overflow-hidden rounded-[12px] bg-white", children: _jsx("div", { className: `h-full min-h-0 overflow-auto ${isCompactBatches ? "p-3" : "p-6"}`, 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-neutral-grey-50", children: [_jsx(LoaderIcon, { className: "h-5 w-5 animate-spin text-highlight-100" }), "Loading recent translations..."] }) })) : groupedBatches.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-32", children: _jsxs("div", { className: "text-center text-neutral-grey-50", children: [_jsx(LanguagesIcon, { className: "h-8 w-8 block mx-auto mb-2 text-neutral-grey-15" }), _jsx("p", { className: "font-medium text-neutral-grey-100", children: "No translations found" }), _jsx("p", { className: "text-sm mt-1", children: (() => {
729
+ const hasActiveFilters = filters.dateRange !== "last30days" ||
730
+ filters.status !== "all" ||
731
+ filters.user !== currentUserName ||
732
+ filters.provider !== "all" ||
733
+ filters.targetLanguage !== "all" ||
734
+ filters.itemIdOrPath.trim() !== "" ||
735
+ filters.itemIncludeSubitems;
736
+ return hasActiveFilters
737
+ ? "Try adjusting your filters or start a translation to see it appear here"
738
+ : "Start a translation to see it appear here";
739
+ })() })] }) })) : (_jsxs("div", { className: isCompactBatches ? "space-y-6" : "space-y-10", children: [groupedBatches.map((dateGroup) => (_jsxs("div", { children: [_jsxs("div", { className: "flex items-baseline gap-2 mb-2", children: [_jsx("h2", { className: "text-[11px] font-medium tracking-[0.08em] text-neutral-grey-50", children: dateGroup.label }), _jsx("span", { className: "flex-1 ml-2 border-t border-border-default" })] }), _jsx("div", { className: "divide-border-default divide-y overflow-hidden rounded-lg bg-white", children: dateGroup.batches.map((b) => {
740
+ const batchDate = new Date(b.timestamp);
741
+ const formattedBatchDate = formatDateTime(batchDate);
742
+ const info = b.info;
743
+ const totalJobs = info?.expectedJobs ?? 0;
744
+ const itemsCount = typeof info?.itemsCount === "number"
745
+ ? info.itemsCount
746
+ : undefined;
747
+ const languagesCsv = info?.languages;
748
+ const languages = languagesCsv
749
+ ? languagesCsv.split(",")
750
+ : [];
751
+ const completedJobs = info?.completedJobs ?? 0;
752
+ const errorJobs = info?.errorJobs ?? 0;
753
+ const anyError = errorJobs > 0;
754
+ const anyInProgress = isActiveBatchStatus(info?.status);
755
+ const anyAborted = info?.status === "Aborted";
756
+ const progressPct = totalJobs > 0
757
+ ? Math.min(100, Math.round((completedJobs / totalJobs) * 100))
758
+ : 0;
759
+ const progressTone = anyError
760
+ ? "error"
761
+ : anyAborted
762
+ ? "aborted"
763
+ : progressPct >= 100
764
+ ? "complete"
765
+ : "active";
766
+ const metaParts = [];
767
+ metaParts.push(_jsxs("span", { className: "text-neutral-grey-100", children: [totalJobs, " translation", totalJobs !== 1 ? "s" : ""] }, "trans"));
768
+ if (typeof itemsCount === "number") {
769
+ metaParts.push(_jsxs("span", { children: [itemsCount, " item", itemsCount !== 1 ? "s" : ""] }, "items"));
770
+ }
771
+ if (languages.length > 0) {
772
+ metaParts.push(_jsxs("span", { title: languages.join(", "), children: [languages.length, " language", languages.length !== 1 ? "s" : ""] }, "langs"));
773
+ }
774
+ if (info?.provider) {
775
+ metaParts.push(_jsx("span", { children: info.provider }, "prov"));
776
+ }
777
+ if (info?.totalCost != null) {
778
+ metaParts.push(_jsxs("span", { children: ["Cost:", " ", _jsx("span", { className: "font-medium text-neutral-grey-100", children: formatUsdCost(info.totalCost) })] }, "cost"));
779
+ }
780
+ const ownerPart = info?.initiatedByUser ? (_jsx("span", { title: info.initiatedByUser, children: userDisplayNames[info.initiatedByUser] ||
781
+ info.initiatedByUser }, "user")) : null;
782
+ const datePart = info?.lastUpdatedUtc ? (_jsx("span", { children: formatDateTime(new Date(info.lastUpdatedUtc)) }, "date")) : null;
783
+ const desktopMetaParts = [...metaParts];
784
+ if (!isCompactBatches) {
785
+ if (ownerPart)
786
+ desktopMetaParts.push(ownerPart);
787
+ if (datePart)
788
+ desktopMetaParts.push(datePart);
789
+ }
790
+ const mobileFooterMetaParts = [];
791
+ if (isCompactBatches) {
792
+ if (datePart)
793
+ mobileFooterMetaParts.push(datePart);
794
+ if (ownerPart)
795
+ mobileFooterMetaParts.push(ownerPart);
796
+ }
797
+ const batchDisplayName = (() => {
798
+ let parsedName;
799
+ if (info?.metadata) {
800
+ try {
801
+ const m = typeof info.metadata === "string"
802
+ ? JSON.parse(info.metadata)
803
+ : info.metadata;
804
+ const candidate = m?.name;
805
+ if (typeof candidate === "string" &&
806
+ candidate.trim()) {
807
+ parsedName = candidate.trim();
808
+ }
809
+ }
810
+ catch {
811
+ // Metadata isn't JSON - fall through to timestamp.
812
+ }
813
+ }
814
+ return (parsedName ??
815
+ `Translation Batch · ${formattedBatchDate}`);
816
+ })();
817
+ const isExpanded = expandedBatchId === b.id;
818
+ const jobs = batchJobs[b.id];
819
+ const isLoadingThis = loadingJobs.has(b.id);
820
+ const canRetry = anyError;
821
+ const canAbort = isActiveBatchStatus(info?.status);
822
+ const isRetryingBatch = retryingKeys.has(`batch:${b.id}`);
823
+ const isAborting = abortingBatchIds.has(b.id);
824
+ const hasBatchActions = canRetry || canAbort;
825
+ // Stable test-id status used by Playwright assertions.
826
+ // Matches the visual status dot/spinner logic above.
827
+ const testStatus = anyError
828
+ ? "error"
829
+ : anyInProgress
830
+ ? "in-progress"
831
+ : anyAborted
832
+ ? "aborted"
833
+ : "completed";
834
+ const batchActions = (_jsxs(_Fragment, { children: [canRetry && (_jsx(SimpleIconButton, { onClick: (event) => {
835
+ event.stopPropagation();
836
+ if (!canRetry)
837
+ return;
838
+ void handleRetryBatch(info);
839
+ }, icon: _jsx(RetryIcon, { className: `h-3.5 w-3.5 ${isRetryingBatch ? "animate-spin" : ""}`, strokeWidth: 1.5 }), label: "Retry failed translations", disabled: !canRetry ||
840
+ isRetryingBatch ||
841
+ isLoadingThis ||
842
+ isAborting })), canAbort && (_jsx(SimpleIconButton, { onClick: (event) => {
843
+ event.stopPropagation();
844
+ if (!canAbort)
845
+ return;
846
+ handleAbortBatch(b.id);
847
+ }, icon: _jsx(AbortIcon, { className: `h-3.5 w-3.5 ${isAborting ? "animate-spin" : ""}`, strokeWidth: 1.5 }), label: "Abort translation batch", disabled: !canAbort || isAborting }))] }));
848
+ return (_jsxs("div", { "data-testid": `batch-row-${b.id}`, "data-batch-status": testStatus, children: [_jsx("div", { role: "button", tabIndex: 0, className: `group w-full cursor-pointer py-3 text-left transition-colors hover:bg-neutral-grey-5 ${isCompactBatches ? "px-2.5" : "px-4"}`, onClick: () => toggleBatch(b.id), onKeyDown: (e) => {
849
+ if (e.key === "Enter" || e.key === " ") {
850
+ e.preventDefault();
851
+ toggleBatch(b.id);
852
+ }
853
+ }, children: _jsxs("div", { className: `flex items-start ${isCompactBatches ? "gap-2" : "gap-4"}`, children: [_jsx("div", { className: `shrink-0 text-neutral-grey-100 transition-transform ${isCompactBatches ? "mt-0.5" : "mt-3"} ${isExpanded ? "rotate-180" : ""}`, children: _jsx(ChevronDownIcon, { className: "h-4 w-4" }) }), !isCompactBatches && totalJobs > 0 && (_jsx(BatchProgressIndicator, { value: progressPct, completedJobs: completedJobs, totalJobs: totalJobs, errorJobs: errorJobs, tone: progressTone, testId: `batch-progress-${b.id}` })), _jsxs("div", { className: `flex min-w-0 flex-1 ${isCompactBatches
854
+ ? "flex-col gap-2"
855
+ : "items-center justify-between gap-3"}`, children: [_jsxs("div", { className: "grid min-w-0 flex-1 grid-cols-[minmax(0,1fr)]", children: [_jsx("span", { className: `min-w-0 text-[14px] font-medium text-neutral-grey-100 ${isCompactBatches
856
+ ? "whitespace-normal break-words leading-5"
857
+ : "truncate"}`, style: isCompactBatches
858
+ ? { overflowWrap: "anywhere" }
859
+ : undefined, title: formattedBatchDate, children: batchDisplayName }), _jsx("div", { className: "mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[12px] text-neutral-grey-50", children: desktopMetaParts.map((part, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx("span", { className: "text-neutral-grey-15", children: "\u00B7" })), part] }, i))) }), mobileFooterMetaParts.length > 0 && (_jsx("div", { className: "mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[12px] text-neutral-grey-50", children: mobileFooterMetaParts.map((part, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx("span", { className: "text-neutral-grey-15", children: "\u00B7" })), part] }, `footer-${i}`))) }))] }), isCompactBatches && (_jsxs("div", { className: "mt-2 flex min-w-0 items-center gap-2", children: [totalJobs > 0 && (_jsx("div", { className: "min-w-0 flex-1", children: _jsx(BatchProgressIndicator, { value: progressPct, completedJobs: completedJobs, totalJobs: totalJobs, errorJobs: errorJobs, tone: progressTone, testId: `batch-progress-${b.id}`, variant: "bar" }) })), hasBatchActions && (_jsx("div", { className: "flex shrink-0 items-center gap-1.5", children: batchActions }))] })), !isCompactBatches && hasBatchActions && (_jsx("div", { className: "flex shrink-0 items-center gap-2 self-center", children: batchActions }))] })] }) }), isExpanded && (_jsxs("div", { className: "border-t border-border-default/50", children: [_jsx(CustomPromptPanel, { prompts: parseCustomPrompts(info?.metadata) }), _jsx(BatchItemList, { batchId: b.id, batchProvider: info?.provider, jobs: jobs, isLoading: isLoadingThis, expandedItems: expandedItems, languages: sitecoreLanguages, currentLanguage: editContext?.item?.language, itemFilter: filters.itemIdOrPath.trim(), itemIncludeSubitems: filters.itemIncludeSubitems, statusFilter: filters.status, batchIsTerminal: isTerminalBatchStatus(info?.status), batchStartedAtUtc: info?.startedAtUtc ?? info?.createdAtUtc, abortingJobIds: abortingJobIds, retryingKeys: retryingKeys, isMobile: isMobile, onToggleItem: toggleItem, onOpenItem: openItemInEditor, onAbortJob: handleAbortJob, onRetryJobs: handleRetryJobs })] }))] }, b.id));
860
+ }) })] }, 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-highlight-100" }), "Loading more..."] })) : (_jsxs(_Fragment, { children: [_jsx(ChevronDownIcon, { className: "h-4 w-4" }), "Load More Batches"] })) }) }))] })) }) })] }));
861
+ }
862
+ export const TRANSLATION_BATCH_FILTERS_SIDEBAR_ID = "translation-batch-filters";
863
+ export function TranslationBatchFiltersSidebar() {
864
+ const editContext = useEditContext();
865
+ const { filters, itemIdOrPathInput } = useTranslationBatchFilterSnapshot();
866
+ const [providers, setProviders] = useState([]);
867
+ const [sitecoreLanguages, setSitecoreLanguages] = useState([]);
868
+ const [itemPickerOpen, setItemPickerOpen] = useState(false);
869
+ const currentUserName = editContext?.user?.name || "all";
870
+ const isLimitedPreviewUser = editContext?.user?.isLimitedPreviewUser ?? false;
871
+ useEffect(() => {
872
+ ensureDefaultTranslationBatchUser(currentUserName);
873
+ }, [currentUserName]);
874
+ useEffect(() => {
875
+ if (isLimitedPreviewUser &&
876
+ currentUserName !== "all" &&
877
+ filters.user !== currentUserName) {
878
+ setTranslationBatchFilters((prev) => ({
879
+ ...prev,
880
+ user: currentUserName,
881
+ }));
882
+ }
883
+ }, [isLimitedPreviewUser, currentUserName, filters.user]);
884
+ const searchTranslationUsers = useCallback((query, limit) => searchBackendUsers(query, limit), []);
885
+ const debouncedSetItemFilter = useDebouncedCallback((value) => setTranslationBatchFilters((prev) => ({
886
+ ...prev,
887
+ itemIdOrPath: value,
888
+ })), 400);
889
+ useEffect(() => {
890
+ const loadProviders = async () => {
891
+ try {
892
+ const res = await getTranslationProviders();
893
+ const maybeProviders = res?.data ?? res;
894
+ setProviders(Array.isArray(maybeProviders)
895
+ ? maybeProviders
896
+ : []);
897
+ }
898
+ catch (error) {
899
+ console.error("Failed to load translation providers:", error);
900
+ }
901
+ };
902
+ loadProviders();
903
+ }, []);
904
+ useEffect(() => {
905
+ const loadLanguages = async () => {
906
+ try {
907
+ const res = await getLanguages();
908
+ const langs = (res?.data ?? res ?? []);
909
+ setSitecoreLanguages(Array.isArray(langs) ? langs : []);
910
+ }
911
+ catch (error) {
912
+ console.error("Failed to load Sitecore languages:", error);
913
+ }
914
+ };
915
+ loadLanguages();
916
+ }, []);
917
+ const languageOptions = useMemo(() => sitecoreLanguages
918
+ .map((language) => {
919
+ const code = language.languageCode || language.name;
920
+ return code ? { ...language, languageCode: code } : null;
921
+ })
922
+ .filter((language) => language !== null)
923
+ .sort((a, b) => a.languageCode.localeCompare(b.languageCode)), [sitecoreLanguages]);
924
+ const currentItem = editContext?.item;
925
+ const multiItemEnabled = editContext?.configuration?.localization?.multiItem !== false;
926
+ const canStartTranslation = !!editContext && (multiItemEnabled || !!currentItem);
927
+ const handleStartTranslation = useCallback(() => {
928
+ if (!editContext)
929
+ return;
930
+ void openLocalizeItemDialog({
931
+ editContext,
932
+ items: currentItem ? [currentItem] : [],
933
+ });
934
+ }, [currentItem, editContext]);
659
935
  const hasActiveFilters = filters.dateRange !== "last30days" ||
660
936
  filters.status !== "all" ||
661
937
  filters.user !== currentUserName ||
662
938
  filters.provider !== "all" ||
663
939
  filters.targetLanguage !== "all" ||
664
- filters.itemIdOrPath.trim() !== "";
665
- const filterLabelClass = "flex items-center gap-1.5 text-[10px] font-medium tracking-[0.06em] text-neutral-grey-50";
666
- return (_jsxs("div", { className: "flex h-full flex-col min-h-0 bg-background", "data-testid": "translation-batches", children: [_jsx("div", { className: "shrink-0 px-4 pt-4 pb-3 md:px-6", children: _jsxs("div", { className: "flex flex-wrap items-end gap-x-3 gap-y-2", children: [_jsxs("div", { className: "flex min-w-[160px] flex-1 flex-col gap-1", children: [_jsxs("label", { className: filterLabelClass, children: [_jsx(CalendarIcon, { className: "h-3 w-3" }), "Date Range"] }), _jsx(Select, { className: "w-full bg-neutral-grey-5", options: DATE_RANGE_OPTIONS, value: filters.dateRange, onValueChange: (value) => setFilters((prev) => ({
940
+ filters.itemIdOrPath.trim() !== "" ||
941
+ filters.itemIncludeSubitems;
942
+ const resetFilters = useCallback(() => {
943
+ debouncedSetItemFilter.cancel();
944
+ setTranslationBatchItemInput("");
945
+ setTranslationBatchFilters({
946
+ ...DEFAULT_FILTERS,
947
+ user: currentUserName,
948
+ });
949
+ }, [currentUserName, debouncedSetItemFilter]);
950
+ const filterLabelClass = "text-[12px] font-medium text-neutral-grey-50";
951
+ const filterFieldClass = "flex flex-col gap-1";
952
+ return (_jsxs("div", { className: "flex h-full min-h-0 flex-col bg-white", "data-testid": "translation-filters-sidebar", children: [_jsx("div", { className: "border-border-default flex shrink-0 flex-col gap-2 border-b px-3 py-3", children: _jsxs(Button, { type: "button", variant: "outline", size: "lg", title: canStartTranslation
953
+ ? "Start new translation"
954
+ : "Open an item to translate", "data-testid": "translation-sidebar-start-new-translation-button", disabled: !canStartTranslation, onClick: handleStartTranslation, className: "border-border-default hover:border-neutral-grey-100 hover:bg-neutral-grey-5 h-9 w-full justify-center gap-3 rounded-md bg-white px-3 text-sm font-medium shadow-none", children: [_jsx("span", { className: "truncate", children: "Start new translation" }), _jsx(PlusIcon, { className: "size-4", strokeWidth: 1.5 })] }) }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-3 py-4", children: _jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: filterFieldClass, children: [_jsx("label", { className: filterLabelClass, children: "Date Range" }), _jsx(Select, { className: "w-full bg-white", options: DATE_RANGE_OPTIONS, value: filters.dateRange, onValueChange: (value) => setTranslationBatchFilters((prev) => ({
667
955
  ...prev,
668
956
  dateRange: value,
669
- })), placeholder: "Select date range" })] }), _jsxs("div", { className: "flex min-w-[140px] flex-1 flex-col gap-1", children: [_jsxs("label", { className: filterLabelClass, children: [_jsx(StatusIcon, { className: "h-3 w-3" }), "Status"] }), _jsx(Select, { className: "w-full bg-neutral-grey-5", options: STATUS_OPTIONS, value: filters.status, onValueChange: (value) => setFilters((prev) => ({
957
+ })), placeholder: "Select date range" })] }), _jsxs("div", { className: filterFieldClass, children: [_jsx("label", { className: filterLabelClass, children: "Status" }), _jsx(Select, { className: "w-full bg-white", options: STATUS_OPTIONS, value: filters.status, onValueChange: (value) => setTranslationBatchFilters((prev) => ({
670
958
  ...prev,
671
959
  status: value,
672
- })), placeholder: "Select status" })] }), _jsxs("div", { className: "flex min-w-[200px] flex-1 flex-col gap-1", children: [_jsxs("label", { className: filterLabelClass, children: [_jsx(UserFilterIcon, { className: "h-3 w-3" }), "User"] }), _jsx(UserPicker, { value: isLimitedPreviewUser
960
+ })), placeholder: "Select status" })] }), _jsxs("div", { className: filterFieldClass, children: [_jsx("label", { className: filterLabelClass, children: "User" }), _jsx(UserPicker, { value: isLimitedPreviewUser
673
961
  ? currentUserName
674
962
  : filters.user === "all"
675
963
  ? null
676
964
  : filters.user, displayValue: isLimitedPreviewUser || filters.user === currentUserName
677
965
  ? `${editContext?.user?.fullName?.trim() || currentUserName} (You)`
678
- : undefined, onChange: (user) => setFilters((prev) => ({
966
+ : undefined, displayAvatarName: isLimitedPreviewUser || filters.user === currentUserName
967
+ ? editContext?.user?.fullName?.trim() || currentUserName
968
+ : undefined, displayAvatarUrl: isLimitedPreviewUser || filters.user === currentUserName
969
+ ? editContext?.user?.avatarUrl
970
+ : undefined, onChange: (user) => setTranslationBatchFilters((prev) => ({
679
971
  ...prev,
680
972
  user: user?.userName ?? "all",
681
- })), disabled: isLimitedPreviewUser, placeholder: "All Users", buttonClassName: "h-[27px] w-full justify-start rounded-md border bg-neutral-grey-5 text-xs font-medium", allowClear: true, clearLabel: "All Users", searchUsers: searchTranslationUsers })] }), _jsxs("div", { className: "flex min-w-[160px] flex-1 flex-col gap-1", children: [_jsxs("label", { className: filterLabelClass, children: [_jsx(ProviderIcon, { className: "h-3 w-3" }), "Provider"] }), _jsx(Select, { className: "w-full bg-neutral-grey-5", options: [
973
+ })), disabled: isLimitedPreviewUser, placeholder: "All Users", buttonClassName: "h-8 min-h-0 w-full justify-start rounded-[4px] border bg-white px-2 py-0 text-xs font-medium", allowClear: true, clearLabel: "All Users", showIcon: isLimitedPreviewUser || filters.user !== "all", searchUsers: searchTranslationUsers })] }), _jsxs("div", { className: filterFieldClass, children: [_jsx("label", { className: filterLabelClass, children: "Provider" }), _jsx(Select, { className: "w-full bg-white", options: [
682
974
  { value: "all", label: "All Providers" },
683
975
  ...providers.map((p) => ({
684
976
  value: p.name,
685
977
  label: p.displayName || p.name,
686
978
  })),
687
- ], value: filters.provider, onValueChange: (value) => setFilters((prev) => ({ ...prev, provider: value })), placeholder: "Select provider" })] }), _jsxs("div", { className: "flex min-w-[160px] flex-1 flex-col gap-1", children: [_jsxs("label", { className: filterLabelClass, children: [_jsx(GlobeIcon, { className: "h-3 w-3" }), "Target Language"] }), _jsx(Select, { className: "w-full bg-neutral-grey-5", options: [
688
- { value: "all", label: "All Languages" },
689
- ...languageOptions.map((lang) => ({
690
- value: lang,
691
- label: lang,
979
+ ], value: filters.provider, onValueChange: (value) => setTranslationBatchFilters((prev) => ({
980
+ ...prev,
981
+ provider: value,
982
+ })), placeholder: "Select provider" })] }), _jsxs("div", { className: filterFieldClass, children: [_jsx("label", { className: filterLabelClass, children: "Target Language" }), _jsx(Select, { className: "w-full bg-white", options: [
983
+ {
984
+ value: "all",
985
+ label: "All Languages",
986
+ },
987
+ ...languageOptions.map((language) => ({
988
+ value: language.languageCode,
989
+ label: language.languageCode,
990
+ icon: renderTargetLanguageIcon(language),
692
991
  })),
693
- ], value: filters.targetLanguage, onValueChange: (value) => setFilters((prev) => ({ ...prev, targetLanguage: value })), placeholder: "Select language" })] }), _jsxs("div", { className: "flex min-w-[200px] flex-1 flex-col gap-1", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs("label", { className: filterLabelClass, children: [_jsx(ItemIcon, { className: "h-3 w-3" }), "Item ID / Path"] }), _jsxs("label", { className: "flex items-center gap-1 text-[10px] text-neutral-grey-50 cursor-pointer select-none", children: [_jsx("input", { type: "checkbox", checked: filters.itemIncludeSubitems, onChange: (e) => setFilters((prev) => ({
992
+ ], value: filters.targetLanguage, onValueChange: (value) => setTranslationBatchFilters((prev) => ({
993
+ ...prev,
994
+ targetLanguage: value,
995
+ })), placeholder: "Select language" })] }), _jsxs("div", { className: filterFieldClass, children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("label", { className: filterLabelClass, children: "Item ID / Path" }), _jsxs("label", { className: "flex cursor-pointer items-center gap-1 text-[10px] text-neutral-grey-50 select-none", children: [_jsx("input", { type: "checkbox", checked: filters.itemIncludeSubitems, onChange: (e) => setTranslationBatchFilters((prev) => ({
694
996
  ...prev,
695
997
  itemIncludeSubitems: e.target.checked,
696
- })), className: "h-3 w-3 accent-highlight-100 cursor-pointer" }), "Include subitems"] })] }), _jsxs("div", { className: "relative", children: [_jsx(Input, { value: itemIdOrPathInput, onChange: (e) => {
998
+ })), className: "h-3 w-3 cursor-pointer accent-highlight-100" }), "Include subitems"] })] }), _jsxs("div", { className: "relative", children: [_jsx(Input, { value: itemIdOrPathInput, onChange: (e) => {
697
999
  const next = e.target.value;
698
- setItemIdOrPathInput(next);
1000
+ setTranslationBatchItemInput(next);
699
1001
  debouncedSetItemFilter(next);
700
- }, placeholder: "GUID or /sitecore/content/\u2026", className: "h-[27px] w-full bg-neutral-grey-5 text-xs pr-14" }), _jsxs("div", { className: "absolute right-1.5 top-1/2 -translate-y-1/2 flex items-center gap-1", children: [itemIdOrPathInput && (_jsx("button", { type: "button", onClick: () => {
701
- setItemIdOrPathInput("");
1002
+ }, placeholder: "GUID or /sitecore/content/...", className: "h-auto min-h-[34px] w-full bg-white py-2 pr-14 text-xs" }), _jsxs("div", { className: "absolute top-1/2 right-1.5 flex -translate-y-1/2 items-center gap-1", children: [itemIdOrPathInput && (_jsx("button", { type: "button", onClick: () => {
1003
+ setTranslationBatchItemInput("");
702
1004
  debouncedSetItemFilter.cancel();
703
- setFilters((prev) => ({
1005
+ setTranslationBatchFilters((prev) => ({
704
1006
  ...prev,
705
1007
  itemIdOrPath: "",
706
1008
  itemIncludeSubitems: false,
707
1009
  }));
708
- }, className: "text-neutral-grey-50 hover:text-neutral-grey-100 cursor-pointer", "aria-label": "Clear", children: _jsx(XIcon, { className: "h-3.5 w-3.5" }) })), _jsxs(Popover, { open: itemPickerOpen, onOpenChange: setItemPickerOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", className: "text-neutral-grey-50 hover:text-neutral-grey-100 cursor-pointer", "aria-label": "Browse content tree", title: "Browse content tree", children: _jsx(FolderTreeIcon, { className: "h-3.5 w-3.5" }) }) }), _jsxs(PopoverContent, { align: "end", sideOffset: 6, className: "w-[28rem] max-w-[calc(100vw-2rem)] p-0", children: [_jsx("div", { className: "flex items-center justify-between gap-2 border-b border-border-default px-3 py-2 text-[11px] text-neutral-grey-50", children: _jsx("span", { children: "Pick item to filter by" }) }), _jsx("div", { className: "max-h-80 overflow-auto p-2", children: _jsx(ContentTree, { language: "en", selectionMode: "single", rootItemIds: ["0de95ae4-41ab-4d01-9eb0-67441b7c2450"], onSelectionChange: (items) => {
1010
+ }, className: "cursor-pointer text-neutral-grey-50 hover:text-neutral-grey-100", "aria-label": "Clear", children: _jsx(XIcon, { className: "h-3.5 w-3.5" }) })), _jsxs(Popover, { open: itemPickerOpen, onOpenChange: setItemPickerOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", className: "cursor-pointer text-neutral-grey-50 hover:text-neutral-grey-100", "aria-label": "Browse content tree", title: "Browse content tree", children: _jsx(FolderTreeIcon, { className: "h-3.5 w-3.5" }) }) }), _jsxs(PopoverContent, { align: "end", sideOffset: 6, className: "w-[28rem] max-w-[calc(100vw-2rem)] p-0", children: [_jsx("div", { className: "flex items-center justify-between gap-2 border-b border-border-default px-3 py-2 text-[11px] text-neutral-grey-50", children: _jsx("span", { children: "Pick item to filter by" }) }), _jsx("div", { className: "max-h-80 overflow-auto p-2", children: _jsx(ContentTree, { language: "en", selectionMode: "single", rootItemIds: ["0de95ae4-41ab-4d01-9eb0-67441b7c2450"], initialExpandedKeys: [
1011
+ "0de95ae4-41ab-4d01-9eb0-67441b7c2450",
1012
+ ], onSelectionChange: (items) => {
709
1013
  const picked = items?.[0];
710
1014
  if (!picked)
711
1015
  return;
712
1016
  const applyValue = (value) => {
713
- setItemIdOrPathInput(value);
1017
+ setTranslationBatchItemInput(value);
714
1018
  debouncedSetItemFilter.cancel();
715
- setFilters((prev) => ({
1019
+ setTranslationBatchFilters((prev) => ({
716
1020
  ...prev,
717
1021
  itemIdOrPath: value,
718
1022
  }));
@@ -722,134 +1026,19 @@ export function TranslationBatches() {
722
1026
  applyValue(picked.path);
723
1027
  return;
724
1028
  }
725
- // Fall back: resolve the item's path via a stub fetch
726
- // so the input shows a human-readable path instead of a GUID.
727
1029
  fetchItemStubs([
728
- { id: picked.id, language: "en", version: 1 },
1030
+ {
1031
+ id: picked.id,
1032
+ language: "en",
1033
+ version: 1,
1034
+ },
729
1035
  ])
730
1036
  .then((stubs) => {
731
1037
  const stubPath = stubs?.[0]?.path;
732
1038
  applyValue(stubPath || picked.id);
733
1039
  })
734
1040
  .catch(() => applyValue(picked.id));
735
- } }) })] })] })] })] })] })] }) }), _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-neutral-grey-50", children: [_jsx(LoaderIcon, { className: "h-5 w-5 animate-spin text-highlight-100" }), "Loading recent translations..."] }) })) : groupedBatches.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-32", children: _jsxs("div", { className: "text-center text-neutral-grey-50", children: [_jsx(LanguagesIcon, { className: "h-8 w-8 block mx-auto mb-2 text-neutral-grey-15" }), _jsx("p", { className: "font-medium text-neutral-grey-100", children: "No translations found" }), _jsx("p", { className: "text-sm mt-1", children: (() => {
736
- const hasActiveFilters = filters.dateRange !== "last30days" ||
737
- filters.status !== "all" ||
738
- filters.user !== currentUserName ||
739
- filters.provider !== "all" ||
740
- filters.targetLanguage !== "all" ||
741
- filters.itemIdOrPath.trim() !== "";
742
- return hasActiveFilters
743
- ? "Try adjusting your filters or start a translation to see it appear here"
744
- : "Start a translation to see it appear here";
745
- })() })] }) })) : (_jsxs("div", { className: "space-y-10", children: [groupedBatches.map((dateGroup) => (_jsxs("div", { children: [_jsxs("div", { className: "flex items-baseline gap-2 mb-2", children: [_jsx("h2", { className: "text-[11px] font-medium tracking-[0.08em] text-neutral-grey-50", children: dateGroup.label }), _jsx("span", { className: "flex-1 ml-2 border-t border-border-default" })] }), _jsx("div", { className: "divide-y divide-border-default/70", children: dateGroup.batches.map((b) => {
746
- const batchDate = new Date(b.timestamp);
747
- const now = new Date();
748
- const isToday = batchDate.toDateString() === now.toDateString();
749
- const timeDisplay = isToday
750
- ? batchDate.toLocaleTimeString()
751
- : batchDate.toLocaleDateString() +
752
- " " +
753
- batchDate.toLocaleTimeString();
754
- const info = b.info;
755
- const totalJobs = info?.expectedJobs ?? 0;
756
- const itemsCount = typeof info?.itemsCount === "number"
757
- ? info.itemsCount
758
- : undefined;
759
- const languagesCsv = info?.languages;
760
- const languages = languagesCsv
761
- ? languagesCsv.split(",")
762
- : [];
763
- const completedJobs = info?.completedJobs ?? 0;
764
- const errorJobs = info?.errorJobs ?? 0;
765
- const anyError = errorJobs > 0;
766
- const anyInProgress = isActiveBatchStatus(info?.status);
767
- const anyAborted = info?.status === "Aborted";
768
- const progressPct = totalJobs > 0
769
- ? Math.min(100, Math.round((completedJobs / totalJobs) * 100))
770
- : 0;
771
- const showProgress = totalJobs > 0 && (anyInProgress || anyError);
772
- const statusDotClass = anyError
773
- ? "bg-feedback-red"
774
- : anyInProgress
775
- ? "bg-highlight-100"
776
- : anyAborted
777
- ? "bg-feedback-orange"
778
- : "bg-feedback-green/70";
779
- const metaParts = [];
780
- metaParts.push(_jsxs("span", { className: "text-neutral-grey-100", children: [totalJobs, " translation", totalJobs !== 1 ? "s" : ""] }, "trans"));
781
- if (typeof itemsCount === "number") {
782
- metaParts.push(_jsxs("span", { children: [itemsCount, " item", itemsCount !== 1 ? "s" : ""] }, "items"));
783
- }
784
- if (languages.length > 0) {
785
- metaParts.push(_jsxs("span", { title: languages.join(", "), children: [languages.length, " language", languages.length !== 1 ? "s" : ""] }, "langs"));
786
- }
787
- if (info?.provider) {
788
- metaParts.push(_jsx("span", { children: info.provider }, "prov"));
789
- }
790
- if (info?.totalCost != null) {
791
- metaParts.push(_jsxs("span", { children: ["Cost:", " ", _jsx("span", { className: "font-medium text-neutral-grey-100", children: formatUsdCost(info.totalCost) })] }, "cost"));
792
- }
793
- if (info?.initiatedByUser) {
794
- const resolved = userDisplayNames[info.initiatedByUser];
795
- metaParts.push(_jsx("span", { title: info.initiatedByUser, children: resolved || info.initiatedByUser }, "user"));
796
- }
797
- const isExpanded = expandedBatchId === b.id;
798
- const jobs = batchJobs[b.id];
799
- const isLoadingThis = loadingJobs.has(b.id);
800
- const canRetry = anyError;
801
- const canAbort = isActiveBatchStatus(info?.status);
802
- const canDelete = isTerminalBatchStatus(info?.status);
803
- const isRetryingBatch = retryingKeys.has(`batch:${b.id}`);
804
- const isAborting = abortingBatchIds.has(b.id);
805
- const isDeleting = deletingBatchIds.has(b.id);
806
- // Stable test-id status used by Playwright assertions.
807
- // Matches the visual status dot/spinner logic above.
808
- const testStatus = anyError
809
- ? "error"
810
- : anyInProgress
811
- ? "in-progress"
812
- : anyAborted
813
- ? "aborted"
814
- : "completed";
815
- return (_jsxs("div", { "data-testid": `batch-row-${b.id}`, "data-batch-status": testStatus, children: [_jsx("div", { role: "button", tabIndex: 0, className: "group w-full text-left px-3 md:px-4 py-3 hover:bg-neutral-grey-5 transition-colors cursor-pointer", onClick: () => toggleBatch(b.id), onKeyDown: (e) => {
816
- if (e.key === "Enter" || e.key === " ") {
817
- e.preventDefault();
818
- toggleBatch(b.id);
819
- }
820
- }, children: _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center gap-2.5", children: [anyInProgress ? (_jsx(LoaderIcon, { className: "h-3 w-3 shrink-0 animate-spin text-highlight-100" })) : (_jsx("span", { className: `h-1.5 w-1.5 shrink-0 rounded-full ${statusDotClass}`, "aria-hidden": "true" })), _jsx("span", { className: "text-neutral-grey-100 truncate", title: timeDisplay, children: (() => {
821
- let parsedName;
822
- if (info?.metadata) {
823
- try {
824
- const m = typeof info.metadata === "string"
825
- ? JSON.parse(info.metadata)
826
- : info.metadata;
827
- const candidate = m?.name;
828
- if (typeof candidate === "string" &&
829
- candidate.trim()) {
830
- parsedName = candidate.trim();
831
- }
832
- }
833
- catch {
834
- // Metadata isn't JSON — fall through to timestamp.
835
- }
836
- }
837
- return (parsedName ??
838
- `Translation Batch · ${timeDisplay}`);
839
- })() }), showProgress && (_jsxs("span", { className: `inline-flex items-center gap-1 text-[11px] tabular-nums ${anyError ? "text-feedback-red" : "text-highlight-100"}`, "data-testid": `batch-progress-${b.id}`, children: [completedJobs, "/", totalJobs, anyError && (_jsxs("span", { className: "text-feedback-red", children: ["\u00B7 ", errorJobs, " error", errorJobs !== 1 ? "s" : ""] }))] }))] }), _jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[12px] text-neutral-grey-50 pl-4", children: [metaParts.map((part, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx("span", { className: "text-neutral-grey-15", children: "\u00B7" })), part] }, i))), info?.lastUpdatedUtc && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-neutral-grey-15", children: "\u00B7" }), _jsx("span", { children: new Date(info.lastUpdatedUtc).toLocaleString() })] }))] })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [canRetry && (_jsx(SimpleIconButton, { onClick: (event) => {
840
- event.stopPropagation();
841
- void handleRetryBatch(info);
842
- }, icon: _jsx(RetryIcon, { className: `h-3.5 w-3.5 ${isRetryingBatch ? "animate-spin" : ""}`, strokeWidth: 1.5 }), label: "Retry failed translations", disabled: isRetryingBatch ||
843
- isLoadingThis ||
844
- isAborting ||
845
- isDeleting, className: "p-0! text-feedback-red hover:text-feedback-red" })), canAbort && (_jsx(SimpleIconButton, { onClick: (event) => {
846
- event.stopPropagation();
847
- handleAbortBatch(b.id);
848
- }, icon: _jsx(AbortIcon, { className: `h-3.5 w-3.5 ${isAborting ? "animate-spin" : ""}`, strokeWidth: 1.5 }), label: "Abort translation batch", disabled: isAborting || isDeleting, className: "p-0! text-feedback-orange hover:text-feedback-orange" })), canDelete && (_jsx(SimpleIconButton, { onClick: (event) => {
849
- event.stopPropagation();
850
- handleDeleteBatch(b.id);
851
- }, icon: _jsx(DeleteIcon, { size: "md" }), label: "Delete translation history", disabled: isDeleting || isAborting, className: "p-0! text-neutral-grey-50 hover:bg-neutral-grey-5 hover:text-neutral-grey-50" })), _jsx("div", { className: `text-neutral-grey-15 transition-transform ${isExpanded ? "rotate-180" : ""} group-hover:text-neutral-grey-50`, children: _jsx(ChevronDownIcon, { className: "h-4 w-4" }) })] })] }) }), isExpanded && (_jsxs(_Fragment, { children: [_jsx(CustomPromptPanel, { prompts: parseCustomPrompts(info?.metadata) }), _jsx(BatchItemList, { batchId: b.id, batchProvider: info?.provider, jobs: jobs, isLoading: isLoadingThis, expandedItems: expandedItems, languages: sitecoreLanguages, itemFilter: filters.itemIdOrPath.trim(), itemIncludeSubitems: filters.itemIncludeSubitems, statusFilter: filters.status, batchIsTerminal: isTerminalBatchStatus(info?.status), batchStartedAtUtc: info?.startedAtUtc ?? info?.createdAtUtc, retryingKeys: retryingKeys, onToggleItem: toggleItem, onOpenItem: openItemInEditor, onRetryJobs: handleRetryJobs })] }))] }, b.id));
852
- }) })] }, 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-highlight-100" }), "Loading more..."] })) : (_jsxs(_Fragment, { children: [_jsx(ChevronDownIcon, { className: "h-4 w-4" }), "Load More Batches"] })) }) }))] })) })] }));
1041
+ } }) })] })] })] })] })] }), _jsx("div", { className: "flex justify-start pt-1", children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: resetFilters, disabled: !hasActiveFilters, children: [_jsx(XIcon, { className: "h-3.5 w-3.5" }), "Reset Filters"] }) })] }) })] }));
853
1042
  }
854
1043
  function isLikelyGuid(value) {
855
1044
  return /^[{(]?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[)}]?$/i.test(value.trim());
@@ -932,6 +1121,45 @@ function formatUsdCost(value) {
932
1121
  maximumFractionDigits: value > 0 && value < 0.01 ? 6 : 2,
933
1122
  }).format(value);
934
1123
  }
1124
+ function BatchProgressIndicator({ value, completedJobs, totalJobs, errorJobs, tone, testId, variant = "circle", }) {
1125
+ const percent = Math.max(0, Math.min(100, Math.round(value)));
1126
+ const radius = 17;
1127
+ const circumference = 2 * Math.PI * radius;
1128
+ const dashOffset = circumference - (percent / 100) * circumference;
1129
+ const hasErrors = errorJobs > 0;
1130
+ const toneClass = tone === "complete"
1131
+ ? "text-feedback-green"
1132
+ : tone === "error"
1133
+ ? "text-feedback-red"
1134
+ : tone === "aborted"
1135
+ ? "text-feedback-orange"
1136
+ : "text-neutral-grey-50";
1137
+ const toneColor = tone === "complete"
1138
+ ? "var(--color-feedback-green)"
1139
+ : tone === "error"
1140
+ ? "var(--color-feedback-red)"
1141
+ : tone === "aborted"
1142
+ ? "var(--color-feedback-orange)"
1143
+ : "var(--color-neutral-grey-50)";
1144
+ const trackColor = tone === "complete"
1145
+ ? "var(--color-feedback-green-light)"
1146
+ : tone === "error"
1147
+ ? "var(--color-feedback-red-light)"
1148
+ : tone === "aborted"
1149
+ ? "var(--color-feedback-orange-light)"
1150
+ : "var(--color-neutral-grey-15)";
1151
+ const label = `${percent}% complete (${completedJobs}/${totalJobs} translations${hasErrors ? `, ${errorJobs} failed` : ""})`;
1152
+ if (variant === "bar") {
1153
+ return (_jsxs(Tooltip, { delayDuration: 250, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { className: "flex min-w-0 items-center gap-2", "aria-label": label, "data-testid": testId, children: [_jsx("div", { className: "h-1.5 min-w-0 flex-1 overflow-hidden rounded-full", style: { backgroundColor: trackColor }, children: _jsx("div", { className: "h-full rounded-full transition-[width]", style: {
1154
+ width: `${percent}%`,
1155
+ backgroundColor: toneColor,
1156
+ } }) }), _jsxs("span", { className: `shrink-0 text-[11px] leading-none font-medium tabular-nums ${toneClass}`, children: [percent, "%"] })] }) }), _jsx(TooltipContent, { side: "top", sideOffset: 6, children: label })] }));
1157
+ }
1158
+ return (_jsxs(Tooltip, { delayDuration: 250, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { className: `relative h-10 w-10 shrink-0 rounded-full ${tone === "complete" ? "bg-feedback-green-light" : ""}`, "aria-label": label, "data-testid": testId, children: [_jsxs("svg", { className: "h-10 w-10 -rotate-90", viewBox: "0 0 40 40", children: [_jsx("circle", { cx: "20", cy: "20", r: radius, fill: "none", stroke: trackColor, strokeWidth: "2" }), _jsx("circle", { cx: "20", cy: "20", r: radius, fill: "none", stroke: toneColor, strokeLinecap: "round", strokeWidth: "2", style: {
1159
+ strokeDasharray: circumference,
1160
+ strokeDashoffset: dashOffset,
1161
+ } })] }), _jsxs("span", { className: `absolute inset-0 flex items-center justify-center text-[10px] leading-none font-medium tabular-nums ${toneClass}`, children: [percent, "%"] })] }) }), _jsx(TooltipContent, { side: "top", sideOffset: 6, children: label })] }));
1162
+ }
935
1163
  function itemStatusDot(jobs) {
936
1164
  const hasError = jobs.some((j) => j.status === "Error");
937
1165
  const hasInProgress = jobs.some((j) => j.status === "In Progress");
@@ -947,6 +1175,30 @@ function itemStatusDot(jobs) {
947
1175
  return { cls: "bg-feedback-orange", label: "Aborted" };
948
1176
  return { cls: "bg-feedback-green/70", label: "Completed" };
949
1177
  }
1178
+ function InlineLanguageJobChips({ batchId, batchProvider, jobs, languageMap, isExpanded, batchStartedAtUtc, abortingJobIds, retryingKeys, singleLine = false, onToggle, onOpenItem, onAbortJob, onRetryJobs, }) {
1179
+ if (jobs.length === 0)
1180
+ return null;
1181
+ const handleCellClick = (event) => {
1182
+ event.stopPropagation();
1183
+ onToggle();
1184
+ };
1185
+ const handleCellKeyDown = (event) => {
1186
+ if (event.key === "Enter" || event.key === " ") {
1187
+ event.preventDefault();
1188
+ event.stopPropagation();
1189
+ onToggle();
1190
+ }
1191
+ };
1192
+ return (_jsx("div", { role: "button", tabIndex: 0, className: `flex min-w-0 cursor-pointer items-start justify-start gap-1.5 ${singleLine
1193
+ ? "flex-nowrap"
1194
+ : isExpanded
1195
+ ? "flex-wrap"
1196
+ : "max-h-7 flex-wrap overflow-hidden"}`, "aria-label": `${jobs.length} target language${jobs.length !== 1 ? "s" : ""}`, "aria-expanded": isExpanded, onClick: handleCellClick, onKeyDown: handleCellKeyDown, children: jobs.map((job, idx) => {
1197
+ const lang = languageMap.get((job.targetLanguage || "").toLowerCase());
1198
+ const retryKey = `job:${job.id ?? `${job.itemId}:${job.sourceLanguage}:${job.targetLanguage}`}`;
1199
+ return (_jsx("span", { className: "shrink-0", onClick: (event) => event.stopPropagation(), children: _jsx(LanguageJobChip, { job: job, language: lang, batchStartedAtUtc: batchStartedAtUtc, onOpen: () => onOpenItem(job.itemId, job.targetLanguage), isAborting: !!job.id && abortingJobIds.has(job.id), onAbort: () => onAbortJob(job), isRetrying: retryingKeys.has(retryKey), onRetry: () => onRetryJobs(batchId, batchProvider, [job], retryKey) }) }, `${job.id ?? idx}-${job.targetLanguage}`));
1200
+ }) }));
1201
+ }
950
1202
  function CustomPromptPanel({ prompts }) {
951
1203
  const [openProvider, setOpenProvider] = React.useState(null);
952
1204
  const [copiedProvider, setCopiedProvider] = React.useState(null);
@@ -971,7 +1223,7 @@ function CustomPromptPanel({ prompts }) {
971
1223
  toast.error("Failed to copy prompt to clipboard");
972
1224
  }
973
1225
  };
974
- return (_jsx("div", { className: "bg-neutral-grey-5/60 border-t border-border-default/50", children: prompts.map((p) => {
1226
+ return (_jsx("div", { className: "border-b border-border-default/50", children: prompts.map((p) => {
975
1227
  const isOpen = openProvider === p.provider;
976
1228
  const justCopied = copiedProvider === p.provider;
977
1229
  return (_jsxs("div", { children: [_jsxs("div", { role: "button", tabIndex: 0, style: { paddingLeft: "2rem" }, className: "group flex items-center gap-2 pr-3 md:pr-4 py-2 text-[12px] text-neutral-grey-50 hover:bg-neutral-grey-5 transition-colors cursor-pointer", onClick: () => setOpenProvider(isOpen ? null : p.provider), onKeyDown: (e) => {
@@ -982,10 +1234,10 @@ function CustomPromptPanel({ prompts }) {
982
1234
  }, children: [_jsx(SparklesIcon, { className: "h-3 w-3 shrink-0 text-highlight-100", strokeWidth: 1.75 }), _jsx("span", { className: "text-neutral-grey-100", children: "Custom prompt" }), _jsx("span", { className: "text-neutral-grey-15", children: "\u00B7" }), _jsx("span", { children: p.provider }), _jsxs("span", { className: "ml-auto flex items-center gap-2", children: [isOpen && (_jsx(SimpleIconButton, { onClick: (event) => {
983
1235
  event.stopPropagation();
984
1236
  void handleCopy(p.provider, p.prompt);
985
- }, icon: justCopied ? (_jsx(CheckIcon, { className: "h-3.5 w-3.5", strokeWidth: 1.5 })) : (_jsx(CopyIcon, { className: "h-3.5 w-3.5", strokeWidth: 1.5 })), label: justCopied ? "Copied" : "Copy prompt", className: "p-0! text-neutral-grey-50 hover:text-neutral-grey-100" })), _jsx(ChevronDownIcon, { className: `h-3.5 w-3.5 text-neutral-grey-15 transition-transform group-hover:text-neutral-grey-50 ${isOpen ? "rotate-180" : ""}` })] })] }), isOpen && (_jsx("div", { style: { paddingLeft: "2.5rem" }, className: "pr-3 md:pr-4 pb-3", children: _jsx("pre", { className: "max-h-64 overflow-y-auto whitespace-pre-wrap break-words rounded border border-border-default/50 bg-white p-2 font-mono text-[12px] text-neutral-grey-100", children: p.prompt }) }))] }, p.provider));
1237
+ }, icon: justCopied ? (_jsx(CheckIcon, { className: "h-3.5 w-3.5", strokeWidth: 1.5 })) : (_jsx(CopyIcon, { className: "h-3.5 w-3.5", strokeWidth: 1.5 })), label: justCopied ? "Copied" : "Copy prompt" })), _jsx(ChevronDownIcon, { className: `h-3.5 w-3.5 text-neutral-grey-100 transition-transform ${isOpen ? "rotate-180" : ""}` })] })] }), isOpen && (_jsx("div", { style: { paddingLeft: "2.5rem" }, className: "pr-3 md:pr-4 pb-3", children: _jsx("pre", { className: "max-h-64 overflow-y-auto whitespace-pre-wrap break-words rounded border border-border-default/50 bg-white p-2 font-mono text-[12px] text-neutral-grey-100", children: p.prompt }) }))] }, p.provider));
986
1238
  }) }));
987
1239
  }
988
- function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems, languages, itemFilter, itemIncludeSubitems, statusFilter, batchIsTerminal, batchStartedAtUtc, retryingKeys, onToggleItem, onOpenItem, onRetryJobs, }) {
1240
+ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems, languages, currentLanguage, itemFilter, itemIncludeSubitems, statusFilter, batchIsTerminal, batchStartedAtUtc, abortingJobIds, retryingKeys, isMobile = false, onToggleItem, onOpenItem, onAbortJob, onRetryJobs, }) {
989
1241
  const languageMap = React.useMemo(() => {
990
1242
  const map = new Map();
991
1243
  for (const l of languages) {
@@ -1066,6 +1318,53 @@ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems,
1066
1318
  return [];
1067
1319
  return filteredJobs.filter((j) => matchingItemIds.has((j.itemId || "").toString().toLowerCase()));
1068
1320
  }, [filteredJobs, statusFilter]);
1321
+ const [stackLanguageChips, setStackLanguageChips] = React.useState(isMobile);
1322
+ const itemTableContainerRef = React.useRef(null);
1323
+ const itemTableRef = React.useRef(null);
1324
+ const normalTableMinWidthRef = React.useRef(null);
1325
+ React.useEffect(() => {
1326
+ const container = itemTableContainerRef.current;
1327
+ const table = itemTableRef.current;
1328
+ if (!container || !table)
1329
+ return;
1330
+ const updateLayout = () => {
1331
+ const availableWidth = container.clientWidth;
1332
+ if (!availableWidth)
1333
+ return;
1334
+ if (isMobile || availableWidth < 640) {
1335
+ setStackLanguageChips(true);
1336
+ return;
1337
+ }
1338
+ if (stackLanguageChips) {
1339
+ const normalMinWidth = normalTableMinWidthRef.current;
1340
+ if (normalMinWidth && availableWidth >= normalMinWidth) {
1341
+ setStackLanguageChips(false);
1342
+ }
1343
+ return;
1344
+ }
1345
+ const requiredWidth = table.scrollWidth;
1346
+ normalTableMinWidthRef.current = requiredWidth;
1347
+ if (requiredWidth > availableWidth + 1) {
1348
+ setStackLanguageChips(true);
1349
+ }
1350
+ };
1351
+ updateLayout();
1352
+ const animationFrame = window.requestAnimationFrame(updateLayout);
1353
+ const timeout = window.setTimeout(updateLayout, 150);
1354
+ let observer;
1355
+ if (typeof ResizeObserver !== "undefined") {
1356
+ observer = new ResizeObserver(updateLayout);
1357
+ observer.observe(container);
1358
+ observer.observe(table);
1359
+ }
1360
+ window.addEventListener("resize", updateLayout);
1361
+ return () => {
1362
+ window.cancelAnimationFrame(animationFrame);
1363
+ window.clearTimeout(timeout);
1364
+ observer?.disconnect();
1365
+ window.removeEventListener("resize", updateLayout);
1366
+ };
1367
+ }, [isMobile, stackLanguageChips, statusFilteredJobs]);
1069
1368
  if (isLoading && !jobs) {
1070
1369
  return (_jsxs("div", { className: "px-3 md:px-4 py-4 pl-8 text-[12px] text-neutral-grey-50", children: [_jsx(LoaderIcon, { className: "inline h-3.5 w-3.5 mr-1.5 animate-spin text-highlight-100" }), "Loading items\u2026"] }));
1071
1370
  }
@@ -1083,45 +1382,55 @@ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems,
1083
1382
  byItem.set(key, []);
1084
1383
  byItem.get(key).push(job);
1085
1384
  }
1086
- return (_jsx("div", { className: "bg-neutral-grey-5/60 divide-y divide-border-default/50 border-t border-border-default/50", children: Array.from(byItem.entries()).map(([itemId, itemJobs]) => {
1087
- const isItemExpanded = expandedItems.has(itemId);
1088
- const itemAnyInProgress = itemJobs.some((j) => j.status === "In Progress");
1089
- const itemAnyPending = itemJobs.some((j) => j.status === "Pending");
1090
- const itemIsQueued = !itemAnyInProgress && itemAnyPending;
1091
- const status = itemStatusDot(itemJobs);
1092
- const firstJob = itemJobs[0];
1093
- const itemPath = firstJob?.itemPath ?? null;
1094
- const serverName = firstJob?.itemName ?? null;
1095
- const metadataName = parseItemName(firstJob?.metadata, "");
1096
- const displayName = metadataName || serverName || itemId;
1097
- const langs = itemJobs.map((j) => j.targetLanguage).filter(Boolean);
1098
- const uniqueLangs = Array.from(new Set(langs));
1099
- const titleAttr = `${itemPath ?? ""}\n${itemId}`.trim();
1100
- const itemErrorJobs = itemJobs.filter(isRetriableJob);
1101
- const itemRetryKey = `item:${batchId}:${itemId}`;
1102
- const isRetryingItem = retryingKeys.has(itemRetryKey);
1103
- return (_jsxs("div", { children: [_jsx("div", { role: "button", tabIndex: 0, style: { paddingLeft: "2.5rem" }, className: "group w-full pr-3 md:pr-4 py-2 hover:bg-neutral-grey-5 transition-colors cursor-pointer text-left", onClick: () => onToggleItem(itemId), onKeyDown: (e) => {
1104
- if (e.key === "Enter" || e.key === " ") {
1105
- e.preventDefault();
1106
- onToggleItem(itemId);
1107
- }
1108
- }, children: _jsxs("div", { className: "flex items-start gap-2.5 min-w-0", children: [_jsx("span", { className: "mt-1 shrink-0", children: itemAnyInProgress ? (_jsx(LoaderIcon, { className: "h-3 w-3 animate-spin text-highlight-100" })) : itemIsQueued ? (_jsx(QueuedIcon, { className: "h-3 w-3 text-neutral-grey-50", strokeWidth: 1.75, "aria-label": "Queued" })) : (_jsx("span", { className: `block h-1.5 w-1.5 rounded-full ${status.cls}`, "aria-hidden": "true" })) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [_jsx("span", { className: "text-[13px] text-neutral-grey-100 truncate", title: titleAttr, children: displayName }), _jsxs("span", { className: "text-[11px] text-neutral-grey-50 shrink-0", children: ["\u00B7 ", uniqueLangs.length, " language", uniqueLangs.length !== 1 ? "s" : ""] })] }), itemPath && (_jsx("div", { className: "text-[11px] text-neutral-grey-50 truncate font-mono", title: titleAttr, children: itemPath }))] }), itemErrorJobs.length > 0 && (_jsx(SimpleIconButton, { onClick: (event) => {
1109
- event.stopPropagation();
1110
- void onRetryJobs(batchId, batchProvider, itemErrorJobs, itemRetryKey);
1111
- }, icon: _jsx(RetryIcon, { className: `h-3.5 w-3.5 ${isRetryingItem ? "animate-spin" : ""}`, strokeWidth: 1.5 }), label: "Retry failed translations for this item", disabled: isRetryingItem, className: "mt-0.5 p-0! text-feedback-red hover:text-feedback-red" })), _jsx("span", { className: "mt-1 shrink-0 text-neutral-grey-15 group-hover:text-neutral-grey-50", children: _jsx(ChevronDownIcon, { className: `h-3.5 w-3.5 transition-transform ${isItemExpanded ? "rotate-180" : ""}` }) })] }) }), isItemExpanded && (_jsx("div", { style: { paddingLeft: "4rem" }, className: "pr-3 md:pr-4 pb-3 pt-1 flex flex-wrap gap-1.5", children: itemJobs.map((job, idx) => {
1112
- const lang = languageMap.get((job.targetLanguage || "").toLowerCase());
1113
- const retryKey = `job:${job.id ?? `${job.itemId}:${job.sourceLanguage}:${job.targetLanguage}`}`;
1114
- return (_jsx(LanguageJobChip, { job: job, language: lang, batchStartedAtUtc: batchStartedAtUtc, onOpen: () => onOpenItem(job.itemId, job.targetLanguage), isRetrying: retryingKeys.has(retryKey), onRetry: () => onRetryJobs(batchId, batchProvider, [job], retryKey) }, `${job.id ?? idx}-${job.targetLanguage}`));
1115
- }) }))] }, itemId));
1116
- }) }));
1385
+ return (_jsx("div", { ref: itemTableContainerRef, className: "w-full overflow-hidden", "data-language-layout": stackLanguageChips ? "stacked" : "columns", children: _jsxs("table", { ref: itemTableRef, className: "w-full border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { children: [!stackLanguageChips && (_jsx("th", { "aria-hidden": "true", className: "px-3 pt-3 pb-1 md:px-4" })), _jsx("th", { scope: "col", className: `pt-3 pb-1 text-left align-top text-[11px] font-medium text-neutral-grey-50 ${stackLanguageChips ? "pr-2 pl-2.5" : "pr-4"}`, children: _jsxs("span", { className: "flex min-w-0 items-center gap-2.5", children: [_jsx("span", { className: "h-1.5 w-1.5 shrink-0", "aria-hidden": "true" }), _jsx("span", { children: "Pages / Items" })] }) }), !stackLanguageChips && (_jsx("th", { scope: "col", className: "pt-3 pb-1 pr-2 text-left align-top text-[11px] font-medium text-neutral-grey-50", children: "Languages" })), !stackLanguageChips && (_jsx("th", { "aria-hidden": "true", className: "pt-3 pr-3 pb-1 md:pr-4" }))] }) }), _jsx("tbody", { children: Array.from(byItem.entries()).map(([itemId, itemJobs], itemIndex) => {
1386
+ const isItemExpanded = expandedItems.has(itemId);
1387
+ const itemAnyInProgress = itemJobs.some((j) => j.status === "In Progress");
1388
+ const itemAnyPending = itemJobs.some((j) => j.status === "Pending");
1389
+ const itemIsQueued = !itemAnyInProgress && itemAnyPending;
1390
+ const status = itemStatusDot(itemJobs);
1391
+ const firstJob = itemJobs[0];
1392
+ const itemPath = firstJob?.itemPath ?? null;
1393
+ const serverName = firstJob?.itemName ?? null;
1394
+ const metadataName = parseItemName(firstJob?.metadata, "");
1395
+ const displayName = serverName || metadataName || itemId;
1396
+ const titleAttr = `${itemPath ?? ""}\n${itemId}`.trim();
1397
+ const itemErrorJobs = itemJobs.filter(isRetriableJob);
1398
+ const itemRetryKey = `item:${batchId}:${itemId}`;
1399
+ const isRetryingItem = retryingKeys.has(itemRetryKey);
1400
+ const rowLanguages = itemJobs
1401
+ .map((job) => job.targetLanguage || job.sourceLanguage)
1402
+ .filter((language) => Boolean(language));
1403
+ const normalizedCurrentLanguage = normalizeLanguageCode(currentLanguage);
1404
+ const rowOpenLanguage = rowLanguages.find((language) => normalizeLanguageCode(language) === normalizedCurrentLanguage) ?? rowLanguages[0];
1405
+ const languageChips = (_jsx(InlineLanguageJobChips, { batchId: batchId, batchProvider: batchProvider, jobs: itemJobs, languageMap: languageMap, isExpanded: isItemExpanded, batchStartedAtUtc: batchStartedAtUtc, abortingJobIds: abortingJobIds, retryingKeys: retryingKeys, singleLine: !stackLanguageChips, onToggle: () => onToggleItem(itemId), onOpenItem: onOpenItem, onAbortJob: onAbortJob, onRetryJobs: onRetryJobs }));
1406
+ const showRetryAction = itemErrorJobs.length > 0;
1407
+ const retryAction = showRetryAction ? (_jsx("div", { className: "flex shrink-0 items-center justify-end", children: _jsx(SimpleIconButton, { onClick: (event) => {
1408
+ event.stopPropagation();
1409
+ if (itemErrorJobs.length === 0)
1410
+ return;
1411
+ void onRetryJobs(batchId, batchProvider, itemErrorJobs, itemRetryKey);
1412
+ }, icon: _jsx(RetryIcon, { className: `h-3.5 w-3.5 ${isRetryingItem ? "animate-spin" : ""}`, strokeWidth: 1.5 }), label: "Retry failed translations for this item", disabled: itemErrorJobs.length === 0 || isRetryingItem }) })) : null;
1413
+ const itemContent = (_jsxs("div", { className: `flex min-w-0 items-start ${stackLanguageChips ? "gap-2" : "gap-2.5"}`, children: [_jsx("span", { className: "mt-1 shrink-0", children: itemAnyInProgress ? (_jsx(LoaderIcon, { className: "h-3 w-3 animate-spin text-highlight-100" })) : itemIsQueued ? (_jsx(QueuedIcon, { className: "h-3 w-3 text-neutral-grey-50", strokeWidth: 1.75, "aria-label": "Queued" })) : (_jsx("span", { className: `block h-1.5 w-1.5 rounded-full ${status.cls}`, "aria-hidden": "true" })) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("span", { className: `block min-w-0 text-[13px] text-neutral-grey-100 ${stackLanguageChips ? "break-words" : "truncate"}`, style: stackLanguageChips
1414
+ ? { overflowWrap: "anywhere" }
1415
+ : undefined, title: titleAttr, children: displayName }), itemPath && (_jsx("div", { className: "whitespace-normal break-words font-mono text-[11px] text-neutral-grey-50", style: { overflowWrap: "anywhere" }, title: titleAttr, children: itemPath })), stackLanguageChips && (_jsx("div", { className: "mt-2", children: languageChips })), stackLanguageChips && retryAction && (_jsx("div", { className: "mt-2 flex justify-start", children: retryAction }))] })] }));
1416
+ return (_jsxs("tr", { role: "button", tabIndex: 0, className: `group cursor-pointer transition-colors hover:bg-neutral-grey-5 ${itemIndex > 0 ? "border-t border-border-default/50" : ""}`, onClick: () => {
1417
+ void onOpenItem(itemId, rowOpenLanguage);
1418
+ }, onKeyDown: (e) => {
1419
+ if (e.key === "Enter" || e.key === " ") {
1420
+ e.preventDefault();
1421
+ void onOpenItem(itemId, rowOpenLanguage);
1422
+ }
1423
+ }, children: [!stackLanguageChips && (_jsx("td", { "aria-hidden": "true", className: "px-3 py-2 md:px-4" })), _jsx("td", { className: `py-2 align-top ${stackLanguageChips ? "pr-2 pl-2.5" : "pr-4"}`, children: itemContent }), !stackLanguageChips && (_jsx("td", { className: "py-2 pr-2 align-top", children: languageChips })), !stackLanguageChips && (_jsx("td", { className: "py-2 pr-3 align-top md:pr-4", children: retryAction }))] }, itemId));
1424
+ }) })] }) }));
1117
1425
  }
1118
- function LanguageJobChip({ job, language, batchStartedAtUtc, onOpen, isRetrying = false, onRetry, }) {
1426
+ function LanguageJobChip({ job, language, batchStartedAtUtc, onOpen, isAborting = false, onAbort, isRetrying = false, onRetry, }) {
1119
1427
  const [open, setOpen] = React.useState(false);
1120
1428
  const jobStatus = job.status;
1121
1429
  const jobInProgress = jobStatus === "In Progress";
1122
1430
  const jobPending = jobStatus === "Pending";
1123
1431
  const jobError = jobStatus === "Error";
1124
1432
  const jobAborted = jobStatus === "Aborted";
1433
+ const canAbort = !!job.id && !!job.batchId && !!onAbort && (jobInProgress || jobPending);
1125
1434
  const langName = language?.name || job.targetLanguage;
1126
1435
  const chipCls = jobError
1127
1436
  ? "border-feedback-red bg-feedback-red-light text-feedback-red hover:bg-feedback-red-light"
@@ -1131,16 +1440,16 @@ function LanguageJobChip({ job, language, batchStartedAtUtc, onOpen, isRetrying
1131
1440
  ? "border-highlight-100/30 bg-highlight-10 text-highlight-100 hover:brightness-95"
1132
1441
  : jobPending
1133
1442
  ? "border-border-default bg-neutral-grey-5 text-neutral-grey-50 hover:bg-neutral-grey-5"
1134
- : "border-border-default bg-background text-neutral-grey-100 hover:bg-neutral-grey-5";
1443
+ : "border-feedback-green bg-feedback-green-light text-feedback-green hover:border-feedback-green hover:brightness-95";
1135
1444
  const statusBadgeCls = jobError
1136
- ? "bg-feedback-red-light text-feedback-red ring-1 ring-inset ring-feedback-red"
1445
+ ? "bg-feedback-red-light text-feedback-red"
1137
1446
  : jobAborted
1138
- ? "bg-feedback-orange-light text-feedback-orange ring-1 ring-inset ring-feedback-orange"
1447
+ ? "bg-feedback-orange-light text-feedback-orange"
1139
1448
  : jobInProgress
1140
- ? "bg-highlight-10 text-highlight-100 ring-1 ring-inset ring-highlight-100/20"
1449
+ ? "bg-highlight-10 text-highlight-100"
1141
1450
  : jobPending
1142
- ? "bg-neutral-grey-5 text-neutral-grey-50 ring-1 ring-inset ring-border-default"
1143
- : "bg-feedback-green-light text-feedback-green ring-1 ring-inset ring-feedback-green";
1451
+ ? "bg-neutral-grey-5 text-neutral-grey-50"
1452
+ : "bg-feedback-green-light text-feedback-green";
1144
1453
  const timestamp = job.timestamp ? new Date(job.timestamp) : null;
1145
1454
  const timestampValid = timestamp && !isNaN(timestamp.getTime());
1146
1455
  const claimedAt = job.claimedAtUtc ? new Date(job.claimedAtUtc) : null;
@@ -1198,15 +1507,19 @@ function LanguageJobChip({ job, language, batchStartedAtUtc, onOpen, isRetrying
1198
1507
  const emptyFieldText = statistics && statistics.emptyFieldCount > 0
1199
1508
  ? `${formatCount(statistics.emptyFieldCount)} empty`
1200
1509
  : null;
1201
- return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: `inline-flex items-center gap-1.5 rounded-md border badge-pad-sm text-[12px] transition-colors cursor-pointer ${chipCls}`, children: [language?.icon ? (_jsx("img", { src: language.icon, alt: langName, className: "h-3.5 shrink-0" })) : null, _jsx("span", { className: "font-mono tracking-tight", children: job.targetLanguage }), jobInProgress && (_jsx(LoaderIcon, { className: "h-3 w-3 shrink-0 animate-spin" })), jobPending && (_jsx(QueuedIcon, { className: "h-3 w-3 shrink-0", strokeWidth: 1.75, "aria-label": "Queued" })), jobError && _jsx(AlertCircleIcon, { className: "h-3 w-3 shrink-0" }), !jobInProgress && !jobError && !jobPending && !jobAborted && (_jsx(CheckIcon, { className: "h-3 w-3 shrink-0 text-feedback-green" }))] }) }), _jsxs(PopoverContent, { align: "start", sideOffset: 6, className: "w-[22rem] max-w-[calc(100vw-2rem)] p-0", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-border-default px-3 py-2", children: [language?.icon ? (_jsx("img", { src: language.icon, alt: langName, className: "h-4 shrink-0" })) : null, _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-[13px] font-medium text-neutral-grey-100 truncate", children: langName }), _jsxs("div", { className: "text-[11px] text-neutral-grey-50 font-mono truncate", children: [job.targetLanguage, job.sourceLanguage ? ` ← ${job.sourceLanguage}` : ""] })] }), _jsxs(Badge, { size: "sm", className: statusBadgeCls, children: [jobInProgress && (_jsx(LoaderIcon, { className: "h-2.5 w-2.5 animate-spin" })), jobStatus || "Unknown"] })] }), _jsxs("dl", { className: "divide-y divide-border-default/60", children: [claimedAtValid && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Claimed" }), _jsx("dd", { className: "text-[12px] text-neutral-grey-100", children: claimedAt.toLocaleString() })] })), timestampValid && (jobInProgress || jobPending) && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Updated" }), _jsx("dd", { className: "text-[12px] text-neutral-grey-100", children: timestamp.toLocaleString() })] })), durationMs != null && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: jobInProgress || jobPending ? "Running" : "Duration" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", title: durationApproximated
1510
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: `group inline-flex items-center gap-1.5 rounded border badge-pad-sm text-[12px] transition-colors cursor-pointer ${chipCls}`, children: [language?.icon ? (_jsx("img", { src: language.icon, alt: langName, className: "h-3.5 shrink-0" })) : null, _jsx("span", { className: "font-mono tracking-tight", children: job.targetLanguage }), jobInProgress && (_jsx(LoaderIcon, { className: "h-3 w-3 shrink-0 animate-spin" })), jobPending && (_jsx(QueuedIcon, { className: "h-3 w-3 shrink-0", strokeWidth: 1.75, "aria-label": "Queued" })), jobError && _jsx(AlertCircleIcon, { className: "h-3 w-3 shrink-0" }), !jobInProgress && !jobError && !jobPending && !jobAborted && (_jsx(CheckIcon, { className: "h-3 w-3 shrink-0 text-feedback-green" }))] }) }), _jsxs(PopoverContent, { align: "start", sideOffset: 6, className: "w-[22rem] max-w-[calc(100vw-2rem)] p-0", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-border-default px-3 py-2", children: [language?.icon ? (_jsx("img", { src: language.icon, alt: langName, className: "h-4 shrink-0" })) : null, _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-[13px] font-medium text-neutral-grey-100 truncate", children: langName }), _jsxs("div", { className: "text-[11px] text-neutral-grey-50 font-mono truncate", children: [job.targetLanguage, job.sourceLanguage ? ` ← ${job.sourceLanguage}` : ""] })] }), _jsxs(Badge, { size: "sm", className: statusBadgeCls, children: [jobInProgress && (_jsx(LoaderIcon, { className: "h-2.5 w-2.5 animate-spin" })), jobStatus || "Unknown"] })] }), _jsxs("dl", { className: "divide-y divide-border-default/60", children: [claimedAtValid && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Claimed" }), _jsx("dd", { className: "text-[12px] text-neutral-grey-100", children: formatDateTime(claimedAt) })] })), timestampValid && (jobInProgress || jobPending) && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Updated" }), _jsx("dd", { className: "text-[12px] text-neutral-grey-100", children: formatDateTime(timestamp) })] })), durationMs != null && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: jobInProgress || jobPending ? "Running" : "Duration" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", title: durationApproximated
1202
1511
  ? "Approximate — measured from batch start"
1203
- : undefined, children: [durationApproximated ? "~" : "", formatDuration(durationMs)] })] })), statistics && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Fields" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: [fieldText, emptyFieldText ? (_jsxs("span", { className: "ml-1.5 text-neutral-grey-50", children: ["(", emptyFieldText, ")"] })) : null] })] })), statistics && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Text" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: [formatCount(statistics.sourceCharacterCount), " chars"] })] })), job.totalCost != null && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Cost" }), _jsx("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: formatUsdCost(job.totalCost) })] })), lastHeartbeatValid && (jobInProgress || jobPending) && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Heartbeat" }), _jsx("dd", { className: "text-[12px] text-neutral-grey-100", children: lastHeartbeat.toLocaleString() })] })), attemptCount > 1 && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Attempts" }), _jsx("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: attemptCount })] })), job.hash && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Hash" }), _jsx("dd", { className: "text-[11px] font-mono text-neutral-grey-100 break-all", children: job.hash })] })), job.message &&
1512
+ : undefined, children: [durationApproximated ? "~" : "", formatDuration(durationMs)] })] })), statistics && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Fields" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: [fieldText, emptyFieldText ? (_jsxs("span", { className: "ml-1.5 text-neutral-grey-50", children: ["(", emptyFieldText, ")"] })) : null] })] })), statistics && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Text" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: [formatCount(statistics.sourceCharacterCount), " chars"] })] })), job.totalCost != null && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Cost" }), _jsx("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: formatUsdCost(job.totalCost) })] })), lastHeartbeatValid && (jobInProgress || jobPending) && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Heartbeat" }), _jsx("dd", { className: "text-[12px] text-neutral-grey-100", children: formatDateTime(lastHeartbeat) })] })), attemptCount > 1 && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Attempts" }), _jsx("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: attemptCount })] })), job.hash && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Hash" }), _jsx("dd", { className: "text-[11px] font-mono text-neutral-grey-100 break-all", children: job.hash })] })), job.message &&
1204
1513
  (jobError || jobAborted ? (
1205
1514
  // Error/abort messages can be long and multi-line (e.g. an external
1206
1515
  // ITranslationService stack trace). Render them in a bounded,
1207
1516
  // scrollable monospace block so the popover stays a sensible size and
1208
1517
  // the original formatting is preserved.
1209
- _jsxs("div", { className: "flex flex-col gap-1 px-3 py-2", children: [_jsx("dt", { className: "text-[10px] tracking-wide text-neutral-grey-50", children: jobError ? "Error" : "Aborted" }), _jsx("dd", { className: `max-h-40 overflow-auto whitespace-pre-wrap break-words rounded-sm bg-neutral-grey-5 p-2 font-mono text-[11px] leading-snug ${jobError ? "text-feedback-red" : "text-feedback-orange"}`, children: job.message })] })) : (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Progress" }), _jsx("dd", { className: "text-[12px] break-words text-neutral-grey-100", children: job.message })] }))), job.batchId && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Batch" }), _jsx("dd", { className: "text-[11px] font-mono text-neutral-grey-50 break-all", children: job.batchId })] }))] }), _jsxs("div", { className: "flex justify-end gap-2 border-t border-border-default px-3 py-2", children: [jobError && onRetry && (_jsxs(Button, { size: "sm", variant: "outline", disabled: isRetrying, onClick: (e) => {
1518
+ _jsxs("div", { className: "flex flex-col gap-1 px-3 py-2", children: [_jsx("dt", { className: "text-[10px] tracking-wide text-neutral-grey-50", children: jobError ? "Error" : "Aborted" }), _jsx("dd", { className: `max-h-40 overflow-auto whitespace-pre-wrap break-words rounded-sm bg-neutral-grey-5 p-2 font-mono text-[11px] leading-snug ${jobError ? "text-feedback-red" : "text-feedback-orange"}`, children: job.message })] })) : (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Progress" }), _jsx("dd", { className: "text-[12px] break-words text-neutral-grey-100", children: job.message })] }))), job.batchId && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Batch" }), _jsx("dd", { className: "text-[11px] font-mono text-neutral-grey-50 break-all", children: job.batchId })] }))] }), _jsxs("div", { className: "flex justify-end gap-2 border-t border-border-default px-3 py-2", children: [canAbort && (_jsxs(Button, { size: "sm", variant: "outline", disabled: isAborting, onClick: (e) => {
1519
+ e.stopPropagation();
1520
+ void onAbort?.();
1521
+ setOpen(false);
1522
+ }, className: "gap-1.5 text-feedback-orange hover:text-feedback-orange", children: [_jsx(AbortIcon, { className: `h-3.5 w-3.5 ${isAborting ? "animate-spin" : ""}` }), "Abort"] })), jobError && onRetry && (_jsxs(Button, { size: "sm", variant: "outline", disabled: isRetrying, onClick: (e) => {
1210
1523
  e.stopPropagation();
1211
1524
  void onRetry();
1212
1525
  setOpen(false);