@parhelia/localization 0.1.12793 → 0.1.12800

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 +1 @@
1
- {"version":3,"file":"TranslationBatches.d.ts","sourceRoot":"","sources":["../../src/translation-center/TranslationBatches.tsx"],"names":[],"mappings":"AA2NA,wBAAgB,kBAAkB,4CA86BjC"}
1
+ {"version":3,"file":"TranslationBatches.d.ts","sourceRoot":"","sources":["../../src/translation-center/TranslationBatches.tsx"],"names":[],"mappings":"AAgPA,wBAAgB,kBAAkB,4CA6gCjC"}
@@ -2,9 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useCallback, useEffect, useRef, useState, useMemo } from "react";
3
3
  import React from "react";
4
4
  import { Button, Select, Input, UserPicker, Popover, PopoverContent, PopoverTrigger, ContentTree, fetchItemStubs, searchUsers as searchBackendUsers, useEditContext, getLanguages, SimpleIconButton, } from "@parhelia/core";
5
- import { listBatches, getTranslationProviders, listBatchTranslationJobs, abortBatch, deleteBatch, } from "../services/translationService";
5
+ import { listBatches, getTranslationProviders, listBatchTranslationJobs, retryBatchTranslation, abortBatch, deleteBatch, } from "../services/translationService";
6
6
  import { useDebouncedCallback } from "use-debounce";
7
- import { X, ChevronDown, Languages, Loader2, Calendar, CheckCircle2, User as UserIcon2, Cloud, Globe, FileText, FolderTree, ExternalLink, Check, AlertCircle, CircleStop, Trash2, Hourglass, Copy, Sparkles, } from "lucide-react";
7
+ import { X, ChevronDown, Languages, Loader2, Calendar, CheckCircle2, User as UserIcon2, Cloud, Globe, FileText, FolderTree, ExternalLink, Check, AlertCircle, CircleStop, Trash2, Hourglass, Copy, Sparkles, RotateCcw, } from "lucide-react";
8
8
  import { toast } from "sonner";
9
9
  // Wrappers for lucide-react icons to work with React 19
10
10
  const XIcon = (props) => React.createElement(X, props);
@@ -26,6 +26,7 @@ const TrashIcon = (props) => React.createElement(Trash2, props);
26
26
  const QueuedIcon = (props) => React.createElement(Hourglass, props);
27
27
  const CopyIcon = (props) => React.createElement(Copy, props);
28
28
  const SparklesIcon = (props) => React.createElement(Sparkles, props);
