@purposeinplay/payload-ai-translate 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +714 -0
- package/dist/alerts-collection.d.ts +21 -0
- package/dist/alerts-collection.js +159 -0
- package/dist/api.d.ts +4 -0
- package/dist/api.js +918 -0
- package/dist/bulk-translate-batches-collection.d.ts +29 -0
- package/dist/bulk-translate-batches-collection.js +404 -0
- package/dist/bulk-translate-units-collection.d.ts +35 -0
- package/dist/bulk-translate-units-collection.js +310 -0
- package/dist/client/estimated-cost-cell.d.ts +6 -0
- package/dist/client/estimated-cost-cell.js +12 -0
- package/dist/client/excluded-fields-field.d.ts +45 -0
- package/dist/client/excluded-fields-field.js +553 -0
- package/dist/client/field-translate-button.d.ts +6 -0
- package/dist/client/field-translate-button.js +199 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.js +6 -0
- package/dist/client/lib/use-global-kill-switches.d.ts +20 -0
- package/dist/client/lib/use-global-kill-switches.js +58 -0
- package/dist/client/translate-button.d.ts +2 -0
- package/dist/client/translate-button.js +228 -0
- package/dist/client/translate-modal.d.ts +16 -0
- package/dist/client/translate-modal.js +549 -0
- package/dist/client/translation-progress.d.ts +10 -0
- package/dist/client/translation-progress.js +297 -0
- package/dist/components/TranslationNavGroup.d.ts +45 -0
- package/dist/components/TranslationNavGroup.js +104 -0
- package/dist/defaults.d.ts +11 -0
- package/dist/defaults.js +16 -0
- package/dist/endpoints/client-config.d.ts +44 -0
- package/dist/endpoints/client-config.js +145 -0
- package/dist/endpoints/estimate.d.ts +5 -0
- package/dist/endpoints/estimate.js +237 -0
- package/dist/endpoints/progress.d.ts +2 -0
- package/dist/endpoints/progress.js +314 -0
- package/dist/endpoints/translate.d.ts +11 -0
- package/dist/endpoints/translate.js +376 -0
- package/dist/endpoints/translation-hub/_helpers.d.ts +140 -0
- package/dist/endpoints/translation-hub/_helpers.js +297 -0
- package/dist/endpoints/translation-hub/active.d.ts +21 -0
- package/dist/endpoints/translation-hub/active.js +220 -0
- package/dist/endpoints/translation-hub/cancel.d.ts +22 -0
- package/dist/endpoints/translation-hub/cancel.js +233 -0
- package/dist/endpoints/translation-hub/enqueue.d.ts +70 -0
- package/dist/endpoints/translation-hub/enqueue.js +529 -0
- package/dist/endpoints/translation-hub/failures.d.ts +12 -0
- package/dist/endpoints/translation-hub/failures.js +67 -0
- package/dist/endpoints/translation-hub/force-reset.d.ts +20 -0
- package/dist/endpoints/translation-hub/force-reset.js +144 -0
- package/dist/endpoints/translation-hub/index.d.ts +21 -0
- package/dist/endpoints/translation-hub/index.js +20 -0
- package/dist/endpoints/translation-hub/list.d.ts +40 -0
- package/dist/endpoints/translation-hub/list.js +182 -0
- package/dist/endpoints/translation-hub/preflight.d.ts +19 -0
- package/dist/endpoints/translation-hub/preflight.js +141 -0
- package/dist/endpoints/translation-hub/retry-failed.d.ts +38 -0
- package/dist/endpoints/translation-hub/retry-failed.js +235 -0
- package/dist/endpoints/translation-hub/revert.d.ts +88 -0
- package/dist/endpoints/translation-hub/revert.js +405 -0
- package/dist/endpoints/translation-hub/status.d.ts +45 -0
- package/dist/endpoints/translation-hub/status.js +391 -0
- package/dist/endpoints/translation-hub/usage-summary.d.ts +114 -0
- package/dist/endpoints/translation-hub/usage-summary.js +481 -0
- package/dist/exports/client.d.ts +6 -0
- package/dist/exports/client.js +6 -0
- package/dist/exports/components.d.ts +6 -0
- package/dist/exports/components.js +5 -0
- package/dist/exports/index.d.ts +8 -0
- package/dist/exports/index.js +7 -0
- package/dist/exports/providers.d.ts +9 -0
- package/dist/exports/providers.js +5 -0
- package/dist/exports/views-client.d.ts +23 -0
- package/dist/exports/views-client.js +22 -0
- package/dist/exports/views.d.ts +30 -0
- package/dist/exports/views.js +29 -0
- package/dist/hooks/after-change-global.d.ts +4 -0
- package/dist/hooks/after-change-global.js +109 -0
- package/dist/hooks/after-change.d.ts +16 -0
- package/dist/hooks/after-change.js +205 -0
- package/dist/hooks/after-delete.d.ts +30 -0
- package/dist/hooks/after-delete.js +95 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/jobs-collection.d.ts +17 -0
- package/dist/jobs-collection.js +139 -0
- package/dist/lexical/classifier.d.ts +3 -0
- package/dist/lexical/classifier.js +108 -0
- package/dist/lexical/deserializer.d.ts +4 -0
- package/dist/lexical/deserializer.js +263 -0
- package/dist/lexical/placeholder-integrity.d.ts +6 -0
- package/dist/lexical/placeholder-integrity.js +21 -0
- package/dist/lexical/placeholders.d.ts +21 -0
- package/dist/lexical/placeholders.js +117 -0
- package/dist/lexical/serializer.d.ts +21 -0
- package/dist/lexical/serializer.js +233 -0
- package/dist/lexical/types.d.ts +32 -0
- package/dist/lexical/types.js +1 -0
- package/dist/lib/auth-diagnostics.d.ts +14 -0
- package/dist/lib/auth-diagnostics.js +19 -0
- package/dist/lib/batch-counts.d.ts +58 -0
- package/dist/lib/batch-counts.js +105 -0
- package/dist/lib/bulk-translate-migrations.d.ts +92 -0
- package/dist/lib/bulk-translate-migrations.js +153 -0
- package/dist/lib/coalescing-queue.d.ts +38 -0
- package/dist/lib/coalescing-queue.js +69 -0
- package/dist/lib/content-extractor.d.ts +16 -0
- package/dist/lib/content-extractor.js +410 -0
- package/dist/lib/content-hash.d.ts +1 -0
- package/dist/lib/content-hash.js +19 -0
- package/dist/lib/content-patcher.d.ts +15 -0
- package/dist/lib/content-patcher.js +293 -0
- package/dist/lib/cost-guards.d.ts +2 -0
- package/dist/lib/cost-guards.js +18 -0
- package/dist/lib/daily-spend-cap.d.ts +58 -0
- package/dist/lib/daily-spend-cap.js +233 -0
- package/dist/lib/effective-locales.d.ts +181 -0
- package/dist/lib/effective-locales.js +302 -0
- package/dist/lib/error-messages.d.ts +245 -0
- package/dist/lib/error-messages.js +626 -0
- package/dist/lib/events.d.ts +39 -0
- package/dist/lib/events.js +146 -0
- package/dist/lib/exclude-fields.d.ts +3 -0
- package/dist/lib/exclude-fields.js +64 -0
- package/dist/lib/field-breadcrumb.d.ts +31 -0
- package/dist/lib/field-breadcrumb.js +227 -0
- package/dist/lib/field-diff.d.ts +1 -0
- package/dist/lib/field-diff.js +25 -0
- package/dist/lib/field-empty.d.ts +2 -0
- package/dist/lib/field-empty.js +68 -0
- package/dist/lib/field-resolver.d.ts +3 -0
- package/dist/lib/field-resolver.js +164 -0
- package/dist/lib/group-soft-skips.d.ts +39 -0
- package/dist/lib/group-soft-skips.js +45 -0
- package/dist/lib/locale-merge.d.ts +44 -0
- package/dist/lib/locale-merge.js +357 -0
- package/dist/lib/locale-row-check.d.ts +30 -0
- package/dist/lib/locale-row-check.js +64 -0
- package/dist/lib/logger.d.ts +74 -0
- package/dist/lib/logger.js +97 -0
- package/dist/lib/manual-edit-guard.d.ts +128 -0
- package/dist/lib/manual-edit-guard.js +393 -0
- package/dist/lib/output-validation.d.ts +48 -0
- package/dist/lib/output-validation.js +148 -0
- package/dist/lib/payload-read.d.ts +16 -0
- package/dist/lib/payload-read.js +51 -0
- package/dist/lib/per-doc-claim.d.ts +90 -0
- package/dist/lib/per-doc-claim.js +140 -0
- package/dist/lib/per-doc-lock.d.ts +94 -0
- package/dist/lib/per-doc-lock.js +119 -0
- package/dist/lib/persist-usage.d.ts +91 -0
- package/dist/lib/persist-usage.js +116 -0
- package/dist/lib/progress-store.d.ts +103 -0
- package/dist/lib/progress-store.js +314 -0
- package/dist/lib/rate-limiter.d.ts +3 -0
- package/dist/lib/rate-limiter.js +53 -0
- package/dist/lib/snapshot-select.d.ts +43 -0
- package/dist/lib/snapshot-select.js +108 -0
- package/dist/lib/translate-prompt.d.ts +31 -0
- package/dist/lib/translate-prompt.js +66 -0
- package/dist/lib/translation-token-bucket.d.ts +57 -0
- package/dist/lib/translation-token-bucket.js +365 -0
- package/dist/lib/truncate-source-value.d.ts +1 -0
- package/dist/lib/truncate-source-value.js +27 -0
- package/dist/manual-edit-collection.d.ts +22 -0
- package/dist/manual-edit-collection.js +124 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.js +934 -0
- package/dist/providers/ai-sdk-adapter.d.ts +35 -0
- package/dist/providers/ai-sdk-adapter.js +100 -0
- package/dist/providers/anthropic.d.ts +31 -0
- package/dist/providers/anthropic.js +66 -0
- package/dist/providers/custom.d.ts +36 -0
- package/dist/providers/custom.js +24 -0
- package/dist/providers/gemini.d.ts +20 -0
- package/dist/providers/gemini.js +48 -0
- package/dist/providers/mock.d.ts +2 -0
- package/dist/providers/mock.js +29 -0
- package/dist/providers/openai.d.ts +28 -0
- package/dist/providers/openai.js +69 -0
- package/dist/settings-global.d.ts +74 -0
- package/dist/settings-global.js +216 -0
- package/dist/tasks/bulk-translate-coordinator.d.ts +115 -0
- package/dist/tasks/bulk-translate-coordinator.js +708 -0
- package/dist/tasks/bulk-translate-doc-task.d.ts +142 -0
- package/dist/tasks/bulk-translate-doc-task.js +1000 -0
- package/dist/tasks/bulk-translate-janitor.d.ts +87 -0
- package/dist/tasks/bulk-translate-janitor.js +311 -0
- package/dist/tasks/translate-job-task.d.ts +51 -0
- package/dist/tasks/translate-job-task.js +154 -0
- package/dist/translate.d.ts +113 -0
- package/dist/translate.js +911 -0
- package/dist/translation-daily-spend-collection.d.ts +24 -0
- package/dist/translation-daily-spend-collection.js +133 -0
- package/dist/translation-rate-limits-collection.d.ts +30 -0
- package/dist/translation-rate-limits-collection.js +144 -0
- package/dist/types.d.ts +672 -0
- package/dist/types.js +1 -0
- package/dist/usage-collection.d.ts +14 -0
- package/dist/usage-collection.js +377 -0
- package/dist/views/BulkRunsHub/BatchRow.d.ts +32 -0
- package/dist/views/BulkRunsHub/BatchRow.js +1222 -0
- package/dist/views/BulkRunsHub/BucketRow.d.ts +62 -0
- package/dist/views/BulkRunsHub/BucketRow.js +982 -0
- package/dist/views/BulkRunsHub/BulkRunsHub.client.d.ts +18 -0
- package/dist/views/BulkRunsHub/BulkRunsHub.client.js +331 -0
- package/dist/views/BulkRunsHub/EmptyState.d.ts +6 -0
- package/dist/views/BulkRunsHub/EmptyState.js +64 -0
- package/dist/views/BulkRunsHub/FilterBar.d.ts +16 -0
- package/dist/views/BulkRunsHub/FilterBar.js +284 -0
- package/dist/views/BulkRunsHub/InFlightBanner.d.ts +14 -0
- package/dist/views/BulkRunsHub/InFlightBanner.js +59 -0
- package/dist/views/BulkRunsHub/StatusBadge.d.ts +64 -0
- package/dist/views/BulkRunsHub/StatusBadge.js +248 -0
- package/dist/views/BulkRunsHub/SummaryStrip.d.ts +22 -0
- package/dist/views/BulkRunsHub/SummaryStrip.js +249 -0
- package/dist/views/BulkRunsHub/bucket-grouping.d.ts +200 -0
- package/dist/views/BulkRunsHub/bucket-grouping.js +344 -0
- package/dist/views/BulkRunsHub/bucketFailureSummary.d.ts +9 -0
- package/dist/views/BulkRunsHub/bucketFailureSummary.js +36 -0
- package/dist/views/BulkRunsHub/dedupedStatusFetch.d.ts +5 -0
- package/dist/views/BulkRunsHub/dedupedStatusFetch.js +45 -0
- package/dist/views/BulkRunsHub/index.d.ts +17 -0
- package/dist/views/BulkRunsHub/index.js +80 -0
- package/dist/views/BulkRunsHub/urlFilters.d.ts +14 -0
- package/dist/views/BulkRunsHub/urlFilters.js +50 -0
- package/dist/views/BulkRunsHub/useBulkRunsList.d.ts +26 -0
- package/dist/views/BulkRunsHub/useBulkRunsList.js +204 -0
- package/dist/views/BulkRunsHub/useUrlFilters.d.ts +10 -0
- package/dist/views/BulkRunsHub/useUrlFilters.js +88 -0
- package/dist/views/TranslationHub/ActiveJobs.d.ts +6 -0
- package/dist/views/TranslationHub/ActiveJobs.js +320 -0
- package/dist/views/TranslationHub/AdvancedPanel.d.ts +17 -0
- package/dist/views/TranslationHub/AdvancedPanel.js +996 -0
- package/dist/views/TranslationHub/AlertBanner.d.ts +6 -0
- package/dist/views/TranslationHub/AlertBanner.js +568 -0
- package/dist/views/TranslationHub/AuditPanel.d.ts +6 -0
- package/dist/views/TranslationHub/AuditPanel.helpers.d.ts +44 -0
- package/dist/views/TranslationHub/AuditPanel.helpers.js +71 -0
- package/dist/views/TranslationHub/AuditPanel.js +1367 -0
- package/dist/views/TranslationHub/BulkTranslate.types.d.ts +242 -0
- package/dist/views/TranslationHub/BulkTranslate.types.js +36 -0
- package/dist/views/TranslationHub/BulkTranslateFailureDrawer.d.ts +19 -0
- package/dist/views/TranslationHub/BulkTranslateFailureDrawer.js +332 -0
- package/dist/views/TranslationHub/BulkTranslateMonitor.d.ts +28 -0
- package/dist/views/TranslationHub/BulkTranslateMonitor.js +305 -0
- package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.d.ts +3 -0
- package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.js +42 -0
- package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.d.ts +26 -0
- package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.js +95 -0
- package/dist/views/TranslationHub/BulkTranslatePreflightModal.d.ts +22 -0
- package/dist/views/TranslationHub/BulkTranslatePreflightModal.js +879 -0
- package/dist/views/TranslationHub/BulkTranslateTerminalCard.d.ts +29 -0
- package/dist/views/TranslationHub/BulkTranslateTerminalCard.js +445 -0
- package/dist/views/TranslationHub/BulkTranslateTrigger.d.ts +66 -0
- package/dist/views/TranslationHub/BulkTranslateTrigger.js +161 -0
- package/dist/views/TranslationHub/EditorRecentRunsPanel.d.ts +33 -0
- package/dist/views/TranslationHub/EditorRecentRunsPanel.js +290 -0
- package/dist/views/TranslationHub/Hub.client.d.ts +74 -0
- package/dist/views/TranslationHub/Hub.client.js +357 -0
- package/dist/views/TranslationHub/ModelCombobox.d.ts +14 -0
- package/dist/views/TranslationHub/ModelCombobox.js +415 -0
- package/dist/views/TranslationHub/PerCollectionConfig.d.ts +10 -0
- package/dist/views/TranslationHub/PerCollectionConfig.helpers.d.ts +16 -0
- package/dist/views/TranslationHub/PerCollectionConfig.helpers.js +19 -0
- package/dist/views/TranslationHub/PerCollectionConfig.js +759 -0
- package/dist/views/TranslationHub/SettingsRail.d.ts +11 -0
- package/dist/views/TranslationHub/SettingsRail.js +382 -0
- package/dist/views/TranslationHub/StatusStrip.d.ts +6 -0
- package/dist/views/TranslationHub/StatusStrip.js +451 -0
- package/dist/views/TranslationHub/UsageTable.d.ts +6 -0
- package/dist/views/TranslationHub/UsageTable.helpers.d.ts +69 -0
- package/dist/views/TranslationHub/UsageTable.helpers.js +49 -0
- package/dist/views/TranslationHub/UsageTable.js +1240 -0
- package/dist/views/TranslationHub/alertGrouping.d.ts +70 -0
- package/dist/views/TranslationHub/alertGrouping.js +99 -0
- package/dist/views/TranslationHub/index.d.ts +20 -0
- package/dist/views/TranslationHub/index.js +109 -0
- package/dist/views/TranslationHub/tabNavigation.d.ts +53 -0
- package/dist/views/TranslationHub/tabNavigation.js +74 -0
- package/dist/views/TranslationHub/terminalBannerVisibility.d.ts +33 -0
- package/dist/views/TranslationHub/terminalBannerVisibility.js +124 -0
- package/dist/views/TranslationHub/useBulkTranslateActive.d.ts +49 -0
- package/dist/views/TranslationHub/useBulkTranslateActive.js +251 -0
- package/dist/views/TranslationHub/useFocusTrap.d.ts +6 -0
- package/dist/views/TranslationHub/useFocusTrap.js +81 -0
- package/dist/views/TranslationHub/useTranslationHubUsageSummary.d.ts +77 -0
- package/dist/views/TranslationHub/useTranslationHubUsageSummary.js +267 -0
- package/dist/views/shared/EditorError.d.ts +97 -0
- package/dist/views/shared/EditorError.js +205 -0
- package/dist/views/shared/ModelCell.d.ts +18 -0
- package/dist/views/shared/ModelCell.js +31 -0
- package/dist/views/shared/docHref.d.ts +16 -0
- package/dist/views/shared/docHref.js +26 -0
- package/dist/views/shared/fetch-error-body.d.ts +25 -0
- package/dist/views/shared/fetch-error-body.js +42 -0
- package/dist/views/shared/filterPillStyle.d.ts +35 -0
- package/dist/views/shared/filterPillStyle.js +40 -0
- package/dist/views/shared/format.d.ts +75 -0
- package/dist/views/shared/format.js +131 -0
- package/package.json +141 -0
|
@@ -0,0 +1,1240 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useConfig } from '@payloadcms/ui';
|
|
4
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
5
|
+
import { editorMessageFor } from '../../lib/error-messages.js';
|
|
6
|
+
import { resolveFieldBreadcrumb } from '../../lib/field-breadcrumb.js';
|
|
7
|
+
import { groupSoftSkipsByValue } from '../../lib/group-soft-skips.js';
|
|
8
|
+
import { docHref as buildDocHref, globalHref } from '../shared/docHref.js';
|
|
9
|
+
import { EditorError } from '../shared/EditorError.js';
|
|
10
|
+
import { readResponseError } from '../shared/fetch-error-body.js';
|
|
11
|
+
import { formatCost, formatDuration } from '../shared/format.js';
|
|
12
|
+
import { ModelCell } from '../shared/ModelCell.js';
|
|
13
|
+
import { deriveBreakdown, deriveStatus } from './UsageTable.helpers.js';
|
|
14
|
+
const SECTION_STYLE = {
|
|
15
|
+
background: 'var(--theme-elevation-50)',
|
|
16
|
+
border: '1px solid var(--theme-elevation-150)',
|
|
17
|
+
borderRadius: '6px',
|
|
18
|
+
padding: '1rem'
|
|
19
|
+
};
|
|
20
|
+
// NEW-14 (v1.2.6): the wrapping `<div style={{ overflowX: 'auto' }}>`
|
|
21
|
+
// only kicks in when the table is wider than its parent. With
|
|
22
|
+
// `width: 100%` alone the browser tries to collapse columns to fit,
|
|
23
|
+
// silently clipping `whiteSpace: nowrap` cells (cost / duration /
|
|
24
|
+
// when) at <= 640px viewports — the user sees missing columns and no
|
|
25
|
+
// scrollbar cue. Pinning `minWidth: 640px` forces the table to its
|
|
26
|
+
// natural size, so the overflow:auto wrapper renders a visible
|
|
27
|
+
// horizontal scrollbar instead of clipping silently. Matches the
|
|
28
|
+
// established codebase pattern (AuditPanel + BulkRunsHub tables).
|
|
29
|
+
const TABLE_STYLE = {
|
|
30
|
+
width: '100%',
|
|
31
|
+
minWidth: '640px',
|
|
32
|
+
borderCollapse: 'collapse',
|
|
33
|
+
fontSize: '0.875rem',
|
|
34
|
+
color: 'var(--theme-elevation-800)'
|
|
35
|
+
};
|
|
36
|
+
const TH_STYLE = {
|
|
37
|
+
textAlign: 'left',
|
|
38
|
+
padding: '0.5rem 0.5rem',
|
|
39
|
+
fontSize: '0.75rem',
|
|
40
|
+
fontWeight: 600,
|
|
41
|
+
textTransform: 'uppercase',
|
|
42
|
+
letterSpacing: '0.05em',
|
|
43
|
+
color: 'var(--theme-elevation-500)',
|
|
44
|
+
borderBottom: '1px solid var(--theme-elevation-150)'
|
|
45
|
+
};
|
|
46
|
+
const TD_STYLE = {
|
|
47
|
+
padding: '0.5rem 0.5rem',
|
|
48
|
+
borderTop: '1px solid var(--theme-elevation-100)',
|
|
49
|
+
whiteSpace: 'nowrap'
|
|
50
|
+
};
|
|
51
|
+
const _STATUS_COLORS = {
|
|
52
|
+
succeeded: 'var(--theme-success-500, #16a34a)',
|
|
53
|
+
failed: 'var(--theme-error-500, #b91c1c)'
|
|
54
|
+
};
|
|
55
|
+
function relTime(iso) {
|
|
56
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
57
|
+
const s = Math.floor(diff / 1000);
|
|
58
|
+
if (s < 60) {
|
|
59
|
+
return `${s}s ago`;
|
|
60
|
+
}
|
|
61
|
+
if (s < 3600) {
|
|
62
|
+
return `${Math.floor(s / 60)}m ago`;
|
|
63
|
+
}
|
|
64
|
+
if (s < 86_400) {
|
|
65
|
+
return `${Math.floor(s / 3600)}h ago`;
|
|
66
|
+
}
|
|
67
|
+
return `${Math.floor(s / 86_400)}d ago`;
|
|
68
|
+
}
|
|
69
|
+
function fmtNum(n) {
|
|
70
|
+
if (n >= 1_000_000) {
|
|
71
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
72
|
+
}
|
|
73
|
+
if (n >= 1000) {
|
|
74
|
+
return `${(n / 1000).toFixed(1)}K`;
|
|
75
|
+
}
|
|
76
|
+
return n.toString();
|
|
77
|
+
}
|
|
78
|
+
function resolveCost(r, pricing) {
|
|
79
|
+
if (r.estimatedCostUsd != null && r.estimatedCostUsd > 0) {
|
|
80
|
+
return r.estimatedCostUsd;
|
|
81
|
+
}
|
|
82
|
+
// Stored 0 / null — try to compute from OpenRouter pricing.
|
|
83
|
+
if (!r.model) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const p = pricing.get(r.model);
|
|
87
|
+
if (!p) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return r.inputTokens * p.prompt + r.outputTokens * p.completion;
|
|
91
|
+
}
|
|
92
|
+
function docHref(basePath, r, locale) {
|
|
93
|
+
// Globals: /admin/globals/<slug>. The `documentId` field gets populated
|
|
94
|
+
// with the slug for globals (cms-plugins persists it that way) — ignore it.
|
|
95
|
+
if (r.kind === 'global') {
|
|
96
|
+
return globalHref(basePath, r.slug, locale);
|
|
97
|
+
}
|
|
98
|
+
// Collections: need a documentId to point at a row.
|
|
99
|
+
if (r.documentId) {
|
|
100
|
+
return buildDocHref(basePath, r.slug, r.documentId, locale);
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
// NEW-3 (v1.2.6): the status text on this table failed WCAG AA — the
|
|
105
|
+
// previous `-500` tokens resolve to mid-tones (`#1587BA` success,
|
|
106
|
+
// `#DA4B48` failed) that miss 4.5:1 against the white row background.
|
|
107
|
+
// Darken to `-800` for the coloured statuses and to `-elevation-900`
|
|
108
|
+
// for the neutral / "no-changes" / "preserved" states; that clears AA
|
|
109
|
+
// while preserving the semantic colour signal. Hex fallbacks updated
|
|
110
|
+
// in lockstep.
|
|
111
|
+
const DERIVED_STATUS_COLOR = {
|
|
112
|
+
failed: 'var(--theme-error-800, #7f1d1d)',
|
|
113
|
+
'needs-review': 'var(--theme-warning-800, #78350f)',
|
|
114
|
+
succeeded: 'var(--theme-success-800, #14532d)',
|
|
115
|
+
'no-changes': 'var(--theme-elevation-700)',
|
|
116
|
+
preserved: 'var(--theme-elevation-900)',
|
|
117
|
+
legacy: 'var(--theme-success-800, #14532d)'
|
|
118
|
+
};
|
|
119
|
+
const DERIVED_STATUS_LABEL = {
|
|
120
|
+
failed: 'failed',
|
|
121
|
+
'needs-review': 'needs review',
|
|
122
|
+
succeeded: 'succeeded',
|
|
123
|
+
'no-changes': 'no changes',
|
|
124
|
+
preserved: 'preserved',
|
|
125
|
+
legacy: 'succeeded'
|
|
126
|
+
};
|
|
127
|
+
function statusTooltipFor(s, b) {
|
|
128
|
+
switch(s){
|
|
129
|
+
case 'needs-review':
|
|
130
|
+
return `${b.softSkipped} field${b.softSkipped === 1 ? '' : 's'} weren’t translated automatically — the AI returned the original text unchanged, or the output didn’t pass quality checks. Open the row to review.`;
|
|
131
|
+
case 'no-changes':
|
|
132
|
+
return 'No translation was needed for this run — every field matched the version last translated.';
|
|
133
|
+
case 'preserved':
|
|
134
|
+
return `${b.preserved} field${b.preserved === 1 ? '' : 's'} were kept as-is because they were manually edited since the last automatic translation.`;
|
|
135
|
+
default:
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Renders a list of labelled segments separated by ` · ` interpuncts.
|
|
141
|
+
* Each segment has a stable string key — separators borrow the next
|
|
142
|
+
* segment's key (e.g. `sep-f`, `sep-r`) so React's reconciliation is
|
|
143
|
+
* stable across renders without resorting to array indices.
|
|
144
|
+
*/ function joinSegments(segments) {
|
|
145
|
+
const out = [];
|
|
146
|
+
for(let i = 0; i < segments.length; i++){
|
|
147
|
+
const seg = segments[i];
|
|
148
|
+
if (!seg) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (i > 0) {
|
|
152
|
+
out.push(/*#__PURE__*/ _jsx("span", {
|
|
153
|
+
style: {
|
|
154
|
+
color: 'var(--theme-elevation-500)'
|
|
155
|
+
},
|
|
156
|
+
children: ' · '
|
|
157
|
+
}, `sep-${seg.key}`));
|
|
158
|
+
}
|
|
159
|
+
out.push(seg.node);
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
// Each derived status's Fields-cell label is extracted into its own
|
|
164
|
+
// helper to keep `fieldsLabelFor` under biome's cognitive-complexity
|
|
165
|
+
// ceiling. They each return a FieldsLabel.
|
|
166
|
+
function labelForFailed(b) {
|
|
167
|
+
const segments = [];
|
|
168
|
+
if (b.translated > 0) {
|
|
169
|
+
segments.push({
|
|
170
|
+
key: 't',
|
|
171
|
+
node: /*#__PURE__*/ _jsxs("span", {
|
|
172
|
+
style: {
|
|
173
|
+
color: 'var(--theme-elevation-900)'
|
|
174
|
+
},
|
|
175
|
+
children: [
|
|
176
|
+
b.translated,
|
|
177
|
+
" translated"
|
|
178
|
+
]
|
|
179
|
+
}, "t")
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
segments.push({
|
|
183
|
+
key: 'f',
|
|
184
|
+
node: /*#__PURE__*/ _jsx("span", {
|
|
185
|
+
style: {
|
|
186
|
+
color: 'var(--theme-error-800, #7f1d1d)'
|
|
187
|
+
},
|
|
188
|
+
children: b.failed > 0 ? `${b.failed} failed` : 'failed'
|
|
189
|
+
}, "f")
|
|
190
|
+
});
|
|
191
|
+
return {
|
|
192
|
+
primary: joinSegments(segments)
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function labelForNeedsReview(b) {
|
|
196
|
+
const segments = [];
|
|
197
|
+
if (b.translated > 0) {
|
|
198
|
+
segments.push({
|
|
199
|
+
key: 't',
|
|
200
|
+
node: /*#__PURE__*/ _jsxs("span", {
|
|
201
|
+
style: {
|
|
202
|
+
color: 'var(--theme-elevation-900)'
|
|
203
|
+
},
|
|
204
|
+
children: [
|
|
205
|
+
b.translated,
|
|
206
|
+
" translated"
|
|
207
|
+
]
|
|
208
|
+
}, "t")
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
segments.push({
|
|
212
|
+
key: 'r',
|
|
213
|
+
node: /*#__PURE__*/ _jsxs("span", {
|
|
214
|
+
style: {
|
|
215
|
+
color: 'var(--theme-warning-800, #78350f)'
|
|
216
|
+
},
|
|
217
|
+
children: [
|
|
218
|
+
b.softSkipped,
|
|
219
|
+
" need",
|
|
220
|
+
b.softSkipped === 1 ? 's' : '',
|
|
221
|
+
" review"
|
|
222
|
+
]
|
|
223
|
+
}, "r")
|
|
224
|
+
});
|
|
225
|
+
return {
|
|
226
|
+
primary: joinSegments(segments),
|
|
227
|
+
tooltip: 'Some fields couldn’t be translated automatically. Open the row to see which fields and why.'
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// Whether the row has anything worth showing in the expansion panel.
|
|
231
|
+
// Plain green "succeeded" with no failures and no soft-skips has
|
|
232
|
+
// nothing to drill into; suppressing the chevron there reduces noise.
|
|
233
|
+
function rowHasDetail(r, status, b) {
|
|
234
|
+
return (r.targetLocales?.length ?? 0) > 0 || (r.softSkippedFields?.length ?? 0) > 0 || (r.preservedFields?.length ?? 0) > 0 || b.failed > 0 || status === 'failed' || status === 'needs-review';
|
|
235
|
+
}
|
|
236
|
+
// Click handler for the row's outer <tr>. Ignores clicks on
|
|
237
|
+
// interactive children so the doc link / retry button work normally.
|
|
238
|
+
function makeRowClickHandler(hasDetail, toggleExpand, id) {
|
|
239
|
+
return (e)=>{
|
|
240
|
+
if (!hasDetail) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const target = e.target;
|
|
244
|
+
if (target.closest('a, button, input, select, textarea')) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
toggleExpand(id);
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function fieldsLabelFor(s, b) {
|
|
251
|
+
const totalFields = b.translated + b.hashSkipped + b.preserved + b.softSkipped + b.failed;
|
|
252
|
+
switch(s){
|
|
253
|
+
case 'failed':
|
|
254
|
+
return labelForFailed(b);
|
|
255
|
+
case 'needs-review':
|
|
256
|
+
return labelForNeedsReview(b);
|
|
257
|
+
case 'no-changes':
|
|
258
|
+
{
|
|
259
|
+
// Hash-skip wins. No LLM call happened. Don't show "0 translated"
|
|
260
|
+
// — the user already knows from the status. Be specific about
|
|
261
|
+
// what's true.
|
|
262
|
+
const n = b.hashSkipped + b.preserved; // both contribute to no-changes
|
|
263
|
+
return {
|
|
264
|
+
primary: /*#__PURE__*/ _jsxs("span", {
|
|
265
|
+
style: {
|
|
266
|
+
color: 'var(--theme-elevation-700)'
|
|
267
|
+
},
|
|
268
|
+
children: [
|
|
269
|
+
"All ",
|
|
270
|
+
n,
|
|
271
|
+
" field",
|
|
272
|
+
n === 1 ? '' : 's',
|
|
273
|
+
" unchanged since last run"
|
|
274
|
+
]
|
|
275
|
+
}),
|
|
276
|
+
tooltip: b.preserved > 0 ? `${b.hashSkipped} unchanged · ${b.preserved} kept (manual edit)` : undefined
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
case 'preserved':
|
|
280
|
+
{
|
|
281
|
+
// Every field was manual-edit-guarded. No LLM, no overwrites.
|
|
282
|
+
return {
|
|
283
|
+
primary: /*#__PURE__*/ _jsxs("span", {
|
|
284
|
+
style: {
|
|
285
|
+
color: 'var(--theme-elevation-700)'
|
|
286
|
+
},
|
|
287
|
+
children: [
|
|
288
|
+
b.preserved,
|
|
289
|
+
" manual edit",
|
|
290
|
+
b.preserved === 1 ? '' : 's',
|
|
291
|
+
" kept"
|
|
292
|
+
]
|
|
293
|
+
}),
|
|
294
|
+
tooltip: "Target locale values diverge from the plugin's last-written hash — assumed manual edits, not overwritten."
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
case 'succeeded':
|
|
298
|
+
{
|
|
299
|
+
// Translation actually happened. May have some hash-skipped too.
|
|
300
|
+
const parts = [
|
|
301
|
+
/*#__PURE__*/ _jsxs("span", {
|
|
302
|
+
style: {
|
|
303
|
+
color: 'var(--theme-elevation-900)'
|
|
304
|
+
},
|
|
305
|
+
children: [
|
|
306
|
+
b.translated,
|
|
307
|
+
" translated"
|
|
308
|
+
]
|
|
309
|
+
}, "t")
|
|
310
|
+
];
|
|
311
|
+
if (b.hashSkipped > 0 || b.preserved > 0) {
|
|
312
|
+
const skippedNum = b.hashSkipped + b.preserved;
|
|
313
|
+
parts.push(/*#__PURE__*/ _jsx("span", {
|
|
314
|
+
style: {
|
|
315
|
+
color: 'var(--theme-elevation-500)'
|
|
316
|
+
},
|
|
317
|
+
children: ' · '
|
|
318
|
+
}, "sep"));
|
|
319
|
+
parts.push(/*#__PURE__*/ _jsxs("span", {
|
|
320
|
+
style: {
|
|
321
|
+
color: 'var(--theme-elevation-500)'
|
|
322
|
+
},
|
|
323
|
+
children: [
|
|
324
|
+
skippedNum,
|
|
325
|
+
" unchanged"
|
|
326
|
+
]
|
|
327
|
+
}, "s"));
|
|
328
|
+
}
|
|
329
|
+
let succeededTooltip;
|
|
330
|
+
if (b.preserved > 0) {
|
|
331
|
+
succeededTooltip = `${b.hashSkipped} source unchanged · ${b.preserved} kept (manual edit)`;
|
|
332
|
+
} else if (b.hashSkipped > 0) {
|
|
333
|
+
succeededTooltip = 'Source content matched the last-translated hash — no LLM call for these fields.';
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
primary: parts,
|
|
337
|
+
tooltip: succeededTooltip
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
default:
|
|
341
|
+
// Pre-migration row — we don't know what happened, just show
|
|
342
|
+
// counts based on what's in succeededCount.
|
|
343
|
+
return {
|
|
344
|
+
primary: /*#__PURE__*/ _jsx("span", {
|
|
345
|
+
style: {
|
|
346
|
+
color: 'var(--theme-elevation-500)'
|
|
347
|
+
},
|
|
348
|
+
children: totalFields > 0 ? `${totalFields} field${totalFields === 1 ? '' : 's'}` : '—'
|
|
349
|
+
})
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// Expanded row detail panel
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
const DETAIL_SECTION_STYLE = {
|
|
357
|
+
marginBottom: '0.75rem'
|
|
358
|
+
};
|
|
359
|
+
const DETAIL_LABEL_STYLE = {
|
|
360
|
+
fontSize: '0.7rem',
|
|
361
|
+
fontWeight: 600,
|
|
362
|
+
textTransform: 'uppercase',
|
|
363
|
+
letterSpacing: '0.05em',
|
|
364
|
+
color: 'var(--theme-elevation-500)',
|
|
365
|
+
marginBottom: '0.25rem'
|
|
366
|
+
};
|
|
367
|
+
const DETAIL_LIST_STYLE = {
|
|
368
|
+
margin: 0,
|
|
369
|
+
padding: 0,
|
|
370
|
+
listStyle: 'none',
|
|
371
|
+
fontSize: '0.8rem'
|
|
372
|
+
};
|
|
373
|
+
const RETRY_BUTTON_STYLE = {
|
|
374
|
+
padding: '0.4rem 0.9rem',
|
|
375
|
+
background: 'var(--theme-success-500, #16a34a)',
|
|
376
|
+
color: '#fff',
|
|
377
|
+
border: 'none',
|
|
378
|
+
borderRadius: '4px',
|
|
379
|
+
fontSize: '0.8rem',
|
|
380
|
+
cursor: 'pointer',
|
|
381
|
+
fontWeight: 500
|
|
382
|
+
};
|
|
383
|
+
const KEPT_AS_IS_GROUP_STYLE = {
|
|
384
|
+
padding: '0.6rem 0',
|
|
385
|
+
borderBottom: '1px solid var(--theme-elevation-100)',
|
|
386
|
+
fontSize: '0.8rem'
|
|
387
|
+
};
|
|
388
|
+
const KEPT_AS_IS_VALUE_STYLE = {
|
|
389
|
+
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
|
390
|
+
color: 'var(--theme-elevation-1000)',
|
|
391
|
+
fontWeight: 600
|
|
392
|
+
};
|
|
393
|
+
const KEPT_AS_IS_META_STYLE = {
|
|
394
|
+
color: 'var(--theme-elevation-600)',
|
|
395
|
+
marginTop: '0.2rem'
|
|
396
|
+
};
|
|
397
|
+
const KeptAsIsSection = ({ basePath, row, softSkipped, surfaceFields })=>{
|
|
398
|
+
const groups = useMemo(()=>groupSoftSkipsByValue(softSkipped), [
|
|
399
|
+
softSkipped
|
|
400
|
+
]);
|
|
401
|
+
return /*#__PURE__*/ _jsxs("section", {
|
|
402
|
+
style: DETAIL_SECTION_STYLE,
|
|
403
|
+
children: [
|
|
404
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
405
|
+
style: DETAIL_LABEL_STYLE,
|
|
406
|
+
children: [
|
|
407
|
+
"Kept as English (",
|
|
408
|
+
groups.length,
|
|
409
|
+
" ",
|
|
410
|
+
groups.length === 1 ? 'value' : 'values',
|
|
411
|
+
")"
|
|
412
|
+
]
|
|
413
|
+
}),
|
|
414
|
+
/*#__PURE__*/ _jsx("p", {
|
|
415
|
+
style: {
|
|
416
|
+
margin: '0 0 0.5rem',
|
|
417
|
+
fontSize: '0.75rem',
|
|
418
|
+
color: 'var(--theme-elevation-600)'
|
|
419
|
+
},
|
|
420
|
+
children: "The AI returned these values unchanged from the source language. Usually a brand name, term, or short label that shouldn’t translate. Open the document on the relevant locale to confirm — or edit manually if a translation was actually needed."
|
|
421
|
+
}),
|
|
422
|
+
/*#__PURE__*/ _jsx("ul", {
|
|
423
|
+
style: {
|
|
424
|
+
...DETAIL_LIST_STYLE,
|
|
425
|
+
padding: 0
|
|
426
|
+
},
|
|
427
|
+
children: groups.map((g, gi)=>{
|
|
428
|
+
const message = g.reasonCode ? editorMessageFor(g.reasonCode, {
|
|
429
|
+
locale: g.locales[0] ?? ''
|
|
430
|
+
}) : null;
|
|
431
|
+
const breadcrumbs = g.paths.map((p)=>resolveFieldBreadcrumb({
|
|
432
|
+
path: p,
|
|
433
|
+
fields: surfaceFields
|
|
434
|
+
}));
|
|
435
|
+
const groupKey = g.sourceValue ? `v:${g.sourceValue}:${gi}` : `p:${g.paths.join('|')}:${gi}`;
|
|
436
|
+
return /*#__PURE__*/ _jsxs("li", {
|
|
437
|
+
style: KEPT_AS_IS_GROUP_STYLE,
|
|
438
|
+
children: [
|
|
439
|
+
g.sourceValue ? /*#__PURE__*/ _jsx("div", {
|
|
440
|
+
children: /*#__PURE__*/ _jsxs("span", {
|
|
441
|
+
style: KEPT_AS_IS_VALUE_STYLE,
|
|
442
|
+
children: [
|
|
443
|
+
"“",
|
|
444
|
+
g.sourceValue,
|
|
445
|
+
"”"
|
|
446
|
+
]
|
|
447
|
+
})
|
|
448
|
+
}) : /*#__PURE__*/ _jsx("div", {
|
|
449
|
+
style: {
|
|
450
|
+
color: 'var(--theme-elevation-700)'
|
|
451
|
+
},
|
|
452
|
+
children: /*#__PURE__*/ _jsx("em", {
|
|
453
|
+
children: "(source value not recorded — legacy run)"
|
|
454
|
+
})
|
|
455
|
+
}),
|
|
456
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
457
|
+
style: KEPT_AS_IS_META_STYLE,
|
|
458
|
+
children: [
|
|
459
|
+
"Field:",
|
|
460
|
+
' ',
|
|
461
|
+
breadcrumbs.map((crumbs, ci)=>/*#__PURE__*/ _jsxs("span", {
|
|
462
|
+
children: [
|
|
463
|
+
ci > 0 && /*#__PURE__*/ _jsx("span", {
|
|
464
|
+
style: {
|
|
465
|
+
color: 'var(--theme-elevation-400)'
|
|
466
|
+
},
|
|
467
|
+
children: ' · '
|
|
468
|
+
}),
|
|
469
|
+
/*#__PURE__*/ _jsx("span", {
|
|
470
|
+
style: {
|
|
471
|
+
color: 'var(--theme-elevation-800)'
|
|
472
|
+
},
|
|
473
|
+
children: crumbs.join(' › ')
|
|
474
|
+
})
|
|
475
|
+
]
|
|
476
|
+
}, g.paths[ci]))
|
|
477
|
+
]
|
|
478
|
+
}),
|
|
479
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
480
|
+
style: KEPT_AS_IS_META_STYLE,
|
|
481
|
+
children: [
|
|
482
|
+
"Kept in ",
|
|
483
|
+
g.locales.length,
|
|
484
|
+
" ",
|
|
485
|
+
g.locales.length === 1 ? 'locale' : 'locales',
|
|
486
|
+
":",
|
|
487
|
+
' ',
|
|
488
|
+
g.locales.map((loc, li)=>{
|
|
489
|
+
const href = docHref(basePath, row, loc);
|
|
490
|
+
return /*#__PURE__*/ _jsxs("span", {
|
|
491
|
+
children: [
|
|
492
|
+
li > 0 && /*#__PURE__*/ _jsx("span", {
|
|
493
|
+
style: {
|
|
494
|
+
color: 'var(--theme-elevation-400)'
|
|
495
|
+
},
|
|
496
|
+
children: ', '
|
|
497
|
+
}),
|
|
498
|
+
href ? /*#__PURE__*/ _jsx("a", {
|
|
499
|
+
href: href,
|
|
500
|
+
rel: "noopener noreferrer",
|
|
501
|
+
style: {
|
|
502
|
+
color: 'var(--theme-success-700, #166534)',
|
|
503
|
+
textDecoration: 'underline dotted',
|
|
504
|
+
fontWeight: 500
|
|
505
|
+
},
|
|
506
|
+
target: "_blank",
|
|
507
|
+
title: `Open ${row.slug} on the ${loc} locale`,
|
|
508
|
+
children: loc
|
|
509
|
+
}) : /*#__PURE__*/ _jsx("span", {
|
|
510
|
+
style: {
|
|
511
|
+
color: 'var(--theme-elevation-800)'
|
|
512
|
+
},
|
|
513
|
+
children: loc
|
|
514
|
+
})
|
|
515
|
+
]
|
|
516
|
+
}, loc);
|
|
517
|
+
})
|
|
518
|
+
]
|
|
519
|
+
}),
|
|
520
|
+
message && /*#__PURE__*/ _jsx("div", {
|
|
521
|
+
style: {
|
|
522
|
+
...KEPT_AS_IS_META_STYLE,
|
|
523
|
+
color: 'var(--theme-elevation-700)'
|
|
524
|
+
},
|
|
525
|
+
title: g.reason ?? message.body,
|
|
526
|
+
children: message.body
|
|
527
|
+
})
|
|
528
|
+
]
|
|
529
|
+
}, groupKey);
|
|
530
|
+
})
|
|
531
|
+
})
|
|
532
|
+
]
|
|
533
|
+
});
|
|
534
|
+
};
|
|
535
|
+
const RowDetail = ({ basePath, row, rowStatus, isRetrying, retryError, onRetry })=>{
|
|
536
|
+
const targetLocales = row.targetLocales ?? [];
|
|
537
|
+
const softSkipped = row.softSkippedFields ?? [];
|
|
538
|
+
const preserved = row.preservedFields ?? [];
|
|
539
|
+
const { config } = useConfig();
|
|
540
|
+
// Look up the field schema for this surface so the breadcrumb resolver
|
|
541
|
+
// can turn `sidebar.featured_sidebar_items.3.label` into
|
|
542
|
+
// `Sidebar Navigation › Featured Sidebar Items › #4 › Label`. Falls
|
|
543
|
+
// back to a titleized-path render when the schema can't be resolved
|
|
544
|
+
// (deleted collection, plugin not registered for this surface).
|
|
545
|
+
const surfaceFields = useMemo(()=>{
|
|
546
|
+
if (!config) return [];
|
|
547
|
+
if (row.kind === 'global') {
|
|
548
|
+
const g = (config.globals ?? []).find((x)=>x?.slug === row.slug);
|
|
549
|
+
return g?.fields ?? [];
|
|
550
|
+
}
|
|
551
|
+
const c = (config.collections ?? []).find((x)=>x?.slug === row.slug);
|
|
552
|
+
return c?.fields ?? [];
|
|
553
|
+
}, [
|
|
554
|
+
config,
|
|
555
|
+
row.kind,
|
|
556
|
+
row.slug
|
|
557
|
+
]);
|
|
558
|
+
// Group soft-skipped and preserved by locale so the detail reads
|
|
559
|
+
// "per-locale" rather than as flat lists. Editors think in locales.
|
|
560
|
+
const softByLocale = new Map();
|
|
561
|
+
for (const s of softSkipped){
|
|
562
|
+
const arr = softByLocale.get(s.locale) ?? [];
|
|
563
|
+
arr.push(s);
|
|
564
|
+
softByLocale.set(s.locale, arr);
|
|
565
|
+
}
|
|
566
|
+
const preservedByLocale = new Map();
|
|
567
|
+
for (const p of preserved){
|
|
568
|
+
const arr = preservedByLocale.get(p.locale) ?? [];
|
|
569
|
+
arr.push(p);
|
|
570
|
+
preservedByLocale.set(p.locale, arr);
|
|
571
|
+
}
|
|
572
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
573
|
+
children: [
|
|
574
|
+
targetLocales.length > 0 && /*#__PURE__*/ _jsxs("section", {
|
|
575
|
+
style: DETAIL_SECTION_STYLE,
|
|
576
|
+
children: [
|
|
577
|
+
/*#__PURE__*/ _jsx("div", {
|
|
578
|
+
style: DETAIL_LABEL_STYLE,
|
|
579
|
+
children: "Per-locale outcome"
|
|
580
|
+
}),
|
|
581
|
+
/*#__PURE__*/ _jsx("ul", {
|
|
582
|
+
style: DETAIL_LIST_STYLE,
|
|
583
|
+
children: targetLocales.map((tl)=>{
|
|
584
|
+
const failedFieldPaths = tl.failedFields?.map((ff)=>ff.path) ?? [];
|
|
585
|
+
const localeSoftSkipped = softByLocale.get(tl.locale) ?? [];
|
|
586
|
+
const localePreserved = preservedByLocale.get(tl.locale) ?? [];
|
|
587
|
+
// Derive per-locale display status:
|
|
588
|
+
// 1. If this specific locale failed → 'failed'
|
|
589
|
+
// 2. If this locale has soft-skipped fields → 'needs-review'
|
|
590
|
+
// 3. Otherwise inherit the row's derived status
|
|
591
|
+
// Avoids the prior bug where each locale just echoed
|
|
592
|
+
// "succeeded" even on a no-changes / preserved / needs-review run.
|
|
593
|
+
let localeStatus = rowStatus;
|
|
594
|
+
if (tl.status === 'failed' || failedFieldPaths.length > 0) {
|
|
595
|
+
localeStatus = 'failed';
|
|
596
|
+
} else if (localeSoftSkipped.length > 0) {
|
|
597
|
+
localeStatus = 'needs-review';
|
|
598
|
+
}
|
|
599
|
+
return /*#__PURE__*/ _jsxs("li", {
|
|
600
|
+
style: {
|
|
601
|
+
padding: '0.5rem 0',
|
|
602
|
+
borderBottom: '1px solid var(--theme-elevation-100)'
|
|
603
|
+
},
|
|
604
|
+
children: [
|
|
605
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
606
|
+
style: {
|
|
607
|
+
display: 'flex',
|
|
608
|
+
gap: '0.5rem',
|
|
609
|
+
alignItems: 'baseline'
|
|
610
|
+
},
|
|
611
|
+
children: [
|
|
612
|
+
(()=>{
|
|
613
|
+
const localeHref = docHref(basePath, row, tl.locale);
|
|
614
|
+
const localeStrong = /*#__PURE__*/ _jsx("strong", {
|
|
615
|
+
style: {
|
|
616
|
+
fontFamily: 'monospace',
|
|
617
|
+
color: 'var(--theme-elevation-1000)'
|
|
618
|
+
},
|
|
619
|
+
children: tl.locale
|
|
620
|
+
});
|
|
621
|
+
return localeHref ? /*#__PURE__*/ _jsx("a", {
|
|
622
|
+
href: localeHref,
|
|
623
|
+
rel: "noopener noreferrer",
|
|
624
|
+
style: {
|
|
625
|
+
color: 'var(--theme-success-500)',
|
|
626
|
+
textDecoration: 'none'
|
|
627
|
+
},
|
|
628
|
+
target: "_blank",
|
|
629
|
+
title: `Open ${row.slug} in ${tl.locale}`,
|
|
630
|
+
children: localeStrong
|
|
631
|
+
}) : localeStrong;
|
|
632
|
+
})(),
|
|
633
|
+
/*#__PURE__*/ _jsx("span", {
|
|
634
|
+
style: {
|
|
635
|
+
color: DERIVED_STATUS_COLOR[localeStatus],
|
|
636
|
+
fontSize: '0.75rem',
|
|
637
|
+
fontWeight: 600
|
|
638
|
+
},
|
|
639
|
+
children: DERIVED_STATUS_LABEL[localeStatus]
|
|
640
|
+
})
|
|
641
|
+
]
|
|
642
|
+
}),
|
|
643
|
+
(tl.errorCode || tl.error) && /*#__PURE__*/ _jsx("div", {
|
|
644
|
+
style: {
|
|
645
|
+
marginTop: '0.35rem'
|
|
646
|
+
},
|
|
647
|
+
children: /*#__PURE__*/ _jsx(EditorError, {
|
|
648
|
+
code: tl.errorCode ?? undefined,
|
|
649
|
+
compact: true,
|
|
650
|
+
context: {
|
|
651
|
+
locale: tl.locale
|
|
652
|
+
},
|
|
653
|
+
details: tl.error ?? undefined,
|
|
654
|
+
hideAction: true
|
|
655
|
+
})
|
|
656
|
+
}),
|
|
657
|
+
failedFieldPaths.length > 0 && /*#__PURE__*/ _jsxs("div", {
|
|
658
|
+
style: {
|
|
659
|
+
marginTop: '0.25rem',
|
|
660
|
+
fontSize: '0.75rem'
|
|
661
|
+
},
|
|
662
|
+
children: [
|
|
663
|
+
/*#__PURE__*/ _jsx("span", {
|
|
664
|
+
style: {
|
|
665
|
+
color: 'var(--theme-elevation-500)'
|
|
666
|
+
},
|
|
667
|
+
children: "Failed fields: "
|
|
668
|
+
}),
|
|
669
|
+
failedFieldPaths.map((p, i)=>/*#__PURE__*/ _jsxs("code", {
|
|
670
|
+
style: {
|
|
671
|
+
color: 'var(--theme-elevation-800)',
|
|
672
|
+
marginRight: '0.25rem'
|
|
673
|
+
},
|
|
674
|
+
children: [
|
|
675
|
+
p,
|
|
676
|
+
i < failedFieldPaths.length - 1 ? ',' : ''
|
|
677
|
+
]
|
|
678
|
+
}, p))
|
|
679
|
+
]
|
|
680
|
+
}),
|
|
681
|
+
localeSoftSkipped.length > 0 && /*#__PURE__*/ _jsxs("div", {
|
|
682
|
+
style: {
|
|
683
|
+
marginTop: '0.25rem',
|
|
684
|
+
fontSize: '0.75rem',
|
|
685
|
+
color: 'var(--theme-elevation-600)'
|
|
686
|
+
},
|
|
687
|
+
children: [
|
|
688
|
+
localeSoftSkipped.length,
|
|
689
|
+
" field",
|
|
690
|
+
localeSoftSkipped.length === 1 ? '' : 's',
|
|
691
|
+
" the AI didn’t translate — see “Kept as English” below."
|
|
692
|
+
]
|
|
693
|
+
}),
|
|
694
|
+
localePreserved.length > 0 && /*#__PURE__*/ _jsxs("div", {
|
|
695
|
+
style: {
|
|
696
|
+
marginTop: '0.25rem',
|
|
697
|
+
fontSize: '0.75rem'
|
|
698
|
+
},
|
|
699
|
+
children: [
|
|
700
|
+
/*#__PURE__*/ _jsx("span", {
|
|
701
|
+
style: {
|
|
702
|
+
color: 'var(--theme-elevation-700)'
|
|
703
|
+
},
|
|
704
|
+
children: "Preserved (manual edits):"
|
|
705
|
+
}),
|
|
706
|
+
' ',
|
|
707
|
+
localePreserved.map((p, i)=>/*#__PURE__*/ _jsxs("code", {
|
|
708
|
+
style: {
|
|
709
|
+
color: 'var(--theme-elevation-800)',
|
|
710
|
+
marginRight: '0.25rem'
|
|
711
|
+
},
|
|
712
|
+
children: [
|
|
713
|
+
p.path,
|
|
714
|
+
i < localePreserved.length - 1 ? ',' : ''
|
|
715
|
+
]
|
|
716
|
+
}, p.path))
|
|
717
|
+
]
|
|
718
|
+
})
|
|
719
|
+
]
|
|
720
|
+
}, tl.locale);
|
|
721
|
+
})
|
|
722
|
+
})
|
|
723
|
+
]
|
|
724
|
+
}),
|
|
725
|
+
softSkipped.length > 0 && /*#__PURE__*/ _jsx(KeptAsIsSection, {
|
|
726
|
+
basePath: basePath,
|
|
727
|
+
row: row,
|
|
728
|
+
softSkipped: softSkipped,
|
|
729
|
+
surfaceFields: surfaceFields
|
|
730
|
+
}),
|
|
731
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
732
|
+
style: {
|
|
733
|
+
display: 'flex',
|
|
734
|
+
gap: '0.75rem',
|
|
735
|
+
alignItems: 'center',
|
|
736
|
+
marginTop: '0.5rem'
|
|
737
|
+
},
|
|
738
|
+
children: [
|
|
739
|
+
/*#__PURE__*/ _jsx("button", {
|
|
740
|
+
disabled: isRetrying,
|
|
741
|
+
onClick: (e)=>{
|
|
742
|
+
e.stopPropagation();
|
|
743
|
+
onRetry();
|
|
744
|
+
},
|
|
745
|
+
style: {
|
|
746
|
+
...RETRY_BUTTON_STYLE,
|
|
747
|
+
opacity: isRetrying ? 0.6 : 1,
|
|
748
|
+
cursor: isRetrying ? 'wait' : 'pointer'
|
|
749
|
+
},
|
|
750
|
+
type: "button",
|
|
751
|
+
children: isRetrying ? 'Retrying…' : 'Retry this translation'
|
|
752
|
+
}),
|
|
753
|
+
retryError && /*#__PURE__*/ _jsx("span", {
|
|
754
|
+
style: {
|
|
755
|
+
color: 'var(--theme-error-800, #7f1d1d)',
|
|
756
|
+
fontSize: '0.75rem'
|
|
757
|
+
},
|
|
758
|
+
children: retryError
|
|
759
|
+
})
|
|
760
|
+
]
|
|
761
|
+
})
|
|
762
|
+
]
|
|
763
|
+
});
|
|
764
|
+
};
|
|
765
|
+
const UsageRowItem = ({ row: r, basePath, pricing, isExpanded, isRetrying, retryError, onToggleExpand, onRetry })=>{
|
|
766
|
+
const href = docHref(basePath, r);
|
|
767
|
+
const breakdown = deriveBreakdown(r);
|
|
768
|
+
const derivedStatus = deriveStatus(r, breakdown);
|
|
769
|
+
const fieldsLabel = fieldsLabelFor(derivedStatus, breakdown);
|
|
770
|
+
const statusLabel = DERIVED_STATUS_LABEL[derivedStatus];
|
|
771
|
+
const statusColor = DERIVED_STATUS_COLOR[derivedStatus];
|
|
772
|
+
const statusTitle = statusTooltipFor(derivedStatus, breakdown);
|
|
773
|
+
const hasDetail = rowHasDetail(r, derivedStatus, breakdown);
|
|
774
|
+
const onRowClick = makeRowClickHandler(hasDetail, onToggleExpand, r.id);
|
|
775
|
+
const docLabel = /*#__PURE__*/ _jsxs(_Fragment, {
|
|
776
|
+
children: [
|
|
777
|
+
/*#__PURE__*/ _jsx("code", {
|
|
778
|
+
style: {
|
|
779
|
+
fontSize: '0.75rem'
|
|
780
|
+
},
|
|
781
|
+
children: r.slug
|
|
782
|
+
}),
|
|
783
|
+
r.documentId && r.kind === 'collection' && /*#__PURE__*/ _jsxs("span", {
|
|
784
|
+
style: {
|
|
785
|
+
color: 'var(--theme-elevation-500)'
|
|
786
|
+
},
|
|
787
|
+
children: [
|
|
788
|
+
" #",
|
|
789
|
+
r.documentId
|
|
790
|
+
]
|
|
791
|
+
})
|
|
792
|
+
]
|
|
793
|
+
});
|
|
794
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
795
|
+
children: [
|
|
796
|
+
/*#__PURE__*/ _jsxs("tr", {
|
|
797
|
+
onClick: onRowClick,
|
|
798
|
+
style: hasDetail ? {
|
|
799
|
+
cursor: 'pointer',
|
|
800
|
+
background: isExpanded ? 'var(--theme-elevation-100)' : undefined
|
|
801
|
+
} : undefined,
|
|
802
|
+
children: [
|
|
803
|
+
/*#__PURE__*/ _jsxs("td", {
|
|
804
|
+
style: TD_STYLE,
|
|
805
|
+
children: [
|
|
806
|
+
hasDetail && /*#__PURE__*/ _jsx("span", {
|
|
807
|
+
"aria-hidden": "true",
|
|
808
|
+
style: {
|
|
809
|
+
display: 'inline-block',
|
|
810
|
+
width: '1rem',
|
|
811
|
+
color: 'var(--theme-elevation-500)',
|
|
812
|
+
fontSize: '0.65rem',
|
|
813
|
+
transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
|
814
|
+
transition: 'transform 80ms ease',
|
|
815
|
+
marginRight: '0.25rem'
|
|
816
|
+
},
|
|
817
|
+
children: "▶"
|
|
818
|
+
}),
|
|
819
|
+
href ? /*#__PURE__*/ _jsx("a", {
|
|
820
|
+
href: href,
|
|
821
|
+
style: {
|
|
822
|
+
color: 'var(--theme-success-500)',
|
|
823
|
+
textDecoration: 'none'
|
|
824
|
+
},
|
|
825
|
+
title: `Open ${r.slug}${r.kind === 'collection' && r.documentId ? ` #${r.documentId}` : ''}`,
|
|
826
|
+
children: docLabel
|
|
827
|
+
}) : docLabel
|
|
828
|
+
]
|
|
829
|
+
}),
|
|
830
|
+
/*#__PURE__*/ _jsx("td", {
|
|
831
|
+
style: {
|
|
832
|
+
...TD_STYLE,
|
|
833
|
+
color: 'var(--theme-elevation-700)',
|
|
834
|
+
fontFamily: 'monospace',
|
|
835
|
+
fontSize: '0.75rem'
|
|
836
|
+
},
|
|
837
|
+
children: /*#__PURE__*/ _jsx(ModelCell, {
|
|
838
|
+
model: r.model
|
|
839
|
+
})
|
|
840
|
+
}),
|
|
841
|
+
/*#__PURE__*/ _jsx("td", {
|
|
842
|
+
style: {
|
|
843
|
+
...TD_STYLE,
|
|
844
|
+
color: statusColor
|
|
845
|
+
},
|
|
846
|
+
title: statusTitle,
|
|
847
|
+
children: statusLabel
|
|
848
|
+
}),
|
|
849
|
+
/*#__PURE__*/ _jsx("td", {
|
|
850
|
+
style: TD_STYLE,
|
|
851
|
+
children: /*#__PURE__*/ _jsx(UsageRowFieldsCell, {
|
|
852
|
+
breakdown: breakdown,
|
|
853
|
+
label: fieldsLabel,
|
|
854
|
+
row: r
|
|
855
|
+
})
|
|
856
|
+
}),
|
|
857
|
+
/*#__PURE__*/ _jsx("td", {
|
|
858
|
+
style: TD_STYLE,
|
|
859
|
+
children: formatCost(resolveCost(r, pricing))
|
|
860
|
+
}),
|
|
861
|
+
/*#__PURE__*/ _jsx("td", {
|
|
862
|
+
style: TD_STYLE,
|
|
863
|
+
children: formatDuration(r.durationMs)
|
|
864
|
+
}),
|
|
865
|
+
/*#__PURE__*/ _jsx("td", {
|
|
866
|
+
style: {
|
|
867
|
+
...TD_STYLE,
|
|
868
|
+
color: 'var(--theme-elevation-500)'
|
|
869
|
+
},
|
|
870
|
+
children: relTime(r.createdAt)
|
|
871
|
+
})
|
|
872
|
+
]
|
|
873
|
+
}),
|
|
874
|
+
isExpanded && hasDetail && /*#__PURE__*/ _jsx("tr", {
|
|
875
|
+
children: /*#__PURE__*/ _jsx("td", {
|
|
876
|
+
colSpan: 7,
|
|
877
|
+
style: {
|
|
878
|
+
...TD_STYLE,
|
|
879
|
+
background: 'var(--theme-elevation-50)',
|
|
880
|
+
whiteSpace: 'normal',
|
|
881
|
+
padding: '1rem 1.25rem',
|
|
882
|
+
borderTop: 'none'
|
|
883
|
+
},
|
|
884
|
+
children: /*#__PURE__*/ _jsx(RowDetail, {
|
|
885
|
+
basePath: basePath,
|
|
886
|
+
isRetrying: isRetrying,
|
|
887
|
+
onRetry: onRetry,
|
|
888
|
+
retryError: retryError,
|
|
889
|
+
row: r,
|
|
890
|
+
rowStatus: derivedStatus
|
|
891
|
+
})
|
|
892
|
+
})
|
|
893
|
+
})
|
|
894
|
+
]
|
|
895
|
+
});
|
|
896
|
+
};
|
|
897
|
+
// Fields-cell render — extracted so UsageRowItem's branching stays
|
|
898
|
+
// minimal. Handles both the breakdown case (current) and the legacy
|
|
899
|
+
// pre-1.1.14 fallback that only had token counts.
|
|
900
|
+
const UsageRowFieldsCell = ({ row, breakdown, label })=>{
|
|
901
|
+
if (!breakdown.hasBreakdown) {
|
|
902
|
+
return /*#__PURE__*/ _jsxs("span", {
|
|
903
|
+
style: {
|
|
904
|
+
color: 'var(--theme-elevation-500)'
|
|
905
|
+
},
|
|
906
|
+
children: [
|
|
907
|
+
fmtNum(row.inputTokens),
|
|
908
|
+
" / ",
|
|
909
|
+
fmtNum(row.outputTokens),
|
|
910
|
+
" tokens"
|
|
911
|
+
]
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
915
|
+
children: [
|
|
916
|
+
/*#__PURE__*/ _jsx("div", {
|
|
917
|
+
style: {
|
|
918
|
+
cursor: label.tooltip ? 'help' : undefined,
|
|
919
|
+
textDecoration: label.tooltip ? 'underline dotted var(--theme-elevation-300)' : undefined
|
|
920
|
+
},
|
|
921
|
+
title: label.tooltip || undefined,
|
|
922
|
+
children: label.primary
|
|
923
|
+
}),
|
|
924
|
+
(row.inputTokens > 0 || row.outputTokens > 0) && /*#__PURE__*/ _jsxs("div", {
|
|
925
|
+
style: {
|
|
926
|
+
color: 'var(--theme-elevation-400)',
|
|
927
|
+
fontSize: '0.7rem',
|
|
928
|
+
marginTop: '0.1rem'
|
|
929
|
+
},
|
|
930
|
+
children: [
|
|
931
|
+
fmtNum(row.inputTokens),
|
|
932
|
+
" in · ",
|
|
933
|
+
fmtNum(row.outputTokens),
|
|
934
|
+
" out"
|
|
935
|
+
]
|
|
936
|
+
})
|
|
937
|
+
]
|
|
938
|
+
});
|
|
939
|
+
};
|
|
940
|
+
// ---------------------------------------------------------------------------
|
|
941
|
+
// Main table component
|
|
942
|
+
// ---------------------------------------------------------------------------
|
|
943
|
+
export const UsageTable = ({ basePath })=>{
|
|
944
|
+
const [rows, setRows] = useState(null);
|
|
945
|
+
const [error, setError] = useState(null);
|
|
946
|
+
const [filter, setFilter] = useState('all');
|
|
947
|
+
// OpenRouter rows store `estimated_cost_usd = 0` because the smart
|
|
948
|
+
// provider doesn't compute cost (pricing varies per sub-model). We
|
|
949
|
+
// compute it client-side using the catalog's per-token prices for
|
|
950
|
+
// each model id we see in the rows.
|
|
951
|
+
const [pricing, setPricing] = useState(new Map());
|
|
952
|
+
// Per-row expansion state — set of row ids currently expanded.
|
|
953
|
+
// We don't auto-collapse on data refresh; users sometimes leave a row
|
|
954
|
+
// open while inspecting and the polling refresh shouldn't clobber it.
|
|
955
|
+
const [expandedIds, setExpandedIds] = useState(new Set());
|
|
956
|
+
const [retrying, setRetrying] = useState(new Set());
|
|
957
|
+
const [retryError, setRetryError] = useState({});
|
|
958
|
+
function toggleExpand(id) {
|
|
959
|
+
setExpandedIds((prev)=>{
|
|
960
|
+
const next = new Set(prev);
|
|
961
|
+
if (next.has(id)) {
|
|
962
|
+
next.delete(id);
|
|
963
|
+
} else {
|
|
964
|
+
next.add(id);
|
|
965
|
+
}
|
|
966
|
+
return next;
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Trigger a retry for a specific row's doc + locales. For collections
|
|
971
|
+
* we POST to `/api/{slug}/ai-translate` with `{ id, sourceLocale, targetLocales }`;
|
|
972
|
+
* for globals to `/api/globals/{slug}/ai-translate`. On success, refresh
|
|
973
|
+
* the usage table so the new row appears at the top.
|
|
974
|
+
*
|
|
975
|
+
* `locales` is optional — when omitted, retries ALL locales that
|
|
976
|
+
* appeared in the original row (so the failed-row default is "retry
|
|
977
|
+
* everything that ran last time"). When the editor selects a subset
|
|
978
|
+
* via the UI, only those locales are retried.
|
|
979
|
+
*/ async function retryRow(r, locales) {
|
|
980
|
+
const id = r.id;
|
|
981
|
+
setRetryError((prev)=>{
|
|
982
|
+
const { [id]: _ignored, ...rest } = prev;
|
|
983
|
+
return rest;
|
|
984
|
+
});
|
|
985
|
+
setRetrying((prev)=>new Set(prev).add(id));
|
|
986
|
+
try {
|
|
987
|
+
const targetLocales = locales && locales.length > 0 ? locales : (r.targetLocales ?? []).map((tl)=>tl.locale);
|
|
988
|
+
if (targetLocales.length === 0) {
|
|
989
|
+
throw new Error('No target locales recorded on this row.');
|
|
990
|
+
}
|
|
991
|
+
const endpoint = r.kind === 'collection' ? `${basePath}/api/${r.slug}/ai-translate` : `${basePath}/api/globals/${r.slug}/ai-translate`;
|
|
992
|
+
const body = r.kind === 'collection' ? {
|
|
993
|
+
id: r.documentId,
|
|
994
|
+
sourceLocale: r.sourceLocale,
|
|
995
|
+
targetLocales
|
|
996
|
+
} : {
|
|
997
|
+
sourceLocale: r.sourceLocale,
|
|
998
|
+
targetLocales
|
|
999
|
+
};
|
|
1000
|
+
const res = await fetch(endpoint, {
|
|
1001
|
+
method: 'POST',
|
|
1002
|
+
credentials: 'include',
|
|
1003
|
+
headers: {
|
|
1004
|
+
'content-type': 'application/json'
|
|
1005
|
+
},
|
|
1006
|
+
body: JSON.stringify(body)
|
|
1007
|
+
});
|
|
1008
|
+
if (!res.ok) {
|
|
1009
|
+
const text = await res.text();
|
|
1010
|
+
throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`);
|
|
1011
|
+
}
|
|
1012
|
+
// Refresh the table so the new row lands at the top.
|
|
1013
|
+
const usageRes = await fetch(`${basePath}/api/translation-usage?limit=20&sort=-createdAt&depth=0`, {
|
|
1014
|
+
credentials: 'include'
|
|
1015
|
+
});
|
|
1016
|
+
if (usageRes.ok) {
|
|
1017
|
+
const d = await usageRes.json();
|
|
1018
|
+
setRows(d.docs ?? []);
|
|
1019
|
+
}
|
|
1020
|
+
} catch (e) {
|
|
1021
|
+
setRetryError((prev)=>({
|
|
1022
|
+
...prev,
|
|
1023
|
+
[id]: e instanceof Error ? e.message : String(e)
|
|
1024
|
+
}));
|
|
1025
|
+
} finally{
|
|
1026
|
+
setRetrying((prev)=>{
|
|
1027
|
+
const next = new Set(prev);
|
|
1028
|
+
next.delete(id);
|
|
1029
|
+
return next;
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
useEffect(()=>{
|
|
1034
|
+
let cancelled = false;
|
|
1035
|
+
fetch(`${basePath}/api/translation-usage?limit=20&sort=-createdAt&depth=0`, {
|
|
1036
|
+
credentials: 'include'
|
|
1037
|
+
}).then(async (r)=>{
|
|
1038
|
+
if (!r.ok) {
|
|
1039
|
+
throw new Error(await readResponseError(r));
|
|
1040
|
+
}
|
|
1041
|
+
return await r.json();
|
|
1042
|
+
}).then((d)=>{
|
|
1043
|
+
if (cancelled) {
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
setRows(d.docs ?? []);
|
|
1047
|
+
}).catch((e)=>{
|
|
1048
|
+
if (cancelled) {
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
1052
|
+
});
|
|
1053
|
+
return ()=>{
|
|
1054
|
+
cancelled = true;
|
|
1055
|
+
};
|
|
1056
|
+
}, [
|
|
1057
|
+
basePath
|
|
1058
|
+
]);
|
|
1059
|
+
// Fetch model catalog for pricing lookup. Cheap (server-side cached).
|
|
1060
|
+
useEffect(()=>{
|
|
1061
|
+
let cancelled = false;
|
|
1062
|
+
fetch(`${basePath}/api/openrouter/models`, {
|
|
1063
|
+
credentials: 'include'
|
|
1064
|
+
}).then((r)=>r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))).then((d)=>{
|
|
1065
|
+
if (cancelled) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const map = new Map();
|
|
1069
|
+
for (const m of d.models ?? []){
|
|
1070
|
+
if (m.pricing) {
|
|
1071
|
+
map.set(m.id, {
|
|
1072
|
+
prompt: Number.parseFloat(m.pricing.prompt),
|
|
1073
|
+
completion: Number.parseFloat(m.pricing.completion)
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
setPricing(map);
|
|
1078
|
+
}).catch(()=>{
|
|
1079
|
+
// Non-fatal — Cost column just falls back to stored value.
|
|
1080
|
+
});
|
|
1081
|
+
return ()=>{
|
|
1082
|
+
cancelled = true;
|
|
1083
|
+
};
|
|
1084
|
+
}, [
|
|
1085
|
+
basePath
|
|
1086
|
+
]);
|
|
1087
|
+
const filtered = useMemo(()=>(rows ?? []).filter((r)=>filter === 'all' || r.status === filter), [
|
|
1088
|
+
rows,
|
|
1089
|
+
filter
|
|
1090
|
+
]);
|
|
1091
|
+
return /*#__PURE__*/ _jsxs("section", {
|
|
1092
|
+
style: SECTION_STYLE,
|
|
1093
|
+
children: [
|
|
1094
|
+
/*#__PURE__*/ _jsxs("header", {
|
|
1095
|
+
style: {
|
|
1096
|
+
display: 'flex',
|
|
1097
|
+
alignItems: 'center',
|
|
1098
|
+
justifyContent: 'space-between',
|
|
1099
|
+
marginBottom: '0.75rem',
|
|
1100
|
+
gap: '1rem'
|
|
1101
|
+
},
|
|
1102
|
+
children: [
|
|
1103
|
+
/*#__PURE__*/ _jsx("h2", {
|
|
1104
|
+
style: {
|
|
1105
|
+
margin: 0,
|
|
1106
|
+
fontSize: '1rem',
|
|
1107
|
+
color: 'var(--theme-elevation-1000)'
|
|
1108
|
+
},
|
|
1109
|
+
children: "Recent translations"
|
|
1110
|
+
}),
|
|
1111
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
1112
|
+
style: {
|
|
1113
|
+
display: 'flex',
|
|
1114
|
+
alignItems: 'center',
|
|
1115
|
+
gap: '0.75rem'
|
|
1116
|
+
},
|
|
1117
|
+
children: [
|
|
1118
|
+
/*#__PURE__*/ _jsxs("select", {
|
|
1119
|
+
onChange: (e)=>setFilter(e.target.value),
|
|
1120
|
+
style: {
|
|
1121
|
+
padding: '0.25rem 0.5rem',
|
|
1122
|
+
fontSize: '0.875rem',
|
|
1123
|
+
background: 'var(--theme-elevation-50)',
|
|
1124
|
+
border: '1px solid var(--theme-elevation-150)',
|
|
1125
|
+
borderRadius: '4px',
|
|
1126
|
+
color: 'var(--theme-elevation-1000)'
|
|
1127
|
+
},
|
|
1128
|
+
value: filter,
|
|
1129
|
+
children: [
|
|
1130
|
+
/*#__PURE__*/ _jsx("option", {
|
|
1131
|
+
value: "all",
|
|
1132
|
+
children: "All statuses"
|
|
1133
|
+
}),
|
|
1134
|
+
/*#__PURE__*/ _jsx("option", {
|
|
1135
|
+
value: "succeeded",
|
|
1136
|
+
children: "Succeeded only"
|
|
1137
|
+
}),
|
|
1138
|
+
/*#__PURE__*/ _jsx("option", {
|
|
1139
|
+
value: "failed",
|
|
1140
|
+
children: "Failed only"
|
|
1141
|
+
})
|
|
1142
|
+
]
|
|
1143
|
+
}),
|
|
1144
|
+
/*#__PURE__*/ _jsx("a", {
|
|
1145
|
+
href: filter === 'all' ? `${basePath}/admin/collections/translation-usage` : `${basePath}/admin/collections/translation-usage?where[status][equals]=${filter}`,
|
|
1146
|
+
style: {
|
|
1147
|
+
fontSize: '0.875rem',
|
|
1148
|
+
color: 'var(--theme-success-500)',
|
|
1149
|
+
textDecoration: 'none',
|
|
1150
|
+
whiteSpace: 'nowrap'
|
|
1151
|
+
},
|
|
1152
|
+
children: "View all →"
|
|
1153
|
+
})
|
|
1154
|
+
]
|
|
1155
|
+
})
|
|
1156
|
+
]
|
|
1157
|
+
}),
|
|
1158
|
+
error && /*#__PURE__*/ _jsx("p", {
|
|
1159
|
+
style: {
|
|
1160
|
+
color: 'var(--theme-error-800, #7f1d1d)',
|
|
1161
|
+
fontSize: '0.875rem',
|
|
1162
|
+
margin: '0 0 0.5rem'
|
|
1163
|
+
},
|
|
1164
|
+
children: error
|
|
1165
|
+
}),
|
|
1166
|
+
rows === null && /*#__PURE__*/ _jsx("p", {
|
|
1167
|
+
style: {
|
|
1168
|
+
margin: 0,
|
|
1169
|
+
color: 'var(--theme-elevation-500)',
|
|
1170
|
+
fontSize: '0.875rem'
|
|
1171
|
+
},
|
|
1172
|
+
children: "Loading…"
|
|
1173
|
+
}),
|
|
1174
|
+
rows !== null && filtered.length === 0 && /*#__PURE__*/ _jsx("p", {
|
|
1175
|
+
style: {
|
|
1176
|
+
margin: 0,
|
|
1177
|
+
color: 'var(--theme-elevation-500)',
|
|
1178
|
+
fontSize: '0.875rem'
|
|
1179
|
+
},
|
|
1180
|
+
children: rows.length === 0 ? 'No translations have run yet. Trigger one from any document.' : `No ${filter} translations in the last 20 runs.`
|
|
1181
|
+
}),
|
|
1182
|
+
rows !== null && filtered.length > 0 && /*#__PURE__*/ _jsx("div", {
|
|
1183
|
+
style: {
|
|
1184
|
+
overflowX: 'auto'
|
|
1185
|
+
},
|
|
1186
|
+
children: /*#__PURE__*/ _jsxs("table", {
|
|
1187
|
+
style: TABLE_STYLE,
|
|
1188
|
+
children: [
|
|
1189
|
+
/*#__PURE__*/ _jsx("thead", {
|
|
1190
|
+
children: /*#__PURE__*/ _jsxs("tr", {
|
|
1191
|
+
children: [
|
|
1192
|
+
/*#__PURE__*/ _jsx("th", {
|
|
1193
|
+
style: TH_STYLE,
|
|
1194
|
+
children: "Collection / doc"
|
|
1195
|
+
}),
|
|
1196
|
+
/*#__PURE__*/ _jsx("th", {
|
|
1197
|
+
style: TH_STYLE,
|
|
1198
|
+
children: "Model"
|
|
1199
|
+
}),
|
|
1200
|
+
/*#__PURE__*/ _jsx("th", {
|
|
1201
|
+
style: TH_STYLE,
|
|
1202
|
+
children: "Status"
|
|
1203
|
+
}),
|
|
1204
|
+
/*#__PURE__*/ _jsx("th", {
|
|
1205
|
+
style: TH_STYLE,
|
|
1206
|
+
children: "Fields"
|
|
1207
|
+
}),
|
|
1208
|
+
/*#__PURE__*/ _jsx("th", {
|
|
1209
|
+
style: TH_STYLE,
|
|
1210
|
+
children: "Cost"
|
|
1211
|
+
}),
|
|
1212
|
+
/*#__PURE__*/ _jsx("th", {
|
|
1213
|
+
style: TH_STYLE,
|
|
1214
|
+
children: "Duration"
|
|
1215
|
+
}),
|
|
1216
|
+
/*#__PURE__*/ _jsx("th", {
|
|
1217
|
+
style: TH_STYLE,
|
|
1218
|
+
children: "When"
|
|
1219
|
+
})
|
|
1220
|
+
]
|
|
1221
|
+
})
|
|
1222
|
+
}),
|
|
1223
|
+
/*#__PURE__*/ _jsx("tbody", {
|
|
1224
|
+
children: filtered.map((r)=>/*#__PURE__*/ _jsx(UsageRowItem, {
|
|
1225
|
+
basePath: basePath,
|
|
1226
|
+
isExpanded: expandedIds.has(r.id),
|
|
1227
|
+
isRetrying: retrying.has(r.id),
|
|
1228
|
+
onRetry: (locales)=>retryRow(r, locales),
|
|
1229
|
+
onToggleExpand: toggleExpand,
|
|
1230
|
+
pricing: pricing,
|
|
1231
|
+
retryError: retryError[r.id],
|
|
1232
|
+
row: r
|
|
1233
|
+
}, r.id))
|
|
1234
|
+
})
|
|
1235
|
+
]
|
|
1236
|
+
})
|
|
1237
|
+
})
|
|
1238
|
+
]
|
|
1239
|
+
});
|
|
1240
|
+
};
|