29
+ const RetryIcon = (props) => React.createElement(RotateCcw, props);
29
30
  const DATE_RANGE_OPTIONS = [
30
31
  { value: "lastHour", label: "Last Hour" },
31
32
  { value: "last24hours", label: "Last 24 Hours" },
@@ -57,6 +58,20 @@ function isTerminalBatchStatus(status) {
57
58
  function isActiveBatchStatus(status) {
58
59
  return status === "Pending" || status === "In Progress";
59
60
  }
61
+ function isRetriableJob(job) {
62
+ return job?.status === "Error";
63
+ }
64
+ function buildRetryJobRequest(job) {
65
+ if (!job.itemId || !job.sourceLanguage || !job.targetLanguage)
66
+ return null;
67
+ return {
68
+ sourceTranslationId: job.id,
69
+ itemId: job.itemId,
70
+ sourceLanguage: job.sourceLanguage,
71
+ targetLanguage: job.targetLanguage,
72
+ metadata: job.metadata,
73
+ };
74
+ }
60
75
  function normalizeGuid(value) {
61
76
  return (value ?? "").toString().replace(/[{}()]/g, "").toLowerCase();
62
77
  }
@@ -110,6 +125,7 @@ export function TranslationBatches() {
110
125
  const [loadingJobs, setLoadingJobs] = useState(new Set());
111
126
  const [abortingBatchIds, setAbortingBatchIds] = useState(new Set());
112
127
  const [deletingBatchIds, setDeletingBatchIds] = useState(new Set());
128
+ const [retryingKeys, setRetryingKeys] = useState(new Set());
113
129
  // Cache of userName → displayName so we can render full names on batch rows
114
130
  // without re-querying the user service every render.
115
131
  const [userDisplayNames, setUserDisplayNames] = useState({});
@@ -388,10 +404,13 @@ export function TranslationBatches() {
388
404
  try {
389
405
  const res = await listBatchTranslationJobs(batchId);
390
406
  const data = (res?.data ?? res ?? []);
391
- setBatchJobs(prev => ({ ...prev, [batchId]: Array.isArray(data) ? data : [] }));
407
+ const jobs = Array.isArray(data) ? data : [];
408
+ setBatchJobs(prev => ({ ...prev, [batchId]: jobs }));
409
+ return jobs;
392
410
  }
393
411
  catch (error) {
394
412
  console.error("Failed to load batch jobs:", error);
413
+ return [];
395
414
  }
396
415
  finally {
397
416
  setLoadingJobs(prev => {
@@ -487,6 +506,64 @@ export function TranslationBatches() {
487
506
  },
488
507
  });
489
508
  }, [editContext]);
509
+ const handleRetryJobs = useCallback(async (batchId, provider, jobs, retryKey) => {
510
+ const sessionId = editContext?.sessionId;
511
+ if (!sessionId) {
512
+ toast.error("Cannot retry translations without an active session.");
513
+ return;
514
+ }
515
+ const retryJobs = jobs
516
+ .filter(isRetriableJob)
517
+ .map(buildRetryJobRequest)
518
+ .filter((job) => job != null);
519
+ if (retryJobs.length === 0) {
520
+ toast.error("No failed translations are available to retry.");
521
+ return;
522
+ }
523
+ setRetryingKeys(prev => new Set(prev).add(retryKey));
524
+ try {
525
+ const result = await retryBatchTranslation({
526
+ sessionId,
527
+ sourceBatchId: batchId,
528
+ provider,
529
+ jobs: retryJobs,
530
+ });
531
+ if (result.type !== "success") {
532
+ toast.error(result.details || result.summary || "Failed to retry translations.");
533
+ return;
534
+ }
535
+ const payload = (result?.data ?? result);
536
+ const startedCount = payload.started?.length ?? 0;
537
+ const skippedCount = payload.skipped?.length ?? 0;
538
+ if (startedCount === 0) {
539
+ toast.error(skippedCount > 0
540
+ ? "No translations were retried. They may already have been retried."
541
+ : "No translations were retried.");
542
+ }
543
+ else {
544
+ toast.success(`Retry queued for ${startedCount} translation${startedCount !== 1 ? "s" : ""}.`);
545
+ if (skippedCount > 0) {
546
+ toast.error(`${skippedCount} translation${skippedCount !== 1 ? "s were" : " was"} skipped.`);
547
+ }
548
+ }
549
+ await loadRecentBatches(0, false, effectiveFilters);
550
+ if (expandedBatchId === batchId || batchJobs[batchId]) {
551
+ await loadBatchJobs(batchId);
552
+ }
553
+ }
554
+ finally {
555
+ setRetryingKeys(prev => {
556
+ const next = new Set(prev);
557
+ next.delete(retryKey);
558
+ return next;
559
+ });
560
+ }
561
+ }, [batchJobs, editContext?.sessionId, effectiveFilters, expandedBatchId, loadBatchJobs, loadRecentBatches]);
562
+ const handleRetryBatch = useCallback(async (batch) => {
563
+ const batchId = batch.id;
564
+ const jobs = batchJobs[batchId] ?? await loadBatchJobs(batchId);
565
+ await handleRetryJobs(batchId, batch.provider, jobs, `batch:${batchId}`);
566
+ }, [batchJobs, handleRetryJobs, loadBatchJobs]);
490
567
  const openItemInEditor = useCallback(async (itemId, language) => {
491
568
  const normalized = (itemId ?? "").toString().toLowerCase();
492
569
  editContext?.setShowAgentsWorkspaceEditor(true);
@@ -609,8 +686,10 @@ export function TranslationBatches() {
609
686
  const isExpanded = expandedBatchId === b.id;
610
687
  const jobs = batchJobs[b.id];
611
688
  const isLoadingThis = loadingJobs.has(b.id);
689
+ const canRetry = anyError;
612
690
  const canAbort = isActiveBatchStatus(info?.status);
613
691
  const canDelete = isTerminalBatchStatus(info?.status);
692
+ const isRetryingBatch = retryingKeys.has(`batch:${b.id}`);
614
693
  const isAborting = abortingBatchIds.has(b.id);
615
694
  const isDeleting = deletingBatchIds.has(b.id);
616
695
  // Stable test-id status used by Playwright assertions.
@@ -644,13 +723,16 @@ export function TranslationBatches() {
644
723
  }
645
724
  }
646
725
  return parsedName ?? `Translation Batch · ${timeDisplay}`;
647
- })() }), showProgress && (_jsxs("span", { className: `inline-flex items-center gap-1 text-[11px] tabular-nums ${anyError ? 'text-red-600' : 'text-theme-secondary'}`, "data-testid": `batch-progress-${b.id}`, children: [completedJobs, "/", totalJobs, anyError && _jsxs("span", { className: "text-red-600", 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-gray-2 pl-4", children: [metaParts.map((part, i) => (_jsxs(React.Fragment, { children: [i > 0 && _jsx("span", { className: "text-gray-3", children: "\u00B7" }), part] }, i))), info?.lastUpdatedUtc && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-gray-3", children: "\u00B7" }), _jsx("span", { children: new Date(info.lastUpdatedUtc).toLocaleString() })] }))] })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [canAbort && (_jsx(SimpleIconButton, { onClick: (event) => {
726
+ })() }), showProgress && (_jsxs("span", { className: `inline-flex items-center gap-1 text-[11px] tabular-nums ${anyError ? 'text-red-600' : 'text-theme-secondary'}`, "data-testid": `batch-progress-${b.id}`, children: [completedJobs, "/", totalJobs, anyError && _jsxs("span", { className: "text-red-600", 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-gray-2 pl-4", children: [metaParts.map((part, i) => (_jsxs(React.Fragment, { children: [i > 0 && _jsx("span", { className: "text-gray-3", children: "\u00B7" }), part] }, i))), info?.lastUpdatedUtc && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-gray-3", 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) => {
727
+ event.stopPropagation();
728
+ void handleRetryBatch(info);
729
+ }, icon: _jsx(RetryIcon, { className: `h-3.5 w-3.5 ${isRetryingBatch ? "animate-spin" : ""}`, strokeWidth: 1.5 }), label: "Retry failed translations", disabled: isRetryingBatch || isLoadingThis || isAborting || isDeleting, className: "p-0! text-red-600 hover:text-red-700" })), canAbort && (_jsx(SimpleIconButton, { onClick: (event) => {
648
730
  event.stopPropagation();
649
731
  handleAbortBatch(b.id);
650
732
  }, 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-amber-600 hover:text-amber-700" })), canDelete && (_jsx(SimpleIconButton, { onClick: (event) => {
651
733
  event.stopPropagation();
652
734
  handleDeleteBatch(b.id);
653
- }, icon: _jsx(TrashIcon, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }), label: "Delete translation history", disabled: isDeleting || isAborting, className: "p-0! text-red-500 hover:text-red-600" })), _jsx("div", { className: `text-gray-3 transition-transform ${isExpanded ? 'rotate-180' : ''} group-hover:text-(--color-gray-2)`, children: _jsx(ChevronDownIcon, { className: "h-4 w-4" }) })] })] }) }), isExpanded && (_jsxs(_Fragment, { children: [_jsx(CustomPromptPanel, { prompts: parseCustomPrompts(info?.metadata) }), _jsx(BatchItemList, { 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, onToggleItem: toggleItem, onOpenItem: openItemInEditor })] }))] }, b.id));
735
+ }, icon: _jsx(TrashIcon, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }), label: "Delete translation history", disabled: isDeleting || isAborting, className: "p-0! text-red-500 hover:text-red-600" })), _jsx("div", { className: `text-gray-3 transition-transform ${isExpanded ? 'rotate-180' : ''} group-hover:text-(--color-gray-2)`, 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));
654
736
  }) })] }, 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"] })) }) }))] })) })] }));
655
737
  }
656
738
  function isLikelyGuid(value) {
@@ -785,7 +867,7 @@ function CustomPromptPanel({ prompts }) {
785
867
  }, 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-gray-2 hover:text-(--color-dark-lighter)" })), _jsx(ChevronDownIcon, { className: `h-3.5 w-3.5 text-gray-3 transition-transform group-hover:text-(--color-gray-2) ${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-gray-3/50 bg-white p-2 font-mono text-[12px] text-(--color-dark-lighter)", children: p.prompt }) }))] }, p.provider));
786
868
  }) }));
787
869
  }
788
- function BatchItemList({ jobs, isLoading, expandedItems, languages, itemFilter, itemIncludeSubitems, statusFilter, batchIsTerminal, batchStartedAtUtc, onToggleItem, onOpenItem }) {
870
+ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems, languages, itemFilter, itemIncludeSubitems, statusFilter, batchIsTerminal, batchStartedAtUtc, retryingKeys, onToggleItem, onOpenItem, onRetryJobs }) {
789
871
  const languageMap = React.useMemo(() => {
790
872
  const map = new Map();
791
873
  for (const l of languages) {
@@ -897,18 +979,25 @@ function BatchItemList({ jobs, isLoading, expandedItems, languages, itemFilter,
897
979
  const langs = itemJobs.map(j => j.targetLanguage).filter(Boolean);
898
980
  const uniqueLangs = Array.from(new Set(langs));
899
981
  const titleAttr = `${itemPath ?? ""}\n${itemId}`.trim();
982
+ const itemErrorJobs = itemJobs.filter(isRetriableJob);
983
+ const itemRetryKey = `item:${batchId}:${itemId}`;
984
+ const isRetryingItem = retryingKeys.has(itemRetryKey);
900
985
  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-gray-5 transition-colors cursor-pointer text-left", onClick: () => onToggleItem(itemId), onKeyDown: (e) => {
901
986
  if (e.key === "Enter" || e.key === " ") {
902
987
  e.preventDefault();
903
988
  onToggleItem(itemId);
904
989
  }
905
- }, 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-theme-secondary" })) : itemIsQueued ? (_jsx(QueuedIcon, { className: "h-3 w-3 text-gray-2", 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-(--color-dark-lighter) truncate", title: titleAttr, children: displayName }), _jsxs("span", { className: "text-[11px] text-gray-2 shrink-0", children: ["\u00B7 ", uniqueLangs.length, " language", uniqueLangs.length !== 1 ? 's' : ''] })] }), itemPath && (_jsx("div", { className: "text-[11px] text-gray-2 truncate font-mono", title: titleAttr, children: itemPath }))] }), _jsx("span", { className: "mt-1 shrink-0 text-gray-3 group-hover:text-(--color-gray-2)", 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) => {
990
+ }, 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-theme-secondary" })) : itemIsQueued ? (_jsx(QueuedIcon, { className: "h-3 w-3 text-gray-2", 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-(--color-dark-lighter) truncate", title: titleAttr, children: displayName }), _jsxs("span", { className: "text-[11px] text-gray-2 shrink-0", children: ["\u00B7 ", uniqueLangs.length, " language", uniqueLangs.length !== 1 ? 's' : ''] })] }), itemPath && (_jsx("div", { className: "text-[11px] text-gray-2 truncate font-mono", title: titleAttr, children: itemPath }))] }), itemErrorJobs.length > 0 && (_jsx(SimpleIconButton, { onClick: (event) => {
991
+ event.stopPropagation();
992
+ void onRetryJobs(batchId, batchProvider, itemErrorJobs, itemRetryKey);
993
+ }, 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-red-600 hover:text-red-700" })), _jsx("span", { className: "mt-1 shrink-0 text-gray-3 group-hover:text-(--color-gray-2)", 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) => {
906
994
  const lang = languageMap.get((job.targetLanguage || "").toLowerCase());
907
- return (_jsx(LanguageJobChip, { job: job, language: lang, batchStartedAtUtc: batchStartedAtUtc, onOpen: () => onOpenItem(job.itemId, job.targetLanguage) }, `${job.id ?? idx}-${job.targetLanguage}`));
995
+ const retryKey = `job:${job.id ?? `${job.itemId}:${job.sourceLanguage}:${job.targetLanguage}`}`;
996
+ 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}`));
908
997
  }) }))] }, itemId));
909
998
  }) }));
910
999
  }
911
- function LanguageJobChip({ job, language, batchStartedAtUtc, onOpen }) {
1000
+ function LanguageJobChip({ job, language, batchStartedAtUtc, onOpen, isRetrying = false, onRetry }) {
912
1001
  const [open, setOpen] = React.useState(false);
913
1002
  const jobStatus = job.status;
914
1003
  const jobInProgress = jobStatus === "In Progress";
@@ -987,9 +1076,13 @@ function LanguageJobChip({ job, language, batchStartedAtUtc, onOpen }) {
987
1076
  const emptyFieldText = statistics && statistics.emptyFieldCount > 0
988
1077
  ? `${formatCount(statistics.emptyFieldCount)} empty`
989
1078
  : null;
990
- 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 px-2 py-0.5 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-emerald-600" }))] }) }), _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-gray-3 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-(--color-dark-lighter) truncate", children: langName }), _jsxs("div", { className: "text-[11px] text-gray-2 font-mono truncate", children: [job.targetLanguage, job.sourceLanguage ? ` ← ${job.sourceLanguage}` : ""] })] }), _jsxs("span", { className: `inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${statusBadgeCls}`, children: [jobInProgress && _jsx(LoaderIcon, { className: "h-2.5 w-2.5 animate-spin" }), jobStatus || "Unknown"] })] }), _jsxs("dl", { className: "divide-y divide-gray-3/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] uppercase tracking-wide text-gray-2", children: "Claimed" }), _jsx("dd", { className: "text-[12px] text-(--color-gray-1)", 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] uppercase tracking-wide text-gray-2", children: "Updated" }), _jsx("dd", { className: "text-[12px] text-(--color-gray-1)", 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] uppercase tracking-wide text-gray-2", children: jobInProgress || jobPending ? "Running" : "Duration" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-(--color-gray-1)", title: durationApproximated ? "Approximate — measured from batch start" : 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] uppercase tracking-wide text-gray-2", children: "Fields" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-(--color-gray-1)", children: [fieldText, emptyFieldText ? _jsxs("span", { className: "ml-1.5 text-gray-2", 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] uppercase tracking-wide text-gray-2", children: "Text" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-(--color-gray-1)", 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] uppercase tracking-wide text-gray-2", children: "Cost" }), _jsx("dd", { className: "text-[12px] tabular-nums text-(--color-gray-1)", 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] uppercase tracking-wide text-gray-2", children: "Heartbeat" }), _jsx("dd", { className: "text-[12px] text-(--color-gray-1)", 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] uppercase tracking-wide text-gray-2", children: "Attempts" }), _jsx("dd", { className: "text-[12px] tabular-nums text-(--color-gray-1)", 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] uppercase tracking-wide text-gray-2", children: "Hash" }), _jsx("dd", { className: "text-[11px] font-mono text-(--color-gray-1) break-all", children: job.hash })] })), job.message && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] uppercase tracking-wide text-gray-2", children: jobError ? "Error" : jobAborted ? "Aborted" : "Message" }), _jsx("dd", { className: `text-[12px] break-words ${jobError ? "text-red-600" : jobAborted ? "text-amber-700" : "text-(--color-gray-1)"}`, 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] uppercase tracking-wide text-gray-2", children: "Batch" }), _jsx("dd", { className: "text-[11px] font-mono text-gray-2 break-all", children: job.batchId })] }))] }), _jsx("div", { className: "flex justify-end gap-2 border-t border-gray-3 px-3 py-2", children: _jsxs(Button, { size: "sm", variant: "default", onClick: (e) => {
991
- e.stopPropagation();
992
- void onOpen();
993
- setOpen(false);
994
- }, className: "gap-1.5", children: [_jsx(ExternalLinkIcon, { className: "h-3.5 w-3.5" }), "Open in editor"] }) })] })] }));
1079
+ 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 px-2 py-0.5 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-emerald-600" }))] }) }), _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-gray-3 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-(--color-dark-lighter) truncate", children: langName }), _jsxs("div", { className: "text-[11px] text-gray-2 font-mono truncate", children: [job.targetLanguage, job.sourceLanguage ? ` ← ${job.sourceLanguage}` : ""] })] }), _jsxs("span", { className: `inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${statusBadgeCls}`, children: [jobInProgress && _jsx(LoaderIcon, { className: "h-2.5 w-2.5 animate-spin" }), jobStatus || "Unknown"] })] }), _jsxs("dl", { className: "divide-y divide-gray-3/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] uppercase tracking-wide text-gray-2", children: "Claimed" }), _jsx("dd", { className: "text-[12px] text-(--color-gray-1)", 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] uppercase tracking-wide text-gray-2", children: "Updated" }), _jsx("dd", { className: "text-[12px] text-(--color-gray-1)", 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] uppercase tracking-wide text-gray-2", children: jobInProgress || jobPending ? "Running" : "Duration" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-(--color-gray-1)", title: durationApproximated ? "Approximate — measured from batch start" : 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] uppercase tracking-wide text-gray-2", children: "Fields" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-(--color-gray-1)", children: [fieldText, emptyFieldText ? _jsxs("span", { className: "ml-1.5 text-gray-2", 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] uppercase tracking-wide text-gray-2", children: "Text" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-(--color-gray-1)", 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] uppercase tracking-wide text-gray-2", children: "Cost" }), _jsx("dd", { className: "text-[12px] tabular-nums text-(--color-gray-1)", 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] uppercase tracking-wide text-gray-2", children: "Heartbeat" }), _jsx("dd", { className: "text-[12px] text-(--color-gray-1)", 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] uppercase tracking-wide text-gray-2", children: "Attempts" }), _jsx("dd", { className: "text-[12px] tabular-nums text-(--color-gray-1)", 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] uppercase tracking-wide text-gray-2", children: "Hash" }), _jsx("dd", { className: "text-[11px] font-mono text-(--color-gray-1) break-all", children: job.hash })] })), job.message && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] uppercase tracking-wide text-gray-2", children: jobError ? "Error" : jobAborted ? "Aborted" : "Message" }), _jsx("dd", { className: `text-[12px] break-words ${jobError ? "text-red-600" : jobAborted ? "text-amber-700" : "text-(--color-gray-1)"}`, 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] uppercase tracking-wide text-gray-2", children: "Batch" }), _jsx("dd", { className: "text-[11px] font-mono text-gray-2 break-all", children: job.batchId })] }))] }), _jsxs("div", { className: "flex justify-end gap-2 border-t border-gray-3 px-3 py-2", children: [jobError && onRetry && (_jsxs(Button, { size: "sm", variant: "outline", disabled: isRetrying, onClick: (e) => {
1080
+ e.stopPropagation();
1081
+ void onRetry();
1082
+ setOpen(false);
1083
+ }, className: "gap-1.5", children: [_jsx(RetryIcon, { className: `h-3.5 w-3.5 ${isRetrying ? "animate-spin" : ""}` }), "Retry"] })), _jsxs(Button, { size: "sm", variant: "default", onClick: (e) => {
1084
+ e.stopPropagation();
1085
+ void onOpen();
1086
+ setOpen(false);
1087
+ }, className: "gap-1.5", children: [_jsx(ExternalLinkIcon, { className: "h-3.5 w-3.5" }), "Open in editor"] })] })] })] }));
995
1088
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parhelia/localization",
3
- "version": "0.1.12793",
3
+ "version": "0.1.12800",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"