@proofhound/web-ui 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hooks/annotation.d.ts +37 -16
- package/dist/hooks/annotation.d.ts.map +1 -1
- package/dist/hooks/canary-release.d.ts +62 -37
- package/dist/hooks/canary-release.d.ts.map +1 -1
- package/dist/hooks/dataset.d.ts +101 -0
- package/dist/hooks/dataset.d.ts.map +1 -1
- package/dist/hooks/dataset.js +27 -0
- package/dist/hooks/dataset.js.map +1 -1
- package/dist/hooks/optimization.d.ts +1 -1
- package/dist/hooks/production-release.d.ts +8 -4
- package/dist/hooks/production-release.d.ts.map +1 -1
- package/dist/hooks/prompt.d.ts +149 -38
- package/dist/hooks/prompt.d.ts.map +1 -1
- package/dist/hooks/prompt.js +20 -0
- package/dist/hooks/prompt.js.map +1 -1
- package/dist/hooks/release-line.d.ts +2522 -72
- package/dist/hooks/release-line.d.ts.map +1 -1
- package/dist/hooks/release-line.js +125 -0
- package/dist/hooks/release-line.js.map +1 -1
- package/dist/hooks/run-result.d.ts +9 -6
- package/dist/hooks/run-result.d.ts.map +1 -1
- package/dist/hooks/run-result.js +2 -1
- package/dist/hooks/run-result.js.map +1 -1
- package/dist/i18n/index.d.ts +652 -160
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +652 -160
- package/dist/i18n/index.js.map +1 -1
- package/dist/lib/releases/release-line-model.d.ts +8 -2
- package/dist/lib/releases/release-line-model.d.ts.map +1 -1
- package/dist/lib/releases/release-line-model.js +66 -29
- package/dist/lib/releases/release-line-model.js.map +1 -1
- package/dist/screens/annotations/annotation-detail-page.js +1 -1
- package/dist/screens/annotations/annotation-new-page.d.ts.map +1 -1
- package/dist/screens/annotations/annotation-new-page.js +213 -49
- package/dist/screens/annotations/annotation-new-page.js.map +1 -1
- package/dist/screens/annotations/annotation-task-model.d.ts +3 -2
- package/dist/screens/annotations/annotation-task-model.d.ts.map +1 -1
- package/dist/screens/annotations/annotation-task-model.js +5 -4
- package/dist/screens/annotations/annotation-task-model.js.map +1 -1
- package/dist/screens/annotations/annotation-ui.d.ts.map +1 -1
- package/dist/screens/annotations/annotation-ui.js +9 -4
- package/dist/screens/annotations/annotation-ui.js.map +1 -1
- package/dist/screens/annotations/annotations-list-page.js +1 -1
- package/dist/screens/connectors/connector-detail-page.js +2 -2
- package/dist/screens/connectors/connector-detail-page.js.map +1 -1
- package/dist/screens/connectors/connector-form-page.js +1 -1
- package/dist/screens/connectors/connector-form-page.js.map +1 -1
- package/dist/screens/connectors/connector-ui.d.ts +6 -0
- package/dist/screens/connectors/connector-ui.d.ts.map +1 -1
- package/dist/screens/connectors/connector-ui.js +7 -1
- package/dist/screens/connectors/connector-ui.js.map +1 -1
- package/dist/screens/connectors/connectors-list-page.d.ts.map +1 -1
- package/dist/screens/connectors/connectors-list-page.js +5 -5
- package/dist/screens/connectors/connectors-list-page.js.map +1 -1
- package/dist/screens/dashboard/dashboard-screen.d.ts.map +1 -1
- package/dist/screens/dashboard/dashboard-screen.js +27 -15
- package/dist/screens/dashboard/dashboard-screen.js.map +1 -1
- package/dist/screens/datasets/dataset-mappers.js +1 -1
- package/dist/screens/datasets/dataset-mappers.js.map +1 -1
- package/dist/screens/datasets/dataset-types.d.ts +1 -1
- package/dist/screens/datasets/dataset-types.d.ts.map +1 -1
- package/dist/screens/datasets/dataset-ui.d.ts +1 -1
- package/dist/screens/datasets/dataset-ui.d.ts.map +1 -1
- package/dist/screens/datasets/dataset-ui.js +2 -2
- package/dist/screens/datasets/dataset-ui.js.map +1 -1
- package/dist/screens/datasets/datasets-list-page.d.ts.map +1 -1
- package/dist/screens/datasets/datasets-list-page.js +35 -24
- package/dist/screens/datasets/datasets-list-page.js.map +1 -1
- package/dist/screens/experiments/experiment-detail-page.js +1 -1
- package/dist/screens/experiments/experiment-detail-page.js.map +1 -1
- package/dist/screens/experiments/run-result-labels.d.ts.map +1 -1
- package/dist/screens/experiments/run-result-labels.js +3 -4
- package/dist/screens/experiments/run-result-labels.js.map +1 -1
- package/dist/screens/prompts/prompt-detail-page.d.ts.map +1 -1
- package/dist/screens/prompts/prompt-detail-page.js +7 -8
- package/dist/screens/prompts/prompt-detail-page.js.map +1 -1
- package/dist/screens/prompts/prompt-model.d.ts +5 -2
- package/dist/screens/prompts/prompt-model.d.ts.map +1 -1
- package/dist/screens/prompts/prompt-model.js +3 -1
- package/dist/screens/prompts/prompt-model.js.map +1 -1
- package/dist/screens/prompts/prompts-list-page.d.ts.map +1 -1
- package/dist/screens/prompts/prompts-list-page.js +44 -20
- package/dist/screens/prompts/prompts-list-page.js.map +1 -1
- package/dist/screens/releases/release-input-route-editor.d.ts +39 -0
- package/dist/screens/releases/release-input-route-editor.d.ts.map +1 -0
- package/dist/screens/releases/release-input-route-editor.js +355 -0
- package/dist/screens/releases/release-input-route-editor.js.map +1 -0
- package/dist/screens/releases/release-line-detail-page.d.ts +62 -0
- package/dist/screens/releases/release-line-detail-page.d.ts.map +1 -1
- package/dist/screens/releases/release-line-detail-page.js +1877 -323
- package/dist/screens/releases/release-line-detail-page.js.map +1 -1
- package/dist/screens/releases/release-line-ui.d.ts.map +1 -1
- package/dist/screens/releases/release-line-ui.js +55 -39
- package/dist/screens/releases/release-line-ui.js.map +1 -1
- package/dist/screens/releases/release-new-model.d.ts.map +1 -1
- package/dist/screens/releases/release-new-model.js +1 -6
- package/dist/screens/releases/release-new-model.js.map +1 -1
- package/dist/screens/releases/release-new-page.d.ts.map +1 -1
- package/dist/screens/releases/release-new-page.js +101 -66
- package/dist/screens/releases/release-new-page.js.map +1 -1
- package/dist/screens/releases/release-topology-canvas.d.ts +11 -2
- package/dist/screens/releases/release-topology-canvas.d.ts.map +1 -1
- package/dist/screens/releases/release-topology-canvas.js +1015 -174
- package/dist/screens/releases/release-topology-canvas.js.map +1 -1
- package/dist/screens/releases/releases-list-page.d.ts.map +1 -1
- package/dist/screens/releases/releases-list-page.js +81 -32
- package/dist/screens/releases/releases-list-page.js.map +1 -1
- package/package.json +5 -4
|
@@ -1,44 +1,49 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
5
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
6
6
|
import { useQueryClient } from '@tanstack/react-query';
|
|
7
|
-
import { Activity, AlertTriangle,
|
|
8
|
-
import
|
|
7
|
+
import { Activity, AlertTriangle, Archive, Check, ChevronDown, CircleDollarSign, Gauge, MoreHorizontal, Play, Plus, RotateCcw, ScrollText, Search, SlidersHorizontal, Square, Tag, Trash2, Timer, } from 'lucide-react';
|
|
8
|
+
import * as echarts from 'echarts/core';
|
|
9
|
+
import { LineChart } from 'echarts/charts';
|
|
10
|
+
import { DataZoomComponent, GridComponent, ToolboxComponent, TooltipComponent, } from 'echarts/components';
|
|
11
|
+
import { SVGRenderer } from 'echarts/renderers';
|
|
9
12
|
import { Main } from '@proofhound/ui/layout';
|
|
10
|
-
import { Button, DateRangeSegmented, resolveDateRangePreset, resolveRollingDateRangeValue, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, PlatformLoader, DetailPageSkeleton, ResourcePaginationFooter, Table, TableBody, TableCell, TableEmpty, TableHead, TableHeader, TableRow, cn, } from '@proofhound/ui';
|
|
11
|
-
import { useAnnotationTaskList } from '../../hooks';
|
|
13
|
+
import { Button, DateRangeSegmented, resolveDateRangePreset, resolveRollingDateRangeValue, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, PlatformLoader, Popover, PopoverContent, PopoverTrigger, DetailPageSkeleton, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, ResourcePaginationFooter, Table, TableBody, TableCell, TableEmpty, TableHead, TableHeader, TableRow, cn, } from '@proofhound/ui';
|
|
12
14
|
import { useDelayedLoading } from '../../hooks';
|
|
13
15
|
import { useProjectModels } from '../../hooks';
|
|
14
|
-
import { useProductionReleaseHistory,
|
|
16
|
+
import { useConnectors, useProductionReleaseHistory, useStopCanaryRelease } from '../../hooks';
|
|
15
17
|
import { useProjectMonitoringStats, useProjectMonitoringTimeseries } from '../../hooks';
|
|
16
|
-
import { useReleaseLineEvents, useReleaseLineList, useUpdateReleaseLineRunConfig, useUpdateReleaseLineTrafficRatio, } from '../../hooks';
|
|
18
|
+
import { useArchiveReleaseLine, usePromoteReleaseLineCanary, useDeleteReleaseLine, useReleaseLineDeleteImpact, useReleaseLineEvents, useReleaseLineList, useRestoreReleaseLineHistoryToCanary, useRestoreReleaseLineHistoryToProduction, useStartReleaseLine, useStopReleaseLine, useUnarchiveReleaseLine, useUpdateReleaseLineInputRoute, useUpdateReleaseLineOutputRoute, useUpdateReleaseLineRunConfig, useUpdateReleaseLineTrafficRatio, } from '../../hooks';
|
|
17
19
|
import { useReleaseRunResults } from '../../hooks';
|
|
18
20
|
import { AUTO_REFRESH_INTERVAL_MS, useAutoRefresh, useDateTimeFormatter } from '../../hooks';
|
|
19
21
|
import { useI18n } from '../../i18n';
|
|
20
|
-
import { getReleaseLineId, getReleaseStopConfirmationName } from '../../lib';
|
|
22
|
+
import { getApiErrorMessage, getReleaseLineId, getReleaseStopConfirmationName } from '../../lib';
|
|
21
23
|
import { BigChartCard } from '../monitoring/big-chart-card';
|
|
22
|
-
import {
|
|
24
|
+
import { formatCount, formatPercent } from './release-line-ui';
|
|
23
25
|
import { ReleaseTopologyCanvas } from './release-topology-canvas';
|
|
26
|
+
echarts.use([LineChart, GridComponent, TooltipComponent, DataZoomComponent, ToolboxComponent, SVGRenderer]);
|
|
24
27
|
const DETAIL_TABS = [
|
|
25
28
|
{ value: 'monitoring', key: 'releases.detail.tab.monitoring' },
|
|
26
|
-
{ value: 'variants', key: 'releases.detail.tab.variants' },
|
|
27
29
|
{ value: 'results', key: 'releases.detail.tab.results' },
|
|
28
30
|
{ value: 'quality', key: 'releases.detail.tab.quality' },
|
|
29
31
|
{ value: 'history', key: 'releases.detail.tab.history' },
|
|
32
|
+
{ value: 'settings', key: 'releases.detail.tab.settings' },
|
|
30
33
|
];
|
|
31
34
|
const RESULT_COLUMNS = [
|
|
32
35
|
{ key: 'externalId', width: 'normal' },
|
|
33
36
|
{ key: 'input', width: 'wide' },
|
|
34
37
|
{ key: 'output', width: 'wide' },
|
|
35
38
|
{ key: 'source', width: 'compact' },
|
|
36
|
-
{ key: '
|
|
39
|
+
{ key: 'version', width: 'normal' },
|
|
37
40
|
{ key: 'latency', width: 'compact' },
|
|
38
41
|
{ key: 'tokens', width: 'compact' },
|
|
39
42
|
{ key: 'createdAt', width: 'normal' },
|
|
40
43
|
];
|
|
41
44
|
const RESULT_PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
|
|
45
|
+
const HISTORY_INITIAL_GROUP_LIMIT = 8;
|
|
46
|
+
const HISTORY_GROUP_PAGE_SIZE = 8;
|
|
42
47
|
const EMPTY_BY_SOURCE = { prod: 0, canary: 0, iter: 0, exp: 0 };
|
|
43
48
|
const EMPTY_TIMESERIES_POINTS = [];
|
|
44
49
|
const RELEASE_MONITORING_SOURCE_KEYS = ['prod', 'canary'];
|
|
@@ -46,6 +51,21 @@ function useDateTimeOrDash() {
|
|
|
46
51
|
const { formatDateTime } = useDateTimeFormatter();
|
|
47
52
|
return useCallback((value) => (value ? formatDateTime(value, { fallback: '—' }) : '—'), [formatDateTime]);
|
|
48
53
|
}
|
|
54
|
+
const QUALITY_OVERALL_SCOPE = '__overall__';
|
|
55
|
+
const QUALITY_METRIC_OPTIONS = [
|
|
56
|
+
{ key: 'recall', labelKey: 'releases.detail.quality.metric.recall' },
|
|
57
|
+
{ key: 'precision', labelKey: 'releases.detail.quality.metric.precision' },
|
|
58
|
+
{ key: 'f1', labelKey: 'releases.detail.quality.metric.f1' },
|
|
59
|
+
{ key: 'accuracy', labelKey: 'releases.detail.quality.metric.accuracy' },
|
|
60
|
+
];
|
|
61
|
+
const QUALITY_SERIES_COLORS = [
|
|
62
|
+
'var(--primary)',
|
|
63
|
+
'var(--src-iter)',
|
|
64
|
+
'var(--status-pending-dot)',
|
|
65
|
+
'var(--destructive)',
|
|
66
|
+
'var(--foreground)',
|
|
67
|
+
'var(--muted-foreground)',
|
|
68
|
+
];
|
|
49
69
|
const COMPACT_METRIC_DOT_CLASS = {
|
|
50
70
|
default: 'bg-muted-foreground',
|
|
51
71
|
production: 'bg-[var(--src-prod-fg)]',
|
|
@@ -53,9 +73,6 @@ const COMPACT_METRIC_DOT_CLASS = {
|
|
|
53
73
|
success: 'bg-[var(--status-running-fg)]',
|
|
54
74
|
danger: 'bg-destructive',
|
|
55
75
|
};
|
|
56
|
-
const QUALITY_LINE_COLORS = {
|
|
57
|
-
score: 'var(--src-canary)',
|
|
58
|
-
};
|
|
59
76
|
function normalizeLineId(value) {
|
|
60
77
|
try {
|
|
61
78
|
return decodeURIComponent(value);
|
|
@@ -67,13 +84,17 @@ function normalizeLineId(value) {
|
|
|
67
84
|
function resolveTab(value) {
|
|
68
85
|
if (value === 'annotation')
|
|
69
86
|
return 'quality';
|
|
87
|
+
if (value === 'variants' || value === 'versions')
|
|
88
|
+
return 'history';
|
|
70
89
|
if (value === 'monitoring' ||
|
|
71
|
-
value === 'variants' ||
|
|
72
90
|
value === 'results' ||
|
|
73
91
|
value === 'quality' ||
|
|
74
|
-
value === 'history'
|
|
92
|
+
value === 'history' ||
|
|
93
|
+
value === 'settings') {
|
|
75
94
|
return value;
|
|
76
95
|
}
|
|
96
|
+
if (value === 'delete')
|
|
97
|
+
return 'settings';
|
|
77
98
|
return 'monitoring';
|
|
78
99
|
}
|
|
79
100
|
function createDefaultMonitoringRange() {
|
|
@@ -87,6 +108,17 @@ function createDefaultMonitoringRange() {
|
|
|
87
108
|
to: now.toISOString(),
|
|
88
109
|
};
|
|
89
110
|
}
|
|
111
|
+
function createDefaultResultDateRange() {
|
|
112
|
+
const preset = resolveDateRangePreset('d7');
|
|
113
|
+
if (preset)
|
|
114
|
+
return { preset: 'all', ...preset };
|
|
115
|
+
const now = new Date();
|
|
116
|
+
return {
|
|
117
|
+
preset: 'all',
|
|
118
|
+
from: new Date(now.getTime() - 7 * 24 * 60 * 60_000).toISOString(),
|
|
119
|
+
to: now.toISOString(),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
90
122
|
function getMonitoringRefreshInterval(preset) {
|
|
91
123
|
if (preset === 'h1')
|
|
92
124
|
return AUTO_REFRESH_INTERVAL_MS;
|
|
@@ -96,6 +128,12 @@ function getMonitoringRefreshInterval(preset) {
|
|
|
96
128
|
return 60_000;
|
|
97
129
|
return false;
|
|
98
130
|
}
|
|
131
|
+
function isResultDateRangeApplied(value) {
|
|
132
|
+
return value.preset !== 'all';
|
|
133
|
+
}
|
|
134
|
+
function isResultDateRangeRolling(value) {
|
|
135
|
+
return value.preset !== 'all' && value.preset !== 'custom';
|
|
136
|
+
}
|
|
99
137
|
function hasRunningRelease(line) {
|
|
100
138
|
return line?.production?.currentEvent?.status === 'running' || line?.canary?.status === 'running';
|
|
101
139
|
}
|
|
@@ -156,29 +194,261 @@ function timeValue(value) {
|
|
|
156
194
|
const parsed = Date.parse(value);
|
|
157
195
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
158
196
|
}
|
|
159
|
-
function
|
|
197
|
+
function latestTime(values) {
|
|
198
|
+
const latest = values
|
|
199
|
+
.filter((value) => Boolean(value))
|
|
200
|
+
.sort((left, right) => timeValue(right) - timeValue(left))[0];
|
|
201
|
+
return latest ?? null;
|
|
202
|
+
}
|
|
203
|
+
function qualityVersionLane(kind) {
|
|
204
|
+
return kind === 'production' ? 'production' : 'canary';
|
|
205
|
+
}
|
|
206
|
+
function isRecord(value) {
|
|
207
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
208
|
+
}
|
|
209
|
+
function stringFromQualityRecord(record, key) {
|
|
210
|
+
const value = record[key];
|
|
211
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
212
|
+
}
|
|
213
|
+
function numberFromQualityRecord(record, key) {
|
|
214
|
+
const value = record[key];
|
|
215
|
+
const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : Number.NaN;
|
|
216
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
217
|
+
return null;
|
|
218
|
+
if (parsed <= 1)
|
|
219
|
+
return toPercentPoint(parsed);
|
|
220
|
+
if (parsed <= 100)
|
|
221
|
+
return parsed;
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
function countFromQualityRecord(record, key) {
|
|
225
|
+
const value = record[key];
|
|
226
|
+
const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : Number.NaN;
|
|
227
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
228
|
+
return null;
|
|
229
|
+
return Math.round(parsed);
|
|
230
|
+
}
|
|
231
|
+
function readReleaseQualityMetricSet(value) {
|
|
232
|
+
if (!isRecord(value))
|
|
233
|
+
return null;
|
|
234
|
+
const recall = numberFromQualityRecord(value, 'recall');
|
|
235
|
+
const precision = numberFromQualityRecord(value, 'precision');
|
|
236
|
+
const f1 = numberFromQualityRecord(value, 'f1');
|
|
237
|
+
const accuracy = numberFromQualityRecord(value, 'accuracy');
|
|
238
|
+
if (recall === null || precision === null || f1 === null || accuracy === null)
|
|
239
|
+
return null;
|
|
240
|
+
const sampleCount = countFromQualityRecord(value, 'sampleCount');
|
|
241
|
+
return {
|
|
242
|
+
recall,
|
|
243
|
+
precision,
|
|
244
|
+
f1,
|
|
245
|
+
accuracy,
|
|
246
|
+
sampleCount,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function readReleaseQualityScopes(metrics, overallLabel) {
|
|
250
|
+
const quality = isRecord(metrics?.['quality']) ? metrics['quality'] : null;
|
|
251
|
+
if (!quality)
|
|
252
|
+
return [];
|
|
253
|
+
const scopes = [];
|
|
254
|
+
const overallMetrics = readReleaseQualityMetricSet(quality['overall']);
|
|
255
|
+
if (overallMetrics) {
|
|
256
|
+
scopes.push({ scope: QUALITY_OVERALL_SCOPE, label: overallLabel, metrics: overallMetrics });
|
|
257
|
+
}
|
|
258
|
+
const rawScopes = quality['scopes'];
|
|
259
|
+
if (Array.isArray(rawScopes)) {
|
|
260
|
+
for (const item of rawScopes) {
|
|
261
|
+
if (!isRecord(item))
|
|
262
|
+
continue;
|
|
263
|
+
const scope = stringFromQualityRecord(item, 'key') ?? stringFromQualityRecord(item, 'label');
|
|
264
|
+
if (!scope)
|
|
265
|
+
continue;
|
|
266
|
+
const metricSet = readReleaseQualityMetricSet(item['metrics']) ?? readReleaseQualityMetricSet(item);
|
|
267
|
+
if (!metricSet)
|
|
268
|
+
continue;
|
|
269
|
+
scopes.push({
|
|
270
|
+
scope,
|
|
271
|
+
label: stringFromQualityRecord(item, 'label') ?? scope,
|
|
272
|
+
metrics: metricSet,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else if (isRecord(rawScopes)) {
|
|
277
|
+
for (const [scope, value] of Object.entries(rawScopes)) {
|
|
278
|
+
if (!isRecord(value))
|
|
279
|
+
continue;
|
|
280
|
+
const metricSet = readReleaseQualityMetricSet(value['metrics']) ?? readReleaseQualityMetricSet(value);
|
|
281
|
+
if (!metricSet)
|
|
282
|
+
continue;
|
|
283
|
+
scopes.push({
|
|
284
|
+
scope,
|
|
285
|
+
label: stringFromQualityRecord(value, 'label') ?? scope,
|
|
286
|
+
metrics: metricSet,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return scopes;
|
|
291
|
+
}
|
|
292
|
+
function buildReleaseQualityPoints(releaseEvents, overallLabel) {
|
|
160
293
|
const points = [];
|
|
161
|
-
for (const
|
|
162
|
-
if (
|
|
294
|
+
for (const event of releaseEvents) {
|
|
295
|
+
if (!event.releaseVersionId)
|
|
296
|
+
continue;
|
|
297
|
+
const releaseVersionKind = event.releaseVersionKind ?? (event.laneType === 'production' ? 'production' : 'candidate');
|
|
298
|
+
for (const scope of readReleaseQualityScopes(event.metrics, overallLabel)) {
|
|
163
299
|
points.push({
|
|
164
|
-
id:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
300
|
+
id: `${event.id}:${scope.scope}`,
|
|
301
|
+
eventId: event.id,
|
|
302
|
+
eventLabel: event.operation,
|
|
303
|
+
scope: scope.scope,
|
|
304
|
+
scopeLabel: scope.label,
|
|
305
|
+
releaseVersionId: event.releaseVersionId,
|
|
306
|
+
releaseVersionKind,
|
|
307
|
+
lane: qualityVersionLane(releaseVersionKind),
|
|
308
|
+
promptVersionLabel: event.promptVersionLabel ?? formatShortId(event.promptVersionId),
|
|
309
|
+
modelName: event.modelName ?? formatShortId(event.modelId),
|
|
310
|
+
releaseVersionLabel: event.releaseVersionLabel ?? formatShortId(event.releaseVersionId),
|
|
311
|
+
sampleCount: scope.metrics.sampleCount,
|
|
312
|
+
createdAt: event.createdAt,
|
|
313
|
+
updatedAt: event.updatedAt,
|
|
314
|
+
recall: scope.metrics.recall,
|
|
315
|
+
precision: scope.metrics.precision,
|
|
316
|
+
f1: scope.metrics.f1,
|
|
317
|
+
accuracy: scope.metrics.accuracy,
|
|
176
318
|
});
|
|
177
319
|
}
|
|
178
320
|
}
|
|
179
|
-
return points
|
|
180
|
-
|
|
181
|
-
|
|
321
|
+
return points.sort((left, right) => timeValue(left.createdAt) - timeValue(right.createdAt));
|
|
322
|
+
}
|
|
323
|
+
function buildQualityChartSeries(points, metrics, scopes) {
|
|
324
|
+
const sortedPoints = [...points].sort((left, right) => timeValue(left.createdAt) - timeValue(right.createdAt));
|
|
325
|
+
const eventOrder = new Map();
|
|
326
|
+
for (const point of sortedPoints) {
|
|
327
|
+
if (!eventOrder.has(point.eventId))
|
|
328
|
+
eventOrder.set(point.eventId, eventOrder.size + 1);
|
|
329
|
+
}
|
|
330
|
+
const series = [];
|
|
331
|
+
for (const scope of scopes) {
|
|
332
|
+
const scopedPoints = sortedPoints.filter((point) => point.scope === scope.id);
|
|
333
|
+
if (scopedPoints.length === 0)
|
|
334
|
+
continue;
|
|
335
|
+
for (const metric of metrics) {
|
|
336
|
+
const seriesId = `${scope.id}:${metric.id}`;
|
|
337
|
+
const seriesLabel = `${scope.label} · ${metric.label}`;
|
|
338
|
+
const color = QUALITY_SERIES_COLORS[series.length % QUALITY_SERIES_COLORS.length] ?? 'var(--primary)';
|
|
339
|
+
series.push({
|
|
340
|
+
id: seriesId,
|
|
341
|
+
label: seriesLabel,
|
|
342
|
+
color,
|
|
343
|
+
metric: metric.id,
|
|
344
|
+
metricLabel: metric.label,
|
|
345
|
+
scope: scope.id,
|
|
346
|
+
scopeLabel: scope.label,
|
|
347
|
+
points: scopedPoints.map((point) => ({
|
|
348
|
+
...point,
|
|
349
|
+
xIndex: eventOrder.get(point.eventId) ?? 0,
|
|
350
|
+
xLabel: `#${eventOrder.get(point.eventId) ?? 0}`,
|
|
351
|
+
metric: metric.id,
|
|
352
|
+
metricLabel: metric.label,
|
|
353
|
+
seriesId,
|
|
354
|
+
seriesLabel,
|
|
355
|
+
seriesColor: color,
|
|
356
|
+
value: point[metric.id],
|
|
357
|
+
})),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return series;
|
|
362
|
+
}
|
|
363
|
+
function buildQualityChartAxisData(series) {
|
|
364
|
+
const points = new Map();
|
|
365
|
+
for (const item of series) {
|
|
366
|
+
for (const point of item.points) {
|
|
367
|
+
points.set(point.eventId, point);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return [...points.values()].sort((left, right) => left.xIndex - right.xIndex);
|
|
371
|
+
}
|
|
372
|
+
function buildQualityVersionOptions(points) {
|
|
373
|
+
const versions = new Map();
|
|
374
|
+
for (const point of points) {
|
|
375
|
+
const existing = versions.get(point.releaseVersionId);
|
|
376
|
+
const latestAt = latestTime([existing?.latestAt, point.updatedAt ?? point.createdAt]);
|
|
377
|
+
if (existing) {
|
|
378
|
+
versions.set(point.releaseVersionId, {
|
|
379
|
+
...existing,
|
|
380
|
+
pointCount: existing.pointCount + 1,
|
|
381
|
+
latestAt,
|
|
382
|
+
});
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
versions.set(point.releaseVersionId, {
|
|
386
|
+
id: point.releaseVersionId,
|
|
387
|
+
label: point.releaseVersionLabel,
|
|
388
|
+
kind: point.releaseVersionKind,
|
|
389
|
+
promptVersion: point.promptVersionLabel,
|
|
390
|
+
model: point.modelName,
|
|
391
|
+
pointCount: 1,
|
|
392
|
+
latestAt,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
return [...versions.values()].sort((left, right) => left.label.localeCompare(right.label, undefined, { numeric: true }));
|
|
396
|
+
}
|
|
397
|
+
function buildQualityScopeOptions(points) {
|
|
398
|
+
const scopes = new Map();
|
|
399
|
+
for (const point of points) {
|
|
400
|
+
scopes.set(point.scope, { id: point.scope, label: point.scopeLabel });
|
|
401
|
+
}
|
|
402
|
+
return [...scopes.values()].sort((left, right) => {
|
|
403
|
+
if (left.id === QUALITY_OVERALL_SCOPE)
|
|
404
|
+
return -1;
|
|
405
|
+
if (right.id === QUALITY_OVERALL_SCOPE)
|
|
406
|
+
return 1;
|
|
407
|
+
return left.label.localeCompare(right.label, undefined, { numeric: true });
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
function resolveActiveQualityScopes(selectedScopes, options) {
|
|
411
|
+
if (selectedScopes) {
|
|
412
|
+
const selectedSet = new Set(selectedScopes);
|
|
413
|
+
const activeOptions = options.filter((option) => selectedSet.has(option.id));
|
|
414
|
+
if (activeOptions.length > 0)
|
|
415
|
+
return activeOptions;
|
|
416
|
+
}
|
|
417
|
+
const fallback = options.find((option) => option.id === QUALITY_OVERALL_SCOPE) ?? options[0] ?? null;
|
|
418
|
+
return fallback ? [fallback] : [];
|
|
419
|
+
}
|
|
420
|
+
function resolveActiveQualityMetrics(selectedMetrics, options) {
|
|
421
|
+
if (selectedMetrics) {
|
|
422
|
+
const selectedSet = new Set(selectedMetrics);
|
|
423
|
+
const activeOptions = options.filter((option) => selectedSet.has(option.id));
|
|
424
|
+
if (activeOptions.length > 0)
|
|
425
|
+
return activeOptions;
|
|
426
|
+
}
|
|
427
|
+
return options.filter((option) => option.id === 'f1');
|
|
428
|
+
}
|
|
429
|
+
function filterQualityPoints(points, versionIds, scopes) {
|
|
430
|
+
if (scopes.length === 0)
|
|
431
|
+
return [];
|
|
432
|
+
const versionSet = new Set(versionIds);
|
|
433
|
+
const scopeSet = new Set(scopes.map((scope) => scope.id));
|
|
434
|
+
return points.filter((point) => versionSet.has(point.releaseVersionId) && scopeSet.has(point.scope));
|
|
435
|
+
}
|
|
436
|
+
function toggleQualityFilterValue(values, value) {
|
|
437
|
+
if (!values.includes(value))
|
|
438
|
+
return [...values, value];
|
|
439
|
+
if (values.length <= 1)
|
|
440
|
+
return values;
|
|
441
|
+
return values.filter((item) => item !== value);
|
|
442
|
+
}
|
|
443
|
+
function normalizeQualitySearch(value) {
|
|
444
|
+
return value.trim().toLowerCase();
|
|
445
|
+
}
|
|
446
|
+
function qualitySearchIncludes(query, parts) {
|
|
447
|
+
return parts
|
|
448
|
+
.filter((part) => part !== null && part !== undefined)
|
|
449
|
+
.join(' ')
|
|
450
|
+
.toLowerCase()
|
|
451
|
+
.includes(query);
|
|
182
452
|
}
|
|
183
453
|
function comparisonFromDelta(current, previous, formatter, label, unit) {
|
|
184
454
|
const delta = current - previous;
|
|
@@ -282,6 +552,41 @@ function CompactMetricGroup({ title, items, className, }) {
|
|
|
282
552
|
return (_jsxs("div", { className: "min-w-0", children: [_jsxs("dt", { className: "flex items-center gap-1.5 text-[12px] text-muted-foreground", children: [_jsx("span", { className: cn('size-1.5 shrink-0 rounded-full', COMPACT_METRIC_DOT_CLASS[tone]) }), _jsx("span", { className: "truncate", children: item.label })] }), _jsx("dd", { className: cn('mt-1 truncate text-[20px] font-semibold leading-none text-foreground', tone === 'danger' && 'text-destructive'), children: item.value })] }, item.label));
|
|
283
553
|
}) })] }));
|
|
284
554
|
}
|
|
555
|
+
function ReleaseLineDeleteImpactPanel({ impact, loading, }) {
|
|
556
|
+
const { t } = useI18n();
|
|
557
|
+
if (loading && !impact) {
|
|
558
|
+
return (_jsx("div", { className: "rounded-lg border bg-muted/35 px-3 py-2 text-[12px] text-muted-foreground", "data-testid": "release-line-delete-impact", children: t('releases.deleteImpact.loading') }));
|
|
559
|
+
}
|
|
560
|
+
if (!impact)
|
|
561
|
+
return null;
|
|
562
|
+
const items = [
|
|
563
|
+
{
|
|
564
|
+
key: 'events',
|
|
565
|
+
label: t('releases.deleteImpact.events'),
|
|
566
|
+
hint: t('releases.deleteImpact.eventsHint'),
|
|
567
|
+
count: impact.events.length,
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
key: 'versions',
|
|
571
|
+
label: t('releases.deleteImpact.versions'),
|
|
572
|
+
hint: t('releases.deleteImpact.versionsHint'),
|
|
573
|
+
count: impact.versions.length,
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
key: 'run-results',
|
|
577
|
+
label: t('releases.deleteImpact.runResults'),
|
|
578
|
+
hint: t('releases.deleteImpact.runResultsHint'),
|
|
579
|
+
count: impact.runResults,
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
key: 'annotation-tasks',
|
|
583
|
+
label: t('releases.deleteImpact.annotationTasks'),
|
|
584
|
+
hint: t('releases.deleteImpact.annotationTasksHint'),
|
|
585
|
+
count: impact.annotationTasks.length,
|
|
586
|
+
},
|
|
587
|
+
];
|
|
588
|
+
return (_jsx("div", { className: "rounded-lg border border-destructive/25 bg-background p-3", "data-testid": "release-line-delete-impact", children: impact.total === 0 ? (_jsx("div", { className: "text-[12px] text-muted-foreground", children: t('releases.deleteImpact.empty') })) : (_jsx("div", { className: "grid gap-2 sm:grid-cols-4", children: items.map((item) => (_jsxs("div", { className: "rounded-md border bg-muted/30 px-3 py-2", "data-testid": `release-line-delete-impact-${item.key}`, children: [_jsx("div", { className: "text-[11px] font-medium text-muted-foreground", children: item.label }), _jsx("div", { className: "mt-1 font-mono text-[18px] font-semibold leading-none", children: formatCount(item.count) }), _jsx("div", { className: "mt-1 truncate text-[10.5px] text-muted-foreground", children: item.hint })] }, item.key))) })) }));
|
|
589
|
+
}
|
|
285
590
|
export function ReleaseLineDetailPage({ projectId, releaseLineId }) {
|
|
286
591
|
const router = useRouter();
|
|
287
592
|
const pathname = usePathname();
|
|
@@ -293,17 +598,35 @@ export function ReleaseLineDetailPage({ projectId, releaseLineId }) {
|
|
|
293
598
|
const line = useMemo(() => listQuery.data.find((item) => item.id === lineId || getReleaseLineId(item.promptId, item.inputConnectorId) === lineId) ?? null, [lineId, listQuery.data]);
|
|
294
599
|
const historyQuery = useProductionReleaseHistory(projectId, line?.promptId ?? '');
|
|
295
600
|
const releaseLineEventsQuery = useReleaseLineEvents(projectId, line?.id ?? '');
|
|
296
|
-
const
|
|
601
|
+
const stopLineMutation = useStopReleaseLine(projectId);
|
|
602
|
+
const startLineMutation = useStartReleaseLine(projectId);
|
|
603
|
+
const archiveLineMutation = useArchiveReleaseLine(projectId);
|
|
604
|
+
const unarchiveLineMutation = useUnarchiveReleaseLine(projectId);
|
|
605
|
+
const deleteLineMutation = useDeleteReleaseLine(projectId);
|
|
606
|
+
const stopCanaryMutation = useStopCanaryRelease(projectId);
|
|
297
607
|
const updateTrafficRatioMutation = useUpdateReleaseLineTrafficRatio(projectId);
|
|
608
|
+
const promoteCanaryMutation = usePromoteReleaseLineCanary(projectId);
|
|
298
609
|
const updateRunConfigMutation = useUpdateReleaseLineRunConfig(projectId);
|
|
610
|
+
const updateOutputRouteMutation = useUpdateReleaseLineOutputRoute(projectId);
|
|
611
|
+
const updateInputRouteMutation = useUpdateReleaseLineInputRoute(projectId);
|
|
299
612
|
const modelQuery = useProjectModels(projectId);
|
|
613
|
+
const outputConnectorsQuery = useConnectors(projectId, { direction: 'output' });
|
|
300
614
|
const tab = resolveTab(searchParams.get('tab'));
|
|
301
|
-
const
|
|
615
|
+
const selectedReleaseVersionId = searchParams.get('version') ?? undefined;
|
|
302
616
|
const [stopDialogOpen, setStopDialogOpen] = useState(false);
|
|
303
617
|
const [stopConfirmationText, setStopConfirmationText] = useState('');
|
|
618
|
+
const [archiveDialogOpen, setArchiveDialogOpen] = useState(false);
|
|
619
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
620
|
+
const [deleteState, setDeleteState] = useState({ lineId: '', confirmationText: '', error: null });
|
|
621
|
+
const activeDeleteLineId = line?.id ?? '';
|
|
622
|
+
const deleteConfirmationText = deleteState.lineId === activeDeleteLineId ? deleteState.confirmationText : '';
|
|
623
|
+
const deleteError = deleteState.lineId === activeDeleteLineId ? deleteState.error : null;
|
|
624
|
+
const deleteImpactQuery = useReleaseLineDeleteImpact(projectId, deleteDialogOpen ? activeDeleteLineId : '');
|
|
304
625
|
const productionReleaseName = useMemo(() => getReleaseStopConfirmationName(line), [line]);
|
|
305
626
|
const canConfirmStopProduction = stopConfirmationText === productionReleaseName && productionReleaseName.length > 0;
|
|
306
|
-
const
|
|
627
|
+
const canConfirmDelete = Boolean(line && deleteConfirmationText === line.label);
|
|
628
|
+
const canAddCanary = Boolean(line && line.production?.currentEvent?.status === 'running');
|
|
629
|
+
const canaryActionPending = stopCanaryMutation.isPending || promoteCanaryMutation.isPending;
|
|
307
630
|
const isLive = hasRunningRelease(line);
|
|
308
631
|
const onAutoRefreshTick = useCallback(async () => {
|
|
309
632
|
await Promise.all([
|
|
@@ -318,10 +641,18 @@ export function ReleaseLineDetailPage({ projectId, releaseLineId }) {
|
|
|
318
641
|
onTick: onAutoRefreshTick,
|
|
319
642
|
});
|
|
320
643
|
useEffect(() => {
|
|
321
|
-
|
|
644
|
+
const rawTab = searchParams.get('tab');
|
|
645
|
+
const normalizedTab = rawTab === 'annotation'
|
|
646
|
+
? 'quality'
|
|
647
|
+
: rawTab === 'variants' || rawTab === 'versions'
|
|
648
|
+
? 'history'
|
|
649
|
+
: rawTab === 'delete'
|
|
650
|
+
? 'settings'
|
|
651
|
+
: null;
|
|
652
|
+
if (!normalizedTab)
|
|
322
653
|
return;
|
|
323
654
|
const params = new URLSearchParams(searchParams.toString());
|
|
324
|
-
params.set('tab',
|
|
655
|
+
params.set('tab', normalizedTab);
|
|
325
656
|
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
|
|
326
657
|
}, [pathname, router, searchParams]);
|
|
327
658
|
const selectTab = useCallback((next) => {
|
|
@@ -331,7 +662,7 @@ export function ReleaseLineDetailPage({ projectId, releaseLineId }) {
|
|
|
331
662
|
else
|
|
332
663
|
params.set('tab', next);
|
|
333
664
|
if (next !== 'results')
|
|
334
|
-
params.delete('
|
|
665
|
+
params.delete('version');
|
|
335
666
|
const query = params.toString();
|
|
336
667
|
router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false });
|
|
337
668
|
}, [pathname, router, searchParams]);
|
|
@@ -343,22 +674,22 @@ export function ReleaseLineDetailPage({ projectId, releaseLineId }) {
|
|
|
343
674
|
return (_jsx(Main, { fixed: true, className: "bg-muted/35", children: _jsx("div", { className: "rounded-lg border bg-card p-10 text-center text-sm text-muted-foreground", children: t('releases.detail.notFound') }) }));
|
|
344
675
|
}
|
|
345
676
|
function openStopProductionDialog() {
|
|
346
|
-
if (!line
|
|
677
|
+
if (!line || line.status !== 'running')
|
|
347
678
|
return;
|
|
348
679
|
setStopConfirmationText('');
|
|
349
680
|
setStopDialogOpen(true);
|
|
350
681
|
}
|
|
351
682
|
function closeStopProductionDialog() {
|
|
352
|
-
if (
|
|
683
|
+
if (stopLineMutation.isPending)
|
|
353
684
|
return;
|
|
354
685
|
setStopDialogOpen(false);
|
|
355
686
|
setStopConfirmationText('');
|
|
356
687
|
}
|
|
357
688
|
function confirmStopProduction() {
|
|
358
|
-
if (!line
|
|
689
|
+
if (!line || !canConfirmStopProduction)
|
|
359
690
|
return;
|
|
360
|
-
|
|
361
|
-
|
|
691
|
+
stopLineMutation.mutate({
|
|
692
|
+
releaseLineId: line.id,
|
|
362
693
|
body: { reason: t('releases.detail.stopReason') },
|
|
363
694
|
}, {
|
|
364
695
|
onSuccess: () => {
|
|
@@ -367,22 +698,116 @@ export function ReleaseLineDetailPage({ projectId, releaseLineId }) {
|
|
|
367
698
|
},
|
|
368
699
|
});
|
|
369
700
|
}
|
|
701
|
+
function startReleaseLine() {
|
|
702
|
+
if (!line || line.status !== 'stopped')
|
|
703
|
+
return;
|
|
704
|
+
startLineMutation.mutate({
|
|
705
|
+
releaseLineId: line.id,
|
|
706
|
+
body: { reason: t('releases.detail.startReason') },
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
function openArchiveDialog() {
|
|
710
|
+
if (!line || line.status !== 'stopped')
|
|
711
|
+
return;
|
|
712
|
+
setArchiveDialogOpen(true);
|
|
713
|
+
}
|
|
714
|
+
function closeArchiveDialog() {
|
|
715
|
+
if (archiveLineMutation.isPending)
|
|
716
|
+
return;
|
|
717
|
+
setArchiveDialogOpen(false);
|
|
718
|
+
}
|
|
719
|
+
function confirmArchiveReleaseLine() {
|
|
720
|
+
if (!line || line.status !== 'stopped')
|
|
721
|
+
return;
|
|
722
|
+
archiveLineMutation.mutate({
|
|
723
|
+
releaseLineId: line.id,
|
|
724
|
+
body: { reason: t('releases.detail.archiveReason') },
|
|
725
|
+
}, {
|
|
726
|
+
onSuccess: () => setArchiveDialogOpen(false),
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
function unarchiveReleaseLine() {
|
|
730
|
+
if (!line || line.status !== 'archived')
|
|
731
|
+
return;
|
|
732
|
+
unarchiveLineMutation.mutate({
|
|
733
|
+
releaseLineId: line.id,
|
|
734
|
+
body: { reason: t('releases.detail.unarchiveReason') },
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
function openDeleteDialog() {
|
|
738
|
+
if (!line)
|
|
739
|
+
return;
|
|
740
|
+
setDeleteState({ lineId: line.id, confirmationText: '', error: null });
|
|
741
|
+
setDeleteDialogOpen(true);
|
|
742
|
+
}
|
|
743
|
+
function closeDeleteDialog() {
|
|
744
|
+
if (deleteLineMutation.isPending)
|
|
745
|
+
return;
|
|
746
|
+
setDeleteDialogOpen(false);
|
|
747
|
+
if (line)
|
|
748
|
+
setDeleteState({ lineId: line.id, confirmationText: '', error: null });
|
|
749
|
+
}
|
|
750
|
+
async function confirmDeleteReleaseLine() {
|
|
751
|
+
if (!line || !canConfirmDelete)
|
|
752
|
+
return;
|
|
753
|
+
setDeleteState((current) => ({
|
|
754
|
+
lineId: line.id,
|
|
755
|
+
confirmationText: current.lineId === line.id ? current.confirmationText : '',
|
|
756
|
+
error: null,
|
|
757
|
+
}));
|
|
758
|
+
try {
|
|
759
|
+
await deleteLineMutation.mutateAsync({
|
|
760
|
+
releaseLineId: line.id,
|
|
761
|
+
body: {
|
|
762
|
+
confirmationName: line.label,
|
|
763
|
+
reason: t('releases.detail.deleteReason'),
|
|
764
|
+
},
|
|
765
|
+
});
|
|
766
|
+
setDeleteDialogOpen(false);
|
|
767
|
+
router.push('/releases');
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
setDeleteState((current) => ({
|
|
771
|
+
lineId: line.id,
|
|
772
|
+
confirmationText: current.lineId === line.id ? current.confirmationText : '',
|
|
773
|
+
error: getApiErrorMessage(error) ?? t('releases.detail.deleteFailed'),
|
|
774
|
+
}));
|
|
775
|
+
}
|
|
776
|
+
}
|
|
370
777
|
function openAddCanaryPage() {
|
|
371
778
|
if (!line || !canAddCanary)
|
|
372
779
|
return;
|
|
373
780
|
router.push(`/releases/new?mode=canary&line=${encodeURIComponent(line.id)}`);
|
|
374
781
|
}
|
|
375
|
-
return (_jsxs(Main, { fixed: true, className: "gap-5 overflow-auto bg-muted/35 pb-8", children: [_jsxs("div", { className: "mx-auto flex w-full max-w-[1760px] flex-col gap-5", "data-testid": "release-line-detail-page", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4", children: [_jsx("div", { className: "min-w-0", children: _jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [_jsx("h1", { className: "truncate text-[22px] font-semibold leading-tight", children: line.promptName }), _jsx("span", { "data-testid": "release-line-detail-status", className: "sr-only", children: line.
|
|
782
|
+
return (_jsxs(Main, { fixed: true, className: "gap-5 overflow-auto bg-muted/35 pb-8", children: [_jsxs("div", { className: "mx-auto flex w-full max-w-[1760px] flex-col gap-5", "data-testid": "release-line-detail-page", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4", children: [_jsx("div", { className: "min-w-0", children: _jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [_jsx("h1", { className: "truncate text-[22px] font-semibold leading-tight", children: line.promptName }), _jsx("span", { "data-testid": "release-line-detail-status", className: "sr-only", children: line.status })] }) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [line.status === 'running' ? (_jsxs(Button, { variant: "outline", onClick: openStopProductionDialog, disabled: stopLineMutation.isPending, className: "text-destructive hover:text-destructive", "data-testid": "release-line-detail-stop", children: [_jsx(Square, { className: "size-4" }), t('releases.detail.action.stopProduction')] })) : null, line.status === 'stopped' ? (_jsxs(Button, { variant: "outline", onClick: startReleaseLine, disabled: startLineMutation.isPending, "data-testid": "release-line-detail-start", children: [_jsx(Play, { className: "size-4" }), startLineMutation.isPending ? t('releases.detail.action.starting') : t('releases.detail.action.start')] })) : null, line.status === 'stopped' ? (_jsxs(Button, { variant: "outline", onClick: openArchiveDialog, disabled: archiveLineMutation.isPending, "data-testid": "release-line-detail-archive", children: [_jsx(Archive, { className: "size-4" }), archiveLineMutation.isPending
|
|
783
|
+
? t('releases.detail.action.archiving')
|
|
784
|
+
: t('releases.detail.action.archive')] })) : null, line.status === 'archived' ? (_jsxs(Button, { variant: "outline", onClick: unarchiveReleaseLine, disabled: unarchiveLineMutation.isPending, "data-testid": "release-line-detail-unarchive", children: [_jsx(RotateCcw, { className: "size-4" }), unarchiveLineMutation.isPending
|
|
785
|
+
? t('releases.detail.action.unarchiving')
|
|
786
|
+
: t('releases.detail.action.unarchive')] })) : null, canAddCanary ? (_jsxs(Button, { onClick: openAddCanaryPage, children: [_jsx(Plus, { className: "size-4" }), line.canary ? t('releases.detail.action.replaceCanary') : t('releases.detail.action.addCanary')] })) : null] })] }), _jsx(ReleaseTopologyCanvas, { line: line, models: modelQuery.data?.data ?? [], modelsLoading: modelQuery.isLoading, outputConnectors: outputConnectorsQuery.data?.data ?? [], outputConnectorsLoading: outputConnectorsQuery.isLoading, onUpdateTrafficRatio: (_canary, trafficRatio) => updateTrafficRatioMutation.mutateAsync({
|
|
376
787
|
releaseLineId: line.id,
|
|
377
788
|
body: { trafficRatio },
|
|
378
789
|
}), trafficRatioPending: updateTrafficRatioMutation.isPending, onUpdateRunConfig: (body) => updateRunConfigMutation.mutateAsync({
|
|
379
790
|
releaseLineId: line.id,
|
|
380
791
|
body,
|
|
381
|
-
}), runConfigPending: updateRunConfigMutation.isPending,
|
|
792
|
+
}), runConfigPending: updateRunConfigMutation.isPending, onUpdateOutputRoute: (body) => updateOutputRouteMutation.mutateAsync({
|
|
793
|
+
releaseLineId: line.id,
|
|
794
|
+
body,
|
|
795
|
+
}), outputRoutePending: updateOutputRouteMutation.isPending, onUpdateInputRoute: (body) => updateInputRouteMutation.mutateAsync({
|
|
796
|
+
releaseLineId: line.id,
|
|
797
|
+
body,
|
|
798
|
+
}), inputRoutePending: updateInputRouteMutation.isPending, onAddCanary: canAddCanary ? openAddCanaryPage : undefined, onStopCanary: (canary) => stopCanaryMutation.mutateAsync(canary.id), onPromoteCanary: () => promoteCanaryMutation.mutateAsync(line.id), canaryActionPending: canaryActionPending }), _jsx("div", { className: "inline-flex w-fit flex-wrap gap-0.5 rounded-lg border bg-card p-1", children: DETAIL_TABS.map((item) => (_jsx("button", { type: "button", onClick: () => selectTab(item.value), className: cn('rounded-md px-3.5 py-1.5 text-[13px] font-medium transition-colors', tab === item.value
|
|
382
799
|
? 'bg-muted font-semibold text-foreground'
|
|
383
|
-
: 'text-muted-foreground hover:text-foreground'), children: t(item.key) }, item.value))) }), tab === 'monitoring' ? (_jsx(MonitoringPane, { projectId: projectId, line: line, releaseEvents: releaseLineEventsQuery.data?.data ?? [] })) : null, tab === '
|
|
800
|
+
: 'text-muted-foreground hover:text-foreground'), children: t(item.key) }, item.value))) }), tab === 'monitoring' ? (_jsx(MonitoringPane, { projectId: projectId, line: line, releaseEvents: releaseLineEventsQuery.data?.data ?? [] })) : null, tab === 'results' ? (_jsx(ResultsPane, { projectId: projectId, line: line, releaseEvents: releaseLineEventsQuery.data?.data ?? [], initialReleaseVersionId: selectedReleaseVersionId })) : null, tab === 'quality' ? (_jsx(QualityMetricsPane, { line: line, releaseEvents: releaseLineEventsQuery.data?.data ?? [] })) : null, tab === 'history' ? (_jsx(HistoryPane, { projectId: projectId, line: line, productionHistory: historyQuery.data?.data ?? [], releaseEvents: releaseLineEventsQuery.data?.data ?? [], loading: historyQuery.isLoading || releaseLineEventsQuery.isLoading })) : null, tab === 'settings' ? (_jsxs("section", { className: "rounded-lg border bg-card", "data-testid": "release-line-settings-tab", children: [_jsxs("div", { className: "border-b px-4 py-3", children: [_jsx("div", { className: "text-[13px] font-semibold", children: t('releases.detail.settings.title') }), _jsx("p", { className: "mt-1 text-[12px] text-muted-foreground", children: t('releases.detail.settings.description') })] }), _jsx("div", { className: "p-4", children: _jsxs("div", { className: "flex flex-col gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-4 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 text-[13px] font-semibold text-destructive", children: [_jsx(AlertTriangle, { className: "size-4" }), t('releases.detail.delete.dangerTitle')] }), _jsx("p", { className: "mt-1 max-w-3xl text-[12px] text-muted-foreground", children: t('releases.detail.delete.description') })] }), _jsxs(Button, { type: "button", variant: "destructive", onClick: openDeleteDialog, "data-testid": "release-line-delete-open", children: [_jsx(Trash2, { className: "size-4" }), t('releases.detail.delete.open')] })] }) })] })) : null] }), _jsx(Dialog, { open: stopDialogOpen, onOpenChange: (open) => (open ? setStopDialogOpen(true) : closeStopProductionDialog()), children: _jsxs(DialogContent, { "data-testid": "release-stop-production-dialog", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('releases.detail.stopDialog.title') }), _jsx(DialogDescription, { children: t('releases.detail.stopDialog.description') })] }), _jsxs("div", { className: "rounded-lg border bg-muted/40 p-3", children: [_jsx("div", { className: "text-[11.5px] font-medium text-muted-foreground", children: t('releases.detail.stopDialog.releaseName') }), _jsx("div", { className: "mt-1 break-all font-mono text-[13px] font-semibold", children: productionReleaseName || '—' })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "release-stop-production-name", className: "text-[12.5px] font-medium", children: t('releases.detail.stopDialog.inputLabel') }), _jsx(Input, { id: "release-stop-production-name", value: stopConfirmationText, onChange: (event) => setStopConfirmationText(event.target.value), placeholder: t('releases.detail.stopDialog.inputPlaceholder').replace('{name}', productionReleaseName), autoComplete: "off" }), stopConfirmationText.length > 0 && !canConfirmStopProduction ? (_jsx("p", { className: "text-[12px] text-destructive", children: t('releases.detail.stopDialog.mismatch') })) : null] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: closeStopProductionDialog, disabled: stopLineMutation.isPending, children: t('common.cancel') }), _jsxs(Button, { type: "button", variant: "destructive", onClick: confirmStopProduction, disabled: !canConfirmStopProduction || stopLineMutation.isPending, "data-testid": "release-stop-production-confirm", children: [_jsx(Square, { className: "size-4" }), stopLineMutation.isPending
|
|
384
801
|
? t('releases.detail.stopDialog.stopping')
|
|
385
|
-
: t('releases.detail.stopDialog.confirm')] })] })] }) })] }))
|
|
802
|
+
: t('releases.detail.stopDialog.confirm')] })] })] }) }), _jsx(Dialog, { open: archiveDialogOpen, onOpenChange: (open) => (open ? setArchiveDialogOpen(true) : closeArchiveDialog()), children: _jsxs(DialogContent, { "data-testid": "release-line-detail-archive-dialog", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('releases.archiveDialog.title') }), _jsx(DialogDescription, { children: t('releases.archiveDialog.description') })] }), _jsxs("div", { className: "rounded-lg border bg-muted/40 p-3", children: [_jsx("div", { className: "text-[11.5px] font-medium text-muted-foreground", children: t('releases.detail.stopDialog.releaseName') }), _jsx("div", { className: "mt-1 break-all font-mono text-[13px] font-semibold", children: line.label })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: closeArchiveDialog, disabled: archiveLineMutation.isPending, children: t('common.cancel') }), _jsxs(Button, { type: "button", onClick: confirmArchiveReleaseLine, disabled: archiveLineMutation.isPending, "data-testid": "release-line-detail-archive-confirm", children: [_jsx(Archive, { className: "size-4" }), archiveLineMutation.isPending
|
|
803
|
+
? t('releases.archiveDialog.archiving')
|
|
804
|
+
: t('releases.archiveDialog.confirm')] })] })] }) }), _jsx(Dialog, { open: deleteDialogOpen, onOpenChange: (open) => (open ? openDeleteDialog() : closeDeleteDialog()), children: _jsxs(DialogContent, { "data-testid": "release-line-delete-dialog", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('releases.detail.delete.title') }), _jsx(DialogDescription, { children: t('releases.detail.delete.dialogDescription') })] }), _jsxs("div", { className: "rounded-lg border border-destructive/30 bg-destructive/5 p-3", children: [_jsx("div", { className: "text-[11.5px] font-medium text-muted-foreground", children: t('releases.detail.delete.releaseName') }), _jsx("div", { className: "mt-1 break-all font-mono text-[13px] font-semibold", children: line.label })] }), _jsx(ReleaseLineDeleteImpactPanel, { impact: deleteImpactQuery.data, loading: deleteImpactQuery.isLoading || deleteImpactQuery.isFetching }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "release-line-delete-name", className: "text-[12.5px] font-medium", children: t('releases.detail.delete.inputLabel') }), _jsx(Input, { id: "release-line-delete-name", value: deleteConfirmationText, onChange: (event) => setDeleteState({
|
|
805
|
+
lineId: line.id,
|
|
806
|
+
confirmationText: event.target.value,
|
|
807
|
+
error: null,
|
|
808
|
+
}), placeholder: t('releases.detail.delete.inputPlaceholder').replace('{name}', line.label), autoComplete: "off" }), deleteConfirmationText.length > 0 && !canConfirmDelete ? (_jsx("p", { className: "text-[12px] text-destructive", children: t('releases.detail.delete.mismatch') })) : null] }), deleteError ? (_jsx("div", { className: "rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm text-destructive", children: deleteError })) : null, _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: closeDeleteDialog, disabled: deleteLineMutation.isPending, children: t('common.cancel') }), _jsxs(Button, { type: "button", variant: "destructive", onClick: () => void confirmDeleteReleaseLine(), disabled: !canConfirmDelete || deleteLineMutation.isPending, "data-testid": "release-line-delete-confirm", children: [_jsx(Trash2, { className: "size-4" }), deleteLineMutation.isPending
|
|
809
|
+
? t('releases.detail.delete.deleting')
|
|
810
|
+
: t('releases.detail.delete.confirm')] })] })] }) })] }));
|
|
386
811
|
}
|
|
387
812
|
function MonitoringPane({ projectId, line, releaseEvents, }) {
|
|
388
813
|
const { t, language } = useI18n();
|
|
@@ -505,58 +930,57 @@ function MonitoringPane({ projectId, line, releaseEvents, }) {
|
|
|
505
930
|
},
|
|
506
931
|
] })] }) }), _jsxs("section", { className: "space-y-3", "aria-label": t('releases.detail.metric.engineering'), children: [_jsx("h3", { className: "text-[14px] font-semibold", children: t('releases.detail.metric.engineering') }), _jsxs("div", { className: "grid grid-cols-1 gap-4 xl:grid-cols-2 2xl:grid-cols-4", children: [_jsx(BigChartCard, { title: t('releases.detail.metric.rpm'), icon: _jsx(Gauge, { className: "size-4", strokeWidth: 2.2 }), iconBg: "var(--status-pending-bg)", iconFg: "var(--status-pending-fg)", total: formatRateValue(timeseriesMax.rpm), comparison: comparisonFromDelta(timeseriesMax.rpm, stats?.rpmPeak.previous ?? 0, formatRateValue, vsPreviousPeriodLabel), subtitle: t('monitoring.delta.rpmSubtitle'), data: pickTimeseries('rpm'), yTickFormatter: formatRateValue, legendFormatter: formatRateValue, bySource: stats?.rpmPeak.bySource ?? EMPTY_BY_SOURCE, sourceLabels: sourceLabels, sourceKeys: RELEASE_MONITORING_SOURCE_KEYS, ...chartLabels }), _jsx(BigChartCard, { title: t('releases.detail.metric.tpm'), icon: _jsx(Activity, { className: "size-4", strokeWidth: 2.2 }), iconBg: "var(--src-iter-soft)", iconFg: "var(--src-iter-fg)", total: formatBigNumber(timeseriesMax.tpm), comparison: comparisonFromDelta(timeseriesMax.tpm, stats?.tpmPeak.previous ?? 0, formatBigNumber, vsPreviousPeriodLabel), subtitle: t('monitoring.delta.tpmSubtitle'), data: pickTimeseries('tpm'), yTickFormatter: formatBigNumber, legendFormatter: formatBigNumber, bySource: stats?.tpmPeak.bySource ?? EMPTY_BY_SOURCE, sourceLabels: sourceLabels, sourceKeys: RELEASE_MONITORING_SOURCE_KEYS, ...chartLabels }), _jsx(BigChartCard, { title: t('releases.detail.metric.averageLatency'), icon: _jsx(Timer, { className: "size-4", strokeWidth: 2.2 }), iconBg: "var(--src-canary-soft)", iconFg: "var(--src-canary-fg)", total: formatLatencyMs(timeseriesMax.latencyAverageMs), comparison: comparisonFromDelta(timeseriesMax.latencyAverageMs, stats?.latencyAverageMs.previous ?? 0, formatLatencyMs, vsPreviousPeriodLabel), data: pickTimeseries('latencyAverageMs'), yTickFormatter: formatLatencyMs, legendFormatter: formatLatencyMs, bySource: stats?.latencyAverageMs.bySource ?? EMPTY_BY_SOURCE, sourceLabels: sourceLabels, sourceKeys: RELEASE_MONITORING_SOURCE_KEYS, ...chartLabels }), _jsx(BigChartCard, { title: t('releases.detail.metric.p50Latency'), icon: _jsx(Timer, { className: "size-4", strokeWidth: 2.2 }), iconBg: "var(--src-prod-soft)", iconFg: "var(--src-prod-fg)", total: formatLatencyMs(timeseriesMax.latencyP50Ms), comparison: comparisonFromDelta(timeseriesMax.latencyP50Ms, stats?.latencyP50Ms.previous ?? 0, formatLatencyMs, vsPreviousPeriodLabel), data: pickTimeseries('latencyP50Ms'), yTickFormatter: formatLatencyMs, legendFormatter: formatLatencyMs, bySource: stats?.latencyP50Ms.bySource ?? EMPTY_BY_SOURCE, sourceLabels: sourceLabels, sourceKeys: RELEASE_MONITORING_SOURCE_KEYS, ...chartLabels }), _jsx(BigChartCard, { title: t('releases.detail.metric.p95Latency'), icon: _jsx(Timer, { className: "size-4", strokeWidth: 2.2 }), iconBg: "var(--status-running-bg)", iconFg: "var(--status-running-fg)", total: formatLatencyMs(timeseriesMax.latencyP95Ms), comparison: comparisonFromDelta(timeseriesMax.latencyP95Ms, stats?.latencyP95Ms.previous ?? 0, formatLatencyMs, vsPreviousPeriodLabel), data: pickTimeseries('latencyP95Ms'), yTickFormatter: formatLatencyMs, legendFormatter: formatLatencyMs, bySource: stats?.latencyP95Ms.bySource ?? EMPTY_BY_SOURCE, sourceLabels: sourceLabels, sourceKeys: RELEASE_MONITORING_SOURCE_KEYS, ...chartLabels }), _jsx(BigChartCard, { title: t('releases.detail.metric.p99Latency'), icon: _jsx(Timer, { className: "size-4", strokeWidth: 2.2 }), iconBg: "var(--status-pending-bg)", iconFg: "var(--status-pending-fg)", total: formatLatencyMs(timeseriesMax.latencyP99Ms), comparison: comparisonFromDelta(timeseriesMax.latencyP99Ms, stats?.latencyP99Ms.previous ?? 0, formatLatencyMs, vsPreviousPeriodLabel), data: pickTimeseries('latencyP99Ms'), yTickFormatter: formatLatencyMs, legendFormatter: formatLatencyMs, bySource: stats?.latencyP99Ms.bySource ?? EMPTY_BY_SOURCE, sourceLabels: sourceLabels, sourceKeys: RELEASE_MONITORING_SOURCE_KEYS, ...chartLabels }), _jsx(BigChartCard, { title: t('releases.detail.metric.cost'), icon: _jsx(CircleDollarSign, { className: "size-4", strokeWidth: 2.2 }), iconBg: "var(--status-running-bg)", iconFg: "var(--status-running-fg)", total: formatCostValue(timeseriesMax.cost), comparison: comparisonFromDelta(timeseriesMax.cost, stats?.cost.previous ?? 0, formatCostValue, vsPreviousPeriodLabel), subtitle: t('monitoring.delta.costSubtitle'), data: pickTimeseries('cost'), yTickFormatter: formatCostValue, legendFormatter: formatCostValue, bySource: stats?.cost.bySource ?? EMPTY_BY_SOURCE, sourceLabels: sourceLabels, sourceKeys: RELEASE_MONITORING_SOURCE_KEYS, ...chartLabels }), _jsx(BigChartCard, { title: t('releases.detail.metric.failureRate'), icon: _jsx(AlertTriangle, { className: "size-4", strokeWidth: 2.2 }), iconBg: "var(--status-pending-bg)", iconFg: "var(--status-pending-fg)", total: timeseriesMax.failureRatePercent.toFixed(2), unit: "%", comparison: comparisonFromDelta(timeseriesMax.failureRatePercent, failureRatePercent(stats, 'previous'), (value) => value.toFixed(2), vsPreviousPeriodLabel, '%'), subtitle: t('monitoring.delta.failureRateSubtitle'), data: pickFailureRateTimeseries(), yTickFormatter: formatPercentValue, legendFormatter: formatPercentValue, bySource: failureRateBySourcePercent(stats), sourceLabels: sourceLabels, sourceKeys: RELEASE_MONITORING_SOURCE_KEYS, sourceDistributionLabel: chartLabels.sourceDistributionLabel, totalLabel: chartLabels.failureRateTotalLabel })] })] })] }));
|
|
507
932
|
}
|
|
508
|
-
function
|
|
509
|
-
const { t } = useI18n();
|
|
510
|
-
const formatDateTimeOrDash = useDateTimeOrDash();
|
|
511
|
-
const details = useMemo(() => buildReleaseVariantDetails(line, releaseEvents), [line, releaseEvents]);
|
|
512
|
-
const showLoader = useDelayedLoading(loading);
|
|
513
|
-
if (loading && details.length === 0) {
|
|
514
|
-
return showLoader ? _jsx(PlatformLoader, { className: "py-8", size: "sm" }) : null;
|
|
515
|
-
}
|
|
516
|
-
if (details.length === 0) {
|
|
517
|
-
return (_jsx("div", { className: "rounded-lg border bg-card p-10 text-center text-sm text-muted-foreground", children: t('releases.detail.variants.empty') }));
|
|
518
|
-
}
|
|
519
|
-
return (_jsx("section", { className: "space-y-3", "data-testid": "release-variants-pane", children: _jsx("div", { className: "grid grid-cols-1 gap-3 xl:grid-cols-2", children: details.map((detail) => (_jsxs("article", { className: "rounded-lg border bg-card p-4", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { className: "font-mono text-[17px] font-semibold", children: detail.label }), _jsx(ReleaseVariantStageBadge, { stage: detail.stage })] }), _jsxs("div", { className: "mt-1 max-w-full truncate text-[12px] text-muted-foreground", children: [detail.promptName, " \u00B7 ", detail.promptVersionLabel ?? formatShortId(detail.promptVersionId), " \u00B7", ' ', detail.modelName ?? formatShortId(detail.modelId)] })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => void navigator.clipboard?.writeText(detail.id), children: [_jsx(Copy, { className: "size-3.5" }), t('releases.detail.variants.copyId')] }), _jsx(Button, { type: "button", variant: "outline", size: "sm", asChild: true, children: _jsxs(Link, { href: `/releases/${encodeURIComponent(line.id)}?tab=results&variant=${encodeURIComponent(detail.id)}`, children: [_jsx(Activity, { className: "size-3.5" }), t('releases.detail.variants.viewResults')] }) }), _jsx(Button, { type: "button", size: "sm", asChild: true, children: _jsxs(Link, { href: `/annotations/new?line=${encodeURIComponent(line.id)}&variant=${encodeURIComponent(detail.id)}`, children: [_jsx(ClipboardCheck, { className: "size-3.5" }), t('releases.detail.variants.newAnnotation')] }) })] })] }), _jsxs("dl", { className: "mt-4 grid grid-cols-2 gap-3 border-t pt-4 md:grid-cols-4", children: [_jsx(VariantMeta, { label: t('releases.detail.variants.promptVersion'), value: detail.promptVersionLabel ?? formatShortId(detail.promptVersionId) }), _jsx(VariantMeta, { label: t('releases.detail.variants.model'), value: detail.modelName ?? formatShortId(detail.modelId) }), _jsx(VariantMeta, { label: t('releases.detail.variants.provider'), value: detail.modelProvider ?? '—' }), _jsx(VariantMeta, { label: t('releases.detail.variants.updatedAt'), value: formatDateTimeOrDash(detail.updatedAt) }), _jsx(VariantMeta, { label: t('releases.detail.variants.productionEvents'), value: formatCount(detail.productionEventCount) }), _jsx(VariantMeta, { label: t('releases.detail.variants.canaryEvents'), value: formatCount(detail.canaryEventCount) }), _jsx(VariantMeta, { label: t('releases.detail.variants.processed'), value: formatCount(detail.totalProcessed) }), _jsx(VariantMeta, { label: t('releases.detail.variants.errors'), value: formatCount(detail.totalErrors) })] }), _jsxs("div", { className: "mt-4 border-t pt-4", children: [_jsx("div", { className: "mb-2 text-[12px] font-medium text-muted-foreground", children: t('releases.detail.variants.events') }), detail.events.length === 0 ? (_jsx("div", { className: "text-[12px] text-muted-foreground", children: t('releases.detail.variants.noEvents') })) : (_jsx("div", { className: "space-y-2", children: detail.events.slice(0, 5).map((event) => (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2 text-[12px]", children: [_jsx(ReleaseEventPill, { event: event.operation }), _jsx("span", { className: "font-mono text-muted-foreground", children: t(event.laneType === 'production'
|
|
520
|
-
? 'releases.detail.history.productionLane'
|
|
521
|
-
: 'releases.detail.history.canaryLane') }), _jsx("span", { className: "min-w-0 flex-1 truncate text-muted-foreground", children: event.submitReason || event.status }), _jsx("span", { className: "font-mono text-[11.5px] text-muted-foreground", children: formatDateTimeOrDash(event.createdAt) })] }, event.id))) }))] }), _jsx("div", { className: "mt-4 border-t pt-3", children: _jsx("div", { className: "font-mono text-[11px] text-muted-foreground", children: detail.id }) })] }, detail.id))) }) }));
|
|
522
|
-
}
|
|
523
|
-
function VariantMeta({ label, value }) {
|
|
524
|
-
return (_jsxs("div", { className: "min-w-0", children: [_jsx("dt", { className: "truncate text-[11.5px] text-muted-foreground", children: label }), _jsx("dd", { className: "mt-1 truncate font-mono text-[12.5px] font-medium text-foreground", children: value })] }));
|
|
525
|
-
}
|
|
526
|
-
function ReleaseVariantStageBadge({ stage }) {
|
|
527
|
-
const { t } = useI18n();
|
|
528
|
-
const isProduction = stage === 'production' || stage === 'production_canary';
|
|
529
|
-
const isCanary = stage === 'canary' || stage === 'production_canary';
|
|
530
|
-
return (_jsx("span", { className: "inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium", style: {
|
|
531
|
-
background: isProduction ? 'var(--src-prod-soft)' : isCanary ? 'var(--src-canary-soft)' : 'var(--muted)',
|
|
532
|
-
color: isProduction ? 'var(--src-prod-fg)' : isCanary ? 'var(--src-canary-fg)' : 'var(--muted-foreground)',
|
|
533
|
-
borderColor: isProduction
|
|
534
|
-
? 'color-mix(in srgb, var(--src-prod) 30%, transparent)'
|
|
535
|
-
: isCanary
|
|
536
|
-
? 'color-mix(in srgb, var(--src-canary) 30%, transparent)'
|
|
537
|
-
: 'var(--border)',
|
|
538
|
-
}, children: t(`releases.detail.variants.stage.${stage}`) }));
|
|
539
|
-
}
|
|
540
|
-
function ResultsPane({ projectId, line, releaseEvents, initialReleaseVariantId, }) {
|
|
541
|
-
const { t } = useI18n();
|
|
933
|
+
function ResultsPane({ projectId, line, releaseEvents, initialReleaseVersionId, }) {
|
|
934
|
+
const { t, language } = useI18n();
|
|
542
935
|
const formatDateTimeOrDash = useDateTimeOrDash();
|
|
543
|
-
const [
|
|
544
|
-
const [
|
|
545
|
-
const [promptVersionFilter, setPromptVersionFilter] = useState('all');
|
|
936
|
+
const [dateRange, setDateRange] = useState(() => createDefaultResultDateRange());
|
|
937
|
+
const [releaseVersionFilter, setReleaseVersionFilter] = useState(initialReleaseVersionId ?? 'all');
|
|
546
938
|
const [pageIndex, setPageIndex] = useState(0);
|
|
547
939
|
const [pageSize, setPageSize] = useState(20);
|
|
548
940
|
const sourceIds = useMemo(() => getReleaseResultSourceIds(line, releaseEvents), [line, releaseEvents]);
|
|
549
|
-
const
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
? releaseVariantFilter
|
|
553
|
-
: 'all';
|
|
554
|
-
const activePromptVersionFilter = promptVersionFilter === 'all' || promptVersionOptions.some((option) => option.id === promptVersionFilter)
|
|
555
|
-
? promptVersionFilter
|
|
941
|
+
const releaseVersionOptions = useMemo(() => getReleaseResultVersionOptions(line, releaseEvents), [line, releaseEvents]);
|
|
942
|
+
const activeReleaseVersionFilter = releaseVersionFilter === 'all' || releaseVersionOptions.some((option) => option.id === releaseVersionFilter)
|
|
943
|
+
? releaseVersionFilter
|
|
556
944
|
: 'all';
|
|
557
|
-
const
|
|
558
|
-
const
|
|
559
|
-
const
|
|
945
|
+
const releaseVersionIds = activeReleaseVersionFilter === 'all' ? undefined : [activeReleaseVersionFilter];
|
|
946
|
+
const applyDateRange = isResultDateRangeApplied(dateRange);
|
|
947
|
+
const handleDateRangeChange = useCallback((next) => {
|
|
948
|
+
setDateRange(next);
|
|
949
|
+
setPageIndex(0);
|
|
950
|
+
}, []);
|
|
951
|
+
const refreshResultDateRange = useCallback(() => {
|
|
952
|
+
const nextDateRange = resolveRollingDateRangeValue(dateRange);
|
|
953
|
+
if (nextDateRange.preset !== dateRange.preset ||
|
|
954
|
+
nextDateRange.from !== dateRange.from ||
|
|
955
|
+
nextDateRange.to !== dateRange.to) {
|
|
956
|
+
setDateRange(nextDateRange);
|
|
957
|
+
}
|
|
958
|
+
}, [dateRange]);
|
|
959
|
+
useAutoRefresh({
|
|
960
|
+
intervalMs: AUTO_REFRESH_INTERVAL_MS,
|
|
961
|
+
enabled: sourceIds.length > 0 && isResultDateRangeRolling(dateRange),
|
|
962
|
+
onTick: refreshResultDateRange,
|
|
963
|
+
});
|
|
964
|
+
const dateRangePresetLabels = useMemo(() => [
|
|
965
|
+
{ value: 'all', label: t('releases.detail.results.dateFilter.all') },
|
|
966
|
+
{ value: 'h24', label: t('monitoring.timeRange.preset.h24') },
|
|
967
|
+
{ value: 'd7', label: t('monitoring.timeRange.preset.d7') },
|
|
968
|
+
{ value: 'd30', label: t('monitoring.timeRange.preset.d30') },
|
|
969
|
+
{ value: 'custom', label: t('monitoring.timeRange.preset.custom') },
|
|
970
|
+
], [t]);
|
|
971
|
+
const dateRangeLabels = useMemo(() => ({
|
|
972
|
+
ariaLabel: t('releases.detail.results.dateFilter.ariaLabel'),
|
|
973
|
+
customRangeAriaLabel: t('releases.detail.results.dateFilter.customRangeAriaLabel'),
|
|
974
|
+
fromLabel: t('monitoring.timeRange.from'),
|
|
975
|
+
toLabel: t('monitoring.timeRange.to'),
|
|
976
|
+
dateLabel: t('monitoring.timeRange.date'),
|
|
977
|
+
timeLabel: t('monitoring.timeRange.time'),
|
|
978
|
+
previousMonth: t('monitoring.timeRange.previousMonth'),
|
|
979
|
+
nextMonth: t('monitoring.timeRange.nextMonth'),
|
|
980
|
+
cancel: t('common.cancel'),
|
|
981
|
+
apply: t('common.apply'),
|
|
982
|
+
invalidRange: t('monitoring.timeRange.invalidRange'),
|
|
983
|
+
}), [t]);
|
|
560
984
|
const resultsQuery = useReleaseRunResults(projectId, {
|
|
561
985
|
page: pageIndex + 1,
|
|
562
986
|
pageSize,
|
|
@@ -565,9 +989,10 @@ function ResultsPane({ projectId, line, releaseEvents, initialReleaseVariantId,
|
|
|
565
989
|
judgmentStatus: undefined,
|
|
566
990
|
isCorrect: undefined,
|
|
567
991
|
sourceIds,
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
992
|
+
releaseVersionIds,
|
|
993
|
+
releaseVersionScope: 'exact',
|
|
994
|
+
from: applyDateRange ? dateRange.from : undefined,
|
|
995
|
+
to: applyDateRange ? dateRange.to : undefined,
|
|
571
996
|
}, sourceIds.length > 0);
|
|
572
997
|
const rows = resultsQuery.data?.data ?? [];
|
|
573
998
|
const resultsLoading = useDelayedLoading(resultsQuery.isLoading);
|
|
@@ -575,16 +1000,10 @@ function ResultsPane({ projectId, line, releaseEvents, initialReleaseVariantId,
|
|
|
575
1000
|
const pageCount = Math.max(1, Math.ceil(total / pageSize));
|
|
576
1001
|
const from = total === 0 ? 0 : pageIndex * pageSize + 1;
|
|
577
1002
|
const to = Math.min((pageIndex + 1) * pageSize, total);
|
|
578
|
-
return (_jsxs("div", { className: "overflow-hidden rounded-lg border bg-card", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3", children: [_jsx("div", { children: _jsx("h2", { className: "text-[14px] font-semibold", children: t('releases.detail.tab.results') }) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("label", { className: "sr-only", htmlFor: "release-result-
|
|
579
|
-
|
|
1003
|
+
return (_jsxs("div", { className: "overflow-hidden rounded-lg border bg-card", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3", children: [_jsx("div", { children: _jsx("h2", { className: "text-[14px] font-semibold", children: t('releases.detail.tab.results') }) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(DateRangeSegmented, { value: dateRange, onChange: handleDateRangeChange, presetLabels: dateRangePresetLabels, labels: dateRangeLabels, locale: language }), _jsx("label", { className: "sr-only", htmlFor: "release-result-version-filter", children: t('releases.detail.results.version') }), _jsx(ResultReleaseVersionSelect, { id: "release-result-version-filter", options: releaseVersionOptions, value: activeReleaseVersionFilter, onChange: (next) => {
|
|
1004
|
+
setReleaseVersionFilter(next);
|
|
580
1005
|
setPageIndex(0);
|
|
581
|
-
},
|
|
582
|
-
setPromptVersionFilter(event.currentTarget.value);
|
|
583
|
-
setPageIndex(0);
|
|
584
|
-
}, className: "h-9 rounded-md border bg-background px-3 text-[12px] font-medium text-foreground shadow-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", disabled: promptVersionOptions.length === 0, children: [_jsx("option", { value: "all", children: t('releases.detail.results.promptVersionFilter.all') }), promptVersionOptions.map((option) => (_jsx("option", { value: option.id, children: option.label }, option.id)))] }), _jsx("div", { className: "inline-flex rounded-lg border bg-background p-1", children: ['all', 'production', 'canary'].map((value) => (_jsx("button", { type: "button", onClick: () => {
|
|
585
|
-
setSourceFilter(value);
|
|
586
|
-
setPageIndex(0);
|
|
587
|
-
}, className: cn('h-7 rounded-md px-3 text-[12px] font-medium transition-colors', sourceFilter === value ? 'bg-muted text-foreground' : 'text-muted-foreground hover:text-foreground'), children: t(`releases.detail.results.sourceFilter.${value}`) }, value))) })] })] }), _jsxs(Table, { columns: RESULT_COLUMNS, children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { column: "externalId", children: t('releases.detail.results.externalId') }), _jsx(TableHead, { column: "input", children: t('releases.detail.results.input') }), _jsx(TableHead, { column: "output", children: t('releases.detail.results.output') }), _jsx(TableHead, { column: "source", children: t('releases.detail.results.source') }), _jsx(TableHead, { column: "variant", children: t('releases.detail.results.variant') }), _jsx(TableHead, { column: "latency", children: t('releases.detail.results.latency') }), _jsx(TableHead, { column: "tokens", children: t('releases.detail.results.tokens') }), _jsx(TableHead, { column: "createdAt", children: t('releases.detail.results.createdAt') })] }) }), _jsxs(TableBody, { children: [resultsLoading && rows.length === 0 ? (_jsx(TableEmpty, { children: _jsx(PlatformLoader, { className: "py-1", size: "sm" }) })) : null, resultsQuery.isError ? _jsx(TableEmpty, { children: t('releases.detail.results.loadFailed') }) : null, !resultsQuery.isLoading && !resultsQuery.isError && rows.length === 0 ? (_jsx(TableEmpty, { children: t('releases.detail.results.empty') })) : null, rows.map((row) => (_jsxs(TableRow, { children: [_jsx(TableCell, { column: "externalId", truncate: true, className: "font-mono text-[11.5px] text-muted-foreground", children: _jsx("span", { title: row.externalId ?? undefined, children: row.externalId ?? '—' }) }), _jsx(TableCell, { column: "input", truncate: 2, className: "text-[12px]", children: _jsx("span", { title: formatReleaseRunResultInput(row, 1000), children: formatReleaseRunResultInput(row, 220) }) }), _jsx(TableCell, { column: "output", truncate: 2, className: "text-[12px]", children: _jsx("span", { title: formatReleaseRunResultOutput(row, 1000), children: formatReleaseRunResultOutput(row, 220) }) }), _jsx(TableCell, { column: "source", children: _jsx(ReleaseRunResultLaneBadge, { lane: row.lane }) }), _jsx(TableCell, { column: "variant", className: "text-[12px]", children: _jsx(ReleaseRunResultVariant, { value: row }) }), _jsx(TableCell, { column: "latency", className: "font-mono text-[11.5px] text-muted-foreground", children: formatResultLatency(row.latencyMs) }), _jsx(TableCell, { column: "tokens", className: "font-mono text-[11.5px] text-muted-foreground", children: formatResultTokens(row) }), _jsx(TableCell, { column: "createdAt", className: "font-mono text-[11.5px] text-muted-foreground", children: formatDateTimeOrDash(row.createdAt) })] }, `${row.id}:${row.createdAt}`)))] })] }), _jsx(ResourcePaginationFooter, { summary: _jsx("span", { children: t('releases.detail.results.summary')
|
|
1006
|
+
}, disabled: releaseVersionOptions.length === 0 })] })] }), _jsxs(Table, { columns: RESULT_COLUMNS, children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { column: "externalId", children: t('releases.detail.results.externalId') }), _jsx(TableHead, { column: "input", children: t('releases.detail.results.input') }), _jsx(TableHead, { column: "output", children: t('releases.detail.results.output') }), _jsx(TableHead, { column: "source", children: t('releases.detail.results.source') }), _jsx(TableHead, { column: "version", children: t('releases.detail.results.version') }), _jsx(TableHead, { column: "latency", children: t('releases.detail.results.latency') }), _jsx(TableHead, { column: "tokens", children: t('releases.detail.results.tokens') }), _jsx(TableHead, { column: "createdAt", children: t('releases.detail.results.createdAt') })] }) }), _jsxs(TableBody, { children: [resultsLoading && rows.length === 0 ? (_jsx(TableEmpty, { children: _jsx(PlatformLoader, { className: "py-1", size: "sm" }) })) : null, resultsQuery.isError ? _jsx(TableEmpty, { children: t('releases.detail.results.loadFailed') }) : null, !resultsQuery.isLoading && !resultsQuery.isError && rows.length === 0 ? (_jsx(TableEmpty, { children: t('releases.detail.results.empty') })) : null, rows.map((row) => (_jsxs(TableRow, { children: [_jsx(TableCell, { column: "externalId", truncate: true, className: "font-mono text-[11.5px] text-muted-foreground", children: _jsx("span", { title: row.externalId ?? undefined, children: row.externalId ?? '—' }) }), _jsx(TableCell, { column: "input", truncate: 2, className: "text-[12px]", children: _jsx("span", { title: formatReleaseRunResultInput(row, 1000), children: formatReleaseRunResultInput(row, 220) }) }), _jsx(TableCell, { column: "output", truncate: 2, className: "text-[12px]", children: _jsx("span", { title: formatReleaseRunResultOutput(row, 1000), children: formatReleaseRunResultOutput(row, 220) }) }), _jsx(TableCell, { column: "source", children: _jsx(ReleaseRunResultLaneBadge, { lane: row.lane }) }), _jsx(TableCell, { column: "version", className: "text-[12px]", children: _jsx(ReleaseRunResultVersion, { value: row }) }), _jsx(TableCell, { column: "latency", className: "font-mono text-[11.5px] text-muted-foreground", children: formatResultLatency(row.latencyMs) }), _jsx(TableCell, { column: "tokens", className: "font-mono text-[11.5px] text-muted-foreground", children: formatResultTokens(row) }), _jsx(TableCell, { column: "createdAt", className: "font-mono text-[11.5px] text-muted-foreground", children: formatDateTimeOrDash(row.createdAt) })] }, `${row.id}:${row.createdAt}`)))] })] }), _jsx(ResourcePaginationFooter, { summary: _jsx("span", { children: t('releases.detail.results.summary')
|
|
588
1007
|
.replace('{from}', String(from))
|
|
589
1008
|
.replace('{to}', String(to))
|
|
590
1009
|
.replace('{total}', formatCount(total)) }), pageIndex: pageIndex, pageCount: pageCount, pageSize: pageSize, pageSizeOptions: RESULT_PAGE_SIZE_OPTIONS, previousPageLabel: t('common.previousPage'), nextPageLabel: t('common.nextPage'), onPageChange: setPageIndex, onPageSizeChange: (nextPageSize) => {
|
|
@@ -592,6 +1011,60 @@ function ResultsPane({ projectId, line, releaseEvents, initialReleaseVariantId,
|
|
|
592
1011
|
setPageIndex(0);
|
|
593
1012
|
} })] }));
|
|
594
1013
|
}
|
|
1014
|
+
function ResultReleaseVersionSelect({ id, options, value, onChange, disabled, }) {
|
|
1015
|
+
const { t } = useI18n();
|
|
1016
|
+
const [open, setOpen] = useState(false);
|
|
1017
|
+
const [query, setQuery] = useState('');
|
|
1018
|
+
const selectedOption = value === 'all' ? null : (options.find((option) => option.id === value) ?? null);
|
|
1019
|
+
const allLabel = t('releases.detail.results.versionFilter.all');
|
|
1020
|
+
const triggerLabel = selectedOption?.label ?? allLabel;
|
|
1021
|
+
const triggerDetail = selectedOption?.detail ?? null;
|
|
1022
|
+
const normalizedQuery = normalizeResultVersionSearch(query);
|
|
1023
|
+
const allOptionVisible = !normalizedQuery || resultVersionSearchIncludes(normalizedQuery, [allLabel, 'all']);
|
|
1024
|
+
const filteredOptions = useMemo(() => {
|
|
1025
|
+
if (!normalizedQuery)
|
|
1026
|
+
return options;
|
|
1027
|
+
return options.filter((option) => resultVersionSearchIncludes(normalizedQuery, [
|
|
1028
|
+
option.id,
|
|
1029
|
+
option.label,
|
|
1030
|
+
option.promptVersion,
|
|
1031
|
+
option.model,
|
|
1032
|
+
option.detail,
|
|
1033
|
+
]));
|
|
1034
|
+
}, [normalizedQuery, options]);
|
|
1035
|
+
function select(next) {
|
|
1036
|
+
onChange(next);
|
|
1037
|
+
setOpen(false);
|
|
1038
|
+
setQuery('');
|
|
1039
|
+
}
|
|
1040
|
+
return (_jsxs(Popover, { open: open, onOpenChange: (next) => {
|
|
1041
|
+
setOpen(next);
|
|
1042
|
+
if (!next)
|
|
1043
|
+
setQuery('');
|
|
1044
|
+
}, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { id: id, type: "button", variant: "outline", disabled: disabled, "data-testid": "release-result-version-filter", className: "h-auto min-h-10 w-full justify-between px-3 py-2 text-left sm:w-[340px]", children: [_jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: "block truncate font-mono text-[13px] font-semibold", children: triggerLabel }), triggerDetail ? (_jsx("span", { className: "mt-0.5 block truncate text-[11px] font-normal text-muted-foreground", children: triggerDetail })) : null] }), _jsx(ChevronDown, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" })] }) }), _jsxs(PopoverContent, { align: "end", sideOffset: 6, className: "w-[calc(100vw-2rem)] p-0 sm:w-[720px]", children: [_jsx(ResultDropdownSearchInput, { value: query, onChange: setQuery, placeholder: t('releases.detail.results.versionDropdown.search') }), _jsxs("div", { className: "max-h-[360px] overflow-y-auto p-1.5", children: [allOptionVisible ? (_jsxs("button", { type: "button", "data-testid": "release-result-version-option-all", onClick: () => select('all'), className: cn('flex w-full items-start gap-3 rounded-md px-3 py-2.5 text-left hover:bg-accent', value === 'all' && 'bg-primary/5'), children: [_jsx(ResultVersionSelectionCheck, { selected: value === 'all' }), _jsx("span", { className: "min-w-0 flex-1", children: _jsx("span", { className: "block min-w-0 truncate font-mono text-[13px] font-semibold", children: allLabel }) })] })) : null, filteredOptions.length === 0 && !allOptionVisible ? (_jsx("div", { className: "px-3 py-8 text-center text-[12px] text-muted-foreground", children: t('releases.detail.results.versionDropdown.noMatches') })) : (filteredOptions.map((option) => {
|
|
1045
|
+
const selected = option.id === value;
|
|
1046
|
+
return (_jsxs("button", { type: "button", "data-testid": `release-result-version-option-${option.id}`, onClick: () => select(option.id), className: cn('flex w-full items-start gap-3 rounded-md px-3 py-2.5 text-left hover:bg-accent', selected && 'bg-primary/5'), children: [_jsx(ResultVersionSelectionCheck, { selected: selected }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block min-w-0 truncate font-mono text-[13px] font-semibold", children: option.label }), _jsxs("span", { className: "mt-1 grid gap-x-4 gap-y-1 text-[11.5px] text-muted-foreground sm:grid-cols-3", children: [_jsx("span", { className: "min-w-0 truncate", children: _jsx(ResultDropdownFieldLabel, { label: t('releases.detail.results.versionDropdown.promptVersion'), value: option.promptVersion }) }), _jsx("span", { className: "min-w-0 truncate", children: _jsx(ResultDropdownFieldLabel, { label: t('releases.detail.results.versionDropdown.model'), value: option.model }) }), _jsx("span", { className: "min-w-0 truncate", children: _jsx(ResultDropdownFieldLabel, { label: t('releases.detail.results.versionDropdown.versionId'), value: formatShortId(option.id) }) })] })] })] }, option.id));
|
|
1047
|
+
}))] })] })] }));
|
|
1048
|
+
}
|
|
1049
|
+
function ResultDropdownSearchInput({ value, onChange, placeholder, }) {
|
|
1050
|
+
return (_jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-2", children: [_jsx(Search, { className: "size-3.5 text-muted-foreground", "aria-hidden": "true" }), _jsx(Input, { value: value, onChange: (event) => onChange(event.target.value), onKeyDown: (event) => event.stopPropagation(), placeholder: placeholder, "data-testid": "release-result-version-search", className: "h-8 border-0 bg-transparent px-0 text-[13px] shadow-none focus-visible:ring-0 focus-visible:ring-offset-0" })] }));
|
|
1051
|
+
}
|
|
1052
|
+
function ResultVersionSelectionCheck({ selected }) {
|
|
1053
|
+
return (_jsx("span", { className: cn('mt-0.5 inline-flex size-4 shrink-0 items-center justify-center rounded-full border', selected ? 'border-primary bg-primary text-primary-foreground' : 'border-muted-foreground/35 bg-background'), "aria-hidden": "true", children: _jsx(Check, { className: cn('size-3', selected ? 'opacity-100' : 'opacity-0') }) }));
|
|
1054
|
+
}
|
|
1055
|
+
function ResultDropdownFieldLabel({ label, value }) {
|
|
1056
|
+
return (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-muted-foreground", children: label }), _jsx("span", { className: "mx-1 text-muted-foreground/60", children: "-" }), _jsx("span", { className: "text-foreground", children: value })] }));
|
|
1057
|
+
}
|
|
1058
|
+
function normalizeResultVersionSearch(value) {
|
|
1059
|
+
return value.trim().toLowerCase();
|
|
1060
|
+
}
|
|
1061
|
+
function resultVersionSearchIncludes(query, parts) {
|
|
1062
|
+
return parts
|
|
1063
|
+
.filter((part) => part !== null && part !== undefined)
|
|
1064
|
+
.join(' ')
|
|
1065
|
+
.toLowerCase()
|
|
1066
|
+
.includes(query);
|
|
1067
|
+
}
|
|
595
1068
|
function getReleaseLineEventSourceIds(line, releaseEvents) {
|
|
596
1069
|
const ids = [
|
|
597
1070
|
...releaseEvents.flatMap((event) => [
|
|
@@ -628,103 +1101,7 @@ function getReleaseLineEventSources(line, releaseEvents) {
|
|
|
628
1101
|
function getReleaseResultSourceIds(line, releaseEvents) {
|
|
629
1102
|
return getReleaseLineEventSourceIds(line, releaseEvents);
|
|
630
1103
|
}
|
|
631
|
-
function
|
|
632
|
-
const baseById = new Map();
|
|
633
|
-
const eventsByVariant = new Map();
|
|
634
|
-
const addVariant = (variant) => {
|
|
635
|
-
baseById.set(variant.id, {
|
|
636
|
-
id: variant.id,
|
|
637
|
-
variantNumber: variant.variantNumber,
|
|
638
|
-
label: variant.label,
|
|
639
|
-
promptName: variant.promptName,
|
|
640
|
-
promptVersionId: variant.promptVersionId,
|
|
641
|
-
promptVersionLabel: variant.promptVersionLabel,
|
|
642
|
-
modelId: variant.modelId,
|
|
643
|
-
modelName: variant.modelName,
|
|
644
|
-
modelProvider: variant.modelProvider,
|
|
645
|
-
createdAt: variant.createdAt,
|
|
646
|
-
updatedAt: variant.updatedAt,
|
|
647
|
-
});
|
|
648
|
-
};
|
|
649
|
-
for (const variant of line.variants)
|
|
650
|
-
addVariant(variant);
|
|
651
|
-
for (const event of releaseEvents) {
|
|
652
|
-
if (!event.releaseVariantId)
|
|
653
|
-
continue;
|
|
654
|
-
const events = eventsByVariant.get(event.releaseVariantId) ?? [];
|
|
655
|
-
events.push(event);
|
|
656
|
-
eventsByVariant.set(event.releaseVariantId, events);
|
|
657
|
-
if (!baseById.has(event.releaseVariantId)) {
|
|
658
|
-
baseById.set(event.releaseVariantId, {
|
|
659
|
-
id: event.releaseVariantId,
|
|
660
|
-
variantNumber: event.releaseVariantNumber,
|
|
661
|
-
label: event.releaseVariantLabel ?? formatShortId(event.releaseVariantId),
|
|
662
|
-
promptName: event.promptName,
|
|
663
|
-
promptVersionId: event.promptVersionId,
|
|
664
|
-
promptVersionLabel: event.promptVersionLabel,
|
|
665
|
-
modelId: event.modelId,
|
|
666
|
-
modelName: event.modelName,
|
|
667
|
-
modelProvider: event.modelProvider,
|
|
668
|
-
createdAt: event.createdAt,
|
|
669
|
-
updatedAt: event.updatedAt,
|
|
670
|
-
});
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
const currentProductionVariantId = releaseEvents.find((event) => event.id === line.production?.currentEvent?.id)?.releaseVariantId ?? null;
|
|
674
|
-
const activeCanaryVariantId = line.canary?.releaseVariantId ??
|
|
675
|
-
releaseEvents.find((event) => event.id === line.canary?.id)?.releaseVariantId ??
|
|
676
|
-
null;
|
|
677
|
-
return [...baseById.values()]
|
|
678
|
-
.map((base) => {
|
|
679
|
-
const events = (eventsByVariant.get(base.id) ?? []).sort((left, right) => timeValue(right.createdAt) - timeValue(left.createdAt));
|
|
680
|
-
return {
|
|
681
|
-
...base,
|
|
682
|
-
createdAt: minDateString([base.createdAt, ...events.map((event) => event.createdAt)]),
|
|
683
|
-
updatedAt: maxDateString([base.updatedAt, ...events.map((event) => event.updatedAt ?? event.createdAt)]),
|
|
684
|
-
stage: resolveReleaseVariantStage(base.id, currentProductionVariantId, activeCanaryVariantId, events),
|
|
685
|
-
events,
|
|
686
|
-
productionEventCount: events.filter((event) => event.laneType === 'production').length,
|
|
687
|
-
canaryEventCount: events.filter((event) => event.laneType === 'canary').length,
|
|
688
|
-
totalProcessed: events.reduce((sum, event) => sum + event.totalProcessed, 0),
|
|
689
|
-
totalErrors: events.reduce((sum, event) => sum + event.totalErrors, 0),
|
|
690
|
-
};
|
|
691
|
-
})
|
|
692
|
-
.sort((left, right) => {
|
|
693
|
-
if (left.variantNumber !== null && right.variantNumber !== null)
|
|
694
|
-
return left.variantNumber - right.variantNumber;
|
|
695
|
-
if (left.variantNumber !== null)
|
|
696
|
-
return -1;
|
|
697
|
-
if (right.variantNumber !== null)
|
|
698
|
-
return 1;
|
|
699
|
-
return left.label.localeCompare(right.label, undefined, { numeric: true });
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
function resolveReleaseVariantStage(releaseVariantId, currentProductionVariantId, activeCanaryVariantId, events) {
|
|
703
|
-
const isProduction = currentProductionVariantId === releaseVariantId ||
|
|
704
|
-
events.some((event) => event.laneType === 'production' && event.status === 'running');
|
|
705
|
-
const isCanary = activeCanaryVariantId === releaseVariantId ||
|
|
706
|
-
events.some((event) => event.laneType === 'canary' && (event.status === 'running' || event.status === 'stopped'));
|
|
707
|
-
if (isProduction && isCanary)
|
|
708
|
-
return 'production_canary';
|
|
709
|
-
if (isProduction)
|
|
710
|
-
return 'production';
|
|
711
|
-
if (isCanary)
|
|
712
|
-
return 'canary';
|
|
713
|
-
return 'history';
|
|
714
|
-
}
|
|
715
|
-
function minDateString(values) {
|
|
716
|
-
const dates = values.filter((value) => Boolean(value));
|
|
717
|
-
if (dates.length === 0)
|
|
718
|
-
return null;
|
|
719
|
-
return dates.reduce((min, value) => (timeValue(value) < timeValue(min) ? value : min));
|
|
720
|
-
}
|
|
721
|
-
function maxDateString(values) {
|
|
722
|
-
const dates = values.filter((value) => Boolean(value));
|
|
723
|
-
if (dates.length === 0)
|
|
724
|
-
return null;
|
|
725
|
-
return dates.reduce((max, value) => (timeValue(value) > timeValue(max) ? value : max));
|
|
726
|
-
}
|
|
727
|
-
function getReleaseResultVariantOptions(line, releaseEvents) {
|
|
1104
|
+
function getReleaseResultVersionOptions(line, releaseEvents) {
|
|
728
1105
|
const options = new Map();
|
|
729
1106
|
const add = (input) => {
|
|
730
1107
|
if (!input.id)
|
|
@@ -734,23 +1111,25 @@ function getReleaseResultVariantOptions(line, releaseEvents) {
|
|
|
734
1111
|
options.set(input.id, {
|
|
735
1112
|
id: input.id,
|
|
736
1113
|
label: input.label?.trim() || formatShortId(input.id),
|
|
1114
|
+
promptVersion,
|
|
1115
|
+
model,
|
|
737
1116
|
detail: `${promptVersion} · ${model}`,
|
|
738
1117
|
});
|
|
739
1118
|
};
|
|
740
|
-
for (const
|
|
1119
|
+
for (const version of line.versions) {
|
|
741
1120
|
add({
|
|
742
|
-
id:
|
|
743
|
-
label:
|
|
744
|
-
promptVersionLabel:
|
|
745
|
-
promptVersionId:
|
|
746
|
-
modelName:
|
|
747
|
-
modelId:
|
|
1121
|
+
id: version.id,
|
|
1122
|
+
label: version.label,
|
|
1123
|
+
promptVersionLabel: version.promptVersionLabel,
|
|
1124
|
+
promptVersionId: version.promptVersionId,
|
|
1125
|
+
modelName: version.modelName,
|
|
1126
|
+
modelId: version.modelId,
|
|
748
1127
|
});
|
|
749
1128
|
}
|
|
750
1129
|
for (const event of releaseEvents) {
|
|
751
1130
|
add({
|
|
752
|
-
id: event.
|
|
753
|
-
label: event.
|
|
1131
|
+
id: event.releaseVersionId,
|
|
1132
|
+
label: event.releaseVersionLabel,
|
|
754
1133
|
promptVersionLabel: event.promptVersionLabel,
|
|
755
1134
|
promptVersionId: event.promptVersionId,
|
|
756
1135
|
modelName: event.modelName,
|
|
@@ -759,20 +1138,6 @@ function getReleaseResultVariantOptions(line, releaseEvents) {
|
|
|
759
1138
|
}
|
|
760
1139
|
return [...options.values()].sort((left, right) => left.label.localeCompare(right.label, undefined, { numeric: true }));
|
|
761
1140
|
}
|
|
762
|
-
function getReleaseResultPromptVersionOptions(line, releaseEvents) {
|
|
763
|
-
const options = new Map();
|
|
764
|
-
const add = (id, label) => {
|
|
765
|
-
if (!id)
|
|
766
|
-
return;
|
|
767
|
-
options.set(id, label?.trim() || formatShortId(id));
|
|
768
|
-
};
|
|
769
|
-
add(line.production?.currentEvent?.promptVersionId, line.productionVersionLabel);
|
|
770
|
-
add(line.canary?.promptVersionId, line.canaryVersionLabel);
|
|
771
|
-
for (const event of releaseEvents) {
|
|
772
|
-
add(event.promptVersionId, event.promptVersionLabel);
|
|
773
|
-
}
|
|
774
|
-
return [...options.entries()].map(([id, label]) => ({ id, label }));
|
|
775
|
-
}
|
|
776
1141
|
function formatReleaseRunResultInput(row, maxLength) {
|
|
777
1142
|
return compactReleaseRunResultValue(row.inputVariables, maxLength);
|
|
778
1143
|
}
|
|
@@ -849,125 +1214,1314 @@ function ReleaseRunResultLaneBadge({ lane }) {
|
|
|
849
1214
|
: 'color-mix(in srgb, var(--src-canary) 30%, transparent)',
|
|
850
1215
|
}, children: t(isProduction ? 'releases.detail.results.lane.production' : 'releases.detail.results.lane.canary') }));
|
|
851
1216
|
}
|
|
852
|
-
function
|
|
853
|
-
const label = value.
|
|
1217
|
+
function ReleaseRunResultVersion({ value }) {
|
|
1218
|
+
const label = value.releaseVersionLabel ?? formatShortId(value.releaseVersionId);
|
|
854
1219
|
const promptVersion = formatReleaseRunResultPromptVersion(value);
|
|
855
1220
|
const model = value.modelName ?? formatShortId(value.modelId);
|
|
856
1221
|
return (_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate font-mono text-[11.5px] font-semibold", children: label }), _jsxs("div", { className: "mt-0.5 truncate text-[11.5px] text-muted-foreground", children: [promptVersion, " \u00B7 ", model] })] }));
|
|
857
1222
|
}
|
|
858
|
-
function QualityMetricsPane({
|
|
1223
|
+
function QualityMetricsPane({ line, releaseEvents }) {
|
|
859
1224
|
const { t } = useI18n();
|
|
860
|
-
const
|
|
861
|
-
const
|
|
862
|
-
const
|
|
863
|
-
const
|
|
864
|
-
const
|
|
865
|
-
const
|
|
866
|
-
const
|
|
867
|
-
const
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1225
|
+
const [selectedVersionIds, setSelectedVersionIds] = useState(null);
|
|
1226
|
+
const [selectedScopeIds, setSelectedScopeIds] = useState(null);
|
|
1227
|
+
const [selectedMetricIds, setSelectedMetricIds] = useState(['f1']);
|
|
1228
|
+
const overallLabel = t('releases.detail.quality.scope.overall');
|
|
1229
|
+
const qualityPoints = useMemo(() => buildReleaseQualityPoints(releaseEvents, overallLabel), [overallLabel, releaseEvents]);
|
|
1230
|
+
const versionOptions = useMemo(() => buildQualityVersionOptions(qualityPoints), [qualityPoints]);
|
|
1231
|
+
const scopeOptions = useMemo(() => buildQualityScopeOptions(qualityPoints), [qualityPoints]);
|
|
1232
|
+
const metricOptions = useMemo(() => QUALITY_METRIC_OPTIONS.map((option) => ({ id: option.key, label: t(option.labelKey) })), [t]);
|
|
1233
|
+
const activeVersionIds = useMemo(() => {
|
|
1234
|
+
const available = versionOptions.map((option) => option.id);
|
|
1235
|
+
if (selectedVersionIds === null)
|
|
1236
|
+
return available;
|
|
1237
|
+
return selectedVersionIds.filter((id) => available.includes(id));
|
|
1238
|
+
}, [selectedVersionIds, versionOptions]);
|
|
1239
|
+
const activeScopes = resolveActiveQualityScopes(selectedScopeIds, scopeOptions);
|
|
1240
|
+
const activeMetrics = resolveActiveQualityMetrics(selectedMetricIds, metricOptions);
|
|
1241
|
+
const visiblePoints = useMemo(() => filterQualityPoints(qualityPoints, activeVersionIds, activeScopes), [activeScopes, activeVersionIds, qualityPoints]);
|
|
1242
|
+
const chartSeries = useMemo(() => buildQualityChartSeries(visiblePoints, activeMetrics, activeScopes), [activeMetrics, activeScopes, visiblePoints]);
|
|
1243
|
+
const chartAxisData = useMemo(() => buildQualityChartAxisData(chartSeries), [chartSeries]);
|
|
1244
|
+
const annotationHref = buildQualityAnnotationHref(line, releaseEvents);
|
|
1245
|
+
return (_jsxs("section", { className: "space-y-4", "data-testid": "release-quality-metrics-pane", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "text-[14px] font-semibold", children: t('releases.detail.quality.title') }), _jsx("p", { className: "mt-1 max-w-3xl text-[12px] leading-5 text-muted-foreground", children: t('releases.detail.quality.description') })] }), _jsxs("div", { className: "overflow-hidden rounded-lg border bg-card shadow-sm", children: [qualityPoints.length > 0 ? (_jsxs("div", { className: "flex flex-wrap items-end gap-3 border-b px-4 py-3", children: [_jsx(QualityVersionFilter, { options: versionOptions, activeIds: activeVersionIds, onChange: setSelectedVersionIds }), _jsx(QualityScopeFilter, { options: scopeOptions, activeIds: activeScopes.map((scope) => scope.id), onChange: setSelectedScopeIds }), _jsx(QualityMetricFilter, { options: metricOptions, activeIds: activeMetrics.map((metric) => metric.id), onChange: setSelectedMetricIds })] })) : null, _jsx("div", { className: "px-4 pb-2 pt-4", children: _jsx(QualityMetricsChart, { axisData: chartAxisData, series: chartSeries, children: chartSeries.length === 0 ? (qualityPoints.length === 0 ? (_jsx(QualityEmptyChartMessage, { annotationHref: annotationHref })) : (_jsx(QualityFilteredEmptyChartMessage, {}))) : null }) }), chartSeries.length > 0 ? _jsx(QualityLegend, { series: chartSeries }) : null] })] }));
|
|
1246
|
+
}
|
|
1247
|
+
function QualityVersionFilter({ options, activeIds, onChange, }) {
|
|
874
1248
|
const { t } = useI18n();
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
];
|
|
878
|
-
|
|
1249
|
+
const [open, setOpen] = useState(false);
|
|
1250
|
+
const [query, setQuery] = useState('');
|
|
1251
|
+
const activeSet = useMemo(() => new Set(activeIds), [activeIds]);
|
|
1252
|
+
const normalizedQuery = normalizeQualitySearch(query);
|
|
1253
|
+
const latestOption = useMemo(() => [...options].sort((left, right) => timeValue(right.latestAt) - timeValue(left.latestAt))[0] ?? null, [options]);
|
|
1254
|
+
const productionVersionIds = useMemo(() => options.filter((option) => option.kind === 'production').map((option) => option.id), [options]);
|
|
1255
|
+
const canaryVersionIds = useMemo(() => options.filter((option) => option.kind !== 'production').map((option) => option.id), [options]);
|
|
1256
|
+
const selectedOptions = options.filter((option) => activeSet.has(option.id));
|
|
1257
|
+
const allSelected = options.length > 0 && selectedOptions.length === options.length;
|
|
1258
|
+
const productionSelected = productionVersionIds.length > 0 &&
|
|
1259
|
+
selectedOptions.length === productionVersionIds.length &&
|
|
1260
|
+
productionVersionIds.every((id) => activeSet.has(id));
|
|
1261
|
+
const canarySelected = canaryVersionIds.length > 0 &&
|
|
1262
|
+
selectedOptions.length === canaryVersionIds.length &&
|
|
1263
|
+
canaryVersionIds.every((id) => activeSet.has(id));
|
|
1264
|
+
let triggerLabel = formatTemplate(t('releases.detail.quality.filter.selectedVersions'), {
|
|
1265
|
+
count: formatCount(selectedOptions.length),
|
|
1266
|
+
});
|
|
1267
|
+
if (allSelected) {
|
|
1268
|
+
triggerLabel = t('releases.detail.quality.filter.allVersions');
|
|
1269
|
+
}
|
|
1270
|
+
else if (productionSelected) {
|
|
1271
|
+
triggerLabel = t('releases.detail.quality.filter.allProductionVersions');
|
|
1272
|
+
}
|
|
1273
|
+
else if (canarySelected) {
|
|
1274
|
+
triggerLabel = t('releases.detail.quality.filter.allCanaryVersions');
|
|
1275
|
+
}
|
|
1276
|
+
else if (selectedOptions.length === 1) {
|
|
1277
|
+
triggerLabel = selectedOptions[0]?.label ?? triggerLabel;
|
|
1278
|
+
}
|
|
1279
|
+
const filteredOptions = useMemo(() => {
|
|
1280
|
+
if (!normalizedQuery)
|
|
1281
|
+
return options;
|
|
1282
|
+
return options.filter((option) => qualitySearchIncludes(normalizedQuery, [option.label, option.promptVersion, option.model, option.id]));
|
|
1283
|
+
}, [normalizedQuery, options]);
|
|
1284
|
+
function commit(next) {
|
|
1285
|
+
onChange(next.length === options.length ? null : next);
|
|
1286
|
+
}
|
|
1287
|
+
return (_jsxs("div", { className: "flex min-w-[220px] flex-col gap-1.5", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: t('releases.detail.quality.filter.version') }), _jsxs(Popover, { open: open, onOpenChange: (next) => {
|
|
1288
|
+
setOpen(next);
|
|
1289
|
+
if (!next)
|
|
1290
|
+
setQuery('');
|
|
1291
|
+
}, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", disabled: options.length === 0, className: "h-10 justify-between gap-2 px-3 text-left", children: [_jsx("span", { className: "min-w-0 truncate font-mono text-[13px] font-semibold", children: triggerLabel }), _jsx(ChevronDown, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true })] }) }), _jsxs(PopoverContent, { align: "start", sideOffset: 6, className: "w-[calc(100vw-2rem)] p-0 sm:w-[560px]", children: [_jsx(ResultDropdownSearchInput, { value: query, onChange: setQuery, placeholder: t('releases.detail.quality.filter.versionSearch') }), _jsxs("div", { className: "flex flex-wrap gap-x-3 gap-y-1.5 border-b px-3 py-2 text-[12px]", children: [_jsx("button", { type: "button", className: "font-medium text-primary", onClick: () => onChange(null), children: t('releases.detail.quality.filter.selectAll') }), _jsx("button", { type: "button", className: "font-medium text-primary disabled:text-muted-foreground", disabled: !latestOption, onClick: () => {
|
|
1292
|
+
if (latestOption)
|
|
1293
|
+
onChange([latestOption.id]);
|
|
1294
|
+
}, children: t('releases.detail.quality.filter.latestOnly') }), _jsx("button", { type: "button", className: "font-medium text-primary disabled:text-muted-foreground", disabled: productionVersionIds.length === 0, onClick: () => commit(productionVersionIds), children: t('releases.detail.quality.filter.allProductionVersions') }), _jsx("button", { type: "button", className: "font-medium text-primary disabled:text-muted-foreground", disabled: canaryVersionIds.length === 0, onClick: () => commit(canaryVersionIds), children: t('releases.detail.quality.filter.allCanaryVersions') })] }), _jsx("div", { className: "max-h-[320px] overflow-y-auto p-1.5", children: filteredOptions.length === 0 ? (_jsx("div", { className: "px-3 py-8 text-center text-[12px] text-muted-foreground", children: t('releases.detail.quality.filter.noVersions') })) : (filteredOptions.map((option) => {
|
|
1295
|
+
const selected = activeSet.has(option.id);
|
|
1296
|
+
return (_jsxs("button", { type: "button", onClick: () => commit(toggleQualityFilterValue(activeIds, option.id)), className: cn('flex w-full items-start gap-3 rounded-md px-3 py-2.5 text-left hover:bg-accent', selected && 'bg-primary/5'), children: [_jsx(ResultVersionSelectionCheck, { selected: selected }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsxs("span", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [_jsx("span", { className: "min-w-0 truncate font-mono text-[13px] font-semibold", children: option.label }), _jsx(QualityVersionKindBadge, { kind: option.kind })] }), _jsxs("span", { className: "mt-1 grid gap-x-4 gap-y-1 text-[11.5px] text-muted-foreground sm:grid-cols-3", children: [_jsx("span", { className: "min-w-0 truncate", children: _jsx(ResultDropdownFieldLabel, { label: t('releases.detail.results.versionDropdown.promptVersion'), value: option.promptVersion }) }), _jsx("span", { className: "min-w-0 truncate", children: _jsx(ResultDropdownFieldLabel, { label: t('releases.detail.results.versionDropdown.model'), value: option.model }) }), _jsx("span", { className: "min-w-0 truncate", children: _jsx(ResultDropdownFieldLabel, { label: t('releases.detail.quality.filter.points'), value: formatCount(option.pointCount) }) })] })] })] }, option.id));
|
|
1297
|
+
})) })] })] })] }));
|
|
1298
|
+
}
|
|
1299
|
+
function QualityScopeFilter({ options, activeIds, onChange, }) {
|
|
1300
|
+
return (_jsx(QualityMultiSelectFilter, { labelKey: "releases.detail.quality.filter.scope", options: options, activeIds: activeIds, onChange: onChange, allLabelKey: "releases.detail.quality.filter.allScopes", selectedLabelKey: "releases.detail.quality.filter.selectedScopes", emptyLabelKey: "releases.detail.quality.filter.scopeEmpty", minWidthClassName: "min-w-[190px]" }));
|
|
879
1301
|
}
|
|
880
|
-
function
|
|
1302
|
+
function QualityMetricFilter({ options, activeIds, onChange, }) {
|
|
1303
|
+
return (_jsx(QualityMultiSelectFilter, { labelKey: "releases.detail.quality.filter.metric", options: options, activeIds: activeIds, onChange: onChange, allLabelKey: "releases.detail.quality.filter.allMetrics", selectedLabelKey: "releases.detail.quality.filter.selectedMetrics", emptyLabelKey: "releases.detail.quality.filter.metricEmpty", minWidthClassName: "min-w-[170px]" }));
|
|
1304
|
+
}
|
|
1305
|
+
function QualityMultiSelectFilter({ labelKey, options, activeIds, onChange, allLabelKey, selectedLabelKey, emptyLabelKey, minWidthClassName, }) {
|
|
881
1306
|
const { t } = useI18n();
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1307
|
+
const [open, setOpen] = useState(false);
|
|
1308
|
+
const activeSet = useMemo(() => new Set(activeIds), [activeIds]);
|
|
1309
|
+
const selectedOptions = options.filter((option) => activeSet.has(option.id));
|
|
1310
|
+
const allSelected = options.length > 0 && selectedOptions.length === options.length;
|
|
1311
|
+
const triggerLabel = allSelected
|
|
1312
|
+
? t(allLabelKey)
|
|
1313
|
+
: selectedOptions.length === 1
|
|
1314
|
+
? selectedOptions[0]?.label
|
|
1315
|
+
: selectedOptions.length > 1
|
|
1316
|
+
? formatTemplate(t(selectedLabelKey), { count: formatCount(selectedOptions.length) })
|
|
1317
|
+
: t(emptyLabelKey);
|
|
1318
|
+
function commit(next) {
|
|
1319
|
+
if (next.length === 0)
|
|
1320
|
+
return;
|
|
1321
|
+
onChange(next);
|
|
1322
|
+
}
|
|
1323
|
+
return (_jsxs("div", { className: cn('flex flex-col gap-1.5', minWidthClassName), children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: t(labelKey) }), _jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", disabled: options.length === 0, className: "h-10 justify-between gap-2 px-3 text-left", children: [_jsx("span", { className: "min-w-0 truncate text-[13px] font-semibold", children: triggerLabel }), _jsx(ChevronDown, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true })] }) }), _jsxs(PopoverContent, { align: "start", sideOffset: 6, className: "w-64 p-0", children: [_jsx("div", { className: "flex gap-3 border-b px-3 py-2 text-[12px]", children: _jsx("button", { type: "button", className: "font-medium text-primary disabled:text-muted-foreground", disabled: allSelected, onClick: () => onChange(options.map((option) => option.id)), children: t('releases.detail.quality.filter.selectAll') }) }), _jsx("div", { className: "max-h-[260px] overflow-y-auto p-1.5", children: options.length === 0 ? (_jsx("div", { className: "px-3 py-8 text-center text-[12px] text-muted-foreground", children: t(emptyLabelKey) })) : (options.map((option) => {
|
|
1324
|
+
const selected = activeSet.has(option.id);
|
|
1325
|
+
return (_jsxs("button", { type: "button", onClick: () => commit(toggleQualityFilterValue(activeIds, option.id)), className: cn('flex w-full items-center gap-3 rounded-md px-3 py-2 text-left hover:bg-accent', selected && 'bg-primary/5'), children: [_jsx(ResultVersionSelectionCheck, { selected: selected }), _jsx("span", { className: "min-w-0 flex-1 truncate text-[13px] font-medium", children: option.label }), option.meta ? (_jsx("span", { className: "shrink-0 text-[11px] text-muted-foreground", children: option.meta })) : null] }, option.id));
|
|
1326
|
+
})) })] })] })] }));
|
|
1327
|
+
}
|
|
1328
|
+
function QualityVersionKindBadge({ kind }) {
|
|
1329
|
+
const { t } = useI18n();
|
|
1330
|
+
const isProduction = kind === 'production';
|
|
1331
|
+
return (_jsx("span", { className: "inline-flex shrink-0 items-center rounded-full border px-2 py-0.5 text-[10.5px] font-medium leading-4", style: {
|
|
1332
|
+
background: isProduction ? 'var(--src-prod-soft)' : 'var(--src-canary-soft)',
|
|
1333
|
+
color: isProduction ? 'var(--src-prod-fg)' : 'var(--src-canary-fg)',
|
|
1334
|
+
borderColor: isProduction
|
|
1335
|
+
? 'color-mix(in srgb, var(--src-prod) 30%, transparent)'
|
|
1336
|
+
: 'color-mix(in srgb, var(--src-canary) 30%, transparent)',
|
|
1337
|
+
}, children: t(isProduction
|
|
1338
|
+
? 'releases.detail.history.versionKind.production'
|
|
1339
|
+
: 'releases.detail.history.versionKind.candidate') }));
|
|
1340
|
+
}
|
|
1341
|
+
function QualityLegend({ series }) {
|
|
1342
|
+
const { t } = useI18n();
|
|
1343
|
+
return (_jsxs("div", { className: "flex flex-wrap items-center gap-x-5 gap-y-2 border-t px-4 py-3 text-[11.5px] text-muted-foreground", children: [series.map((item) => (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx("span", { className: "h-0.5 w-5 rounded-full", style: { background: item.color }, "aria-hidden": true }), item.label] }, item.id))), _jsxs("span", { className: "inline-flex items-center gap-1.5 border-l pl-4", children: [_jsx("span", { className: "size-2.5 rounded-full bg-[var(--src-prod)]", "aria-hidden": true }), t('releases.detail.quality.legend.production')] }), _jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx("span", { className: "size-2.5 rounded-full border-2 border-[var(--src-canary)] bg-card", "aria-hidden": true }), t('releases.detail.quality.legend.canary')] })] }));
|
|
1344
|
+
}
|
|
1345
|
+
function readChartCssColor(name, fallback) {
|
|
1346
|
+
if (typeof window === 'undefined')
|
|
1347
|
+
return fallback;
|
|
1348
|
+
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
1349
|
+
return value || fallback;
|
|
1350
|
+
}
|
|
1351
|
+
function resolveChartColor(value, fallback) {
|
|
1352
|
+
const variableName = value.match(/var\((--[^),\s]+)/)?.[1];
|
|
1353
|
+
return variableName ? readChartCssColor(variableName, fallback) : value;
|
|
1354
|
+
}
|
|
1355
|
+
function escapeHtml(value) {
|
|
1356
|
+
return value.replace(/[&<>"']/g, (char) => {
|
|
1357
|
+
const escaped = {
|
|
1358
|
+
'&': '&',
|
|
1359
|
+
'<': '<',
|
|
1360
|
+
'>': '>',
|
|
1361
|
+
'"': '"',
|
|
1362
|
+
"'": ''',
|
|
1363
|
+
};
|
|
1364
|
+
return escaped[char] ?? char;
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
function resolveQualityPercentAxisExtent(extent) {
|
|
1368
|
+
if (!Number.isFinite(extent.min) || !Number.isFinite(extent.max)) {
|
|
1369
|
+
return { min: 0, max: 100 };
|
|
1370
|
+
}
|
|
1371
|
+
const dataMin = Math.max(0, Math.min(100, Math.min(extent.min, extent.max)));
|
|
1372
|
+
const dataMax = Math.max(0, Math.min(100, Math.max(extent.min, extent.max)));
|
|
1373
|
+
const dataSpan = dataMax - dataMin;
|
|
1374
|
+
const padding = Math.max(2, dataSpan * 0.2);
|
|
1375
|
+
let nextMin = Math.max(0, dataMin - padding);
|
|
1376
|
+
let nextMax = Math.min(100, dataMax + padding);
|
|
1377
|
+
if (nextMax - nextMin < 10) {
|
|
1378
|
+
const center = (dataMin + dataMax) / 2;
|
|
1379
|
+
nextMin = Math.max(0, center - 5);
|
|
1380
|
+
nextMax = Math.min(100, center + 5);
|
|
1381
|
+
if (nextMin === 0)
|
|
1382
|
+
nextMax = Math.min(100, Math.max(10, nextMax));
|
|
1383
|
+
if (nextMax === 100)
|
|
1384
|
+
nextMin = Math.max(0, Math.min(90, nextMin));
|
|
1385
|
+
}
|
|
1386
|
+
const roundedMin = Math.max(0, Math.floor(nextMin / 5) * 5);
|
|
1387
|
+
const roundedMax = Math.min(100, Math.ceil(nextMax / 5) * 5);
|
|
1388
|
+
return roundedMax > roundedMin ? { min: roundedMin, max: roundedMax } : { min: 0, max: 100 };
|
|
1389
|
+
}
|
|
1390
|
+
function getQualityPercentAxisMin(extent) {
|
|
1391
|
+
return resolveQualityPercentAxisExtent(extent).min;
|
|
1392
|
+
}
|
|
1393
|
+
function getQualityPercentAxisMax(extent) {
|
|
1394
|
+
return resolveQualityPercentAxisExtent(extent).max;
|
|
1395
|
+
}
|
|
1396
|
+
function QualityMetricsChart({ axisData, series, children, }) {
|
|
896
1397
|
const { t } = useI18n();
|
|
897
1398
|
const formatDateTimeOrDash = useDateTimeOrDash();
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
const
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1399
|
+
const chartRef = useRef(null);
|
|
1400
|
+
const chartInstanceRef = useRef(null);
|
|
1401
|
+
const [chartColorVersion, setChartColorVersion] = useState(0);
|
|
1402
|
+
useEffect(() => {
|
|
1403
|
+
if (typeof MutationObserver === 'undefined')
|
|
1404
|
+
return undefined;
|
|
1405
|
+
const observer = new MutationObserver(() => setChartColorVersion((current) => current + 1));
|
|
1406
|
+
observer.observe(document.documentElement, {
|
|
1407
|
+
attributes: true,
|
|
1408
|
+
attributeFilter: ['class', 'style', 'data-theme'],
|
|
1409
|
+
});
|
|
1410
|
+
return () => observer.disconnect();
|
|
1411
|
+
}, []);
|
|
1412
|
+
const chartOption = useMemo(() => {
|
|
1413
|
+
void chartColorVersion;
|
|
1414
|
+
const foregroundColor = readChartCssColor('--foreground', '#e5e7eb');
|
|
1415
|
+
const mutedColor = readChartCssColor('--muted-foreground', '#94a3b8');
|
|
1416
|
+
const borderColor = readChartCssColor('--border', '#1f2937');
|
|
1417
|
+
const cardColor = readChartCssColor('--card', '#020617');
|
|
1418
|
+
const popoverColor = readChartCssColor('--popover', cardColor);
|
|
1419
|
+
const primaryColor = readChartCssColor('--primary', '#60a5fa');
|
|
1420
|
+
const productionColor = readChartCssColor('--src-prod', '#22c55e');
|
|
1421
|
+
const canaryColor = readChartCssColor('--src-canary', '#3b82f6');
|
|
1422
|
+
const categoryLabels = axisData.map((point) => point.releaseVersionLabel);
|
|
1423
|
+
const qualitySeries = series.map((item) => {
|
|
1424
|
+
const pointByEvent = new Map(item.points.map((point) => [point.eventId, point]));
|
|
1425
|
+
const lineColor = resolveChartColor(item.color, primaryColor);
|
|
1426
|
+
return {
|
|
1427
|
+
id: item.id,
|
|
1428
|
+
name: item.label,
|
|
1429
|
+
type: 'line',
|
|
1430
|
+
smooth: true,
|
|
1431
|
+
connectNulls: true,
|
|
1432
|
+
showSymbol: true,
|
|
1433
|
+
symbol: 'circle',
|
|
1434
|
+
symbolSize: 8,
|
|
1435
|
+
lineStyle: {
|
|
1436
|
+
width: 2,
|
|
1437
|
+
color: lineColor,
|
|
1438
|
+
opacity: 1,
|
|
1439
|
+
},
|
|
1440
|
+
itemStyle: {
|
|
1441
|
+
color: lineColor,
|
|
1442
|
+
opacity: 1,
|
|
1443
|
+
},
|
|
1444
|
+
emphasis: {
|
|
1445
|
+
disabled: true,
|
|
1446
|
+
},
|
|
1447
|
+
blur: {
|
|
1448
|
+
lineStyle: {
|
|
1449
|
+
opacity: 1,
|
|
1450
|
+
},
|
|
1451
|
+
itemStyle: {
|
|
1452
|
+
opacity: 1,
|
|
1453
|
+
},
|
|
1454
|
+
},
|
|
1455
|
+
data: axisData.map((axisPoint) => {
|
|
1456
|
+
const point = pointByEvent.get(axisPoint.eventId);
|
|
1457
|
+
if (!point)
|
|
1458
|
+
return null;
|
|
1459
|
+
const laneColor = point.lane === 'production' ? productionColor : canaryColor;
|
|
1460
|
+
return {
|
|
1461
|
+
value: point.value,
|
|
1462
|
+
qualityPoint: point,
|
|
1463
|
+
symbol: 'circle',
|
|
1464
|
+
symbolSize: 8,
|
|
1465
|
+
itemStyle: {
|
|
1466
|
+
color: point.lane === 'production' ? laneColor : cardColor,
|
|
1467
|
+
borderColor: laneColor,
|
|
1468
|
+
borderWidth: 2,
|
|
1469
|
+
},
|
|
1470
|
+
};
|
|
1471
|
+
}),
|
|
1472
|
+
};
|
|
1473
|
+
});
|
|
1474
|
+
return {
|
|
1475
|
+
animation: false,
|
|
1476
|
+
backgroundColor: 'transparent',
|
|
1477
|
+
grid: {
|
|
1478
|
+
top: 26,
|
|
1479
|
+
right: 24,
|
|
1480
|
+
bottom: axisData.length > 1 ? 62 : 38,
|
|
1481
|
+
left: 48,
|
|
1482
|
+
containLabel: false,
|
|
1483
|
+
},
|
|
1484
|
+
tooltip: {
|
|
1485
|
+
trigger: 'axis',
|
|
1486
|
+
appendToBody: true,
|
|
1487
|
+
confine: true,
|
|
1488
|
+
borderWidth: 1,
|
|
1489
|
+
borderColor,
|
|
1490
|
+
backgroundColor: popoverColor,
|
|
1491
|
+
textStyle: {
|
|
1492
|
+
color: foregroundColor,
|
|
1493
|
+
fontSize: 12,
|
|
1494
|
+
fontFamily: 'inherit',
|
|
1495
|
+
},
|
|
1496
|
+
extraCssText: 'box-shadow: 0 12px 30px rgba(15, 23, 42, 0.22); border-radius: 6px;',
|
|
1497
|
+
axisPointer: {
|
|
1498
|
+
type: 'line',
|
|
1499
|
+
lineStyle: {
|
|
1500
|
+
color: borderColor,
|
|
1501
|
+
type: 'dashed',
|
|
1502
|
+
},
|
|
1503
|
+
},
|
|
1504
|
+
formatter: (rawParams) => {
|
|
1505
|
+
const params = (Array.isArray(rawParams) ? rawParams : [rawParams]);
|
|
1506
|
+
const points = params
|
|
1507
|
+
.map((param) => param.data?.qualityPoint)
|
|
1508
|
+
.filter((point) => Boolean(point));
|
|
1509
|
+
const point = points[0];
|
|
1510
|
+
if (!point)
|
|
1511
|
+
return '';
|
|
1512
|
+
const kindText = t(point.releaseVersionKind === 'production'
|
|
1513
|
+
? 'releases.detail.history.versionKind.production'
|
|
1514
|
+
: 'releases.detail.history.versionKind.candidate');
|
|
1515
|
+
const rows = params
|
|
1516
|
+
.map((param) => {
|
|
1517
|
+
const qualityPoint = param.data?.qualityPoint;
|
|
1518
|
+
if (!qualityPoint)
|
|
1519
|
+
return '';
|
|
1520
|
+
const sampleCount = qualityPoint.sampleCount === null
|
|
1521
|
+
? '—'
|
|
1522
|
+
: formatTemplate(t('releases.detail.quality.sampleCountShort'), {
|
|
1523
|
+
count: formatCount(qualityPoint.sampleCount),
|
|
1524
|
+
});
|
|
1525
|
+
return [
|
|
1526
|
+
'<div style="display:flex;align-items:center;gap:8px;min-width:260px;margin-top:4px;">',
|
|
1527
|
+
param.marker ?? '',
|
|
1528
|
+
`<span style="min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:${mutedColor};">${escapeHtml(param.seriesName ?? qualityPoint.seriesLabel)}</span>`,
|
|
1529
|
+
`<span style="font-family:JetBrains Mono, ui-monospace, monospace;font-weight:600;">${escapeHtml(formatQualityPercent(qualityPoint.value))}</span>`,
|
|
1530
|
+
`<span style="font-family:JetBrains Mono, ui-monospace, monospace;font-size:11px;color:${mutedColor};">${escapeHtml(sampleCount)}</span>`,
|
|
1531
|
+
'</div>',
|
|
1532
|
+
].join('');
|
|
1533
|
+
})
|
|
1534
|
+
.join('');
|
|
1535
|
+
return [
|
|
1536
|
+
'<div style="min-width:280px;">',
|
|
1537
|
+
`<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;font-family:JetBrains Mono, ui-monospace, monospace;font-size:11px;color:${mutedColor};">`,
|
|
1538
|
+
`<span>${escapeHtml(point.xLabel)}</span><span>·</span><span>${escapeHtml(formatDateTimeOrDash(point.updatedAt ?? point.createdAt))}</span>`,
|
|
1539
|
+
'</div>',
|
|
1540
|
+
'<div style="display:flex;align-items:center;gap:8px;min-width:0;">',
|
|
1541
|
+
`<span style="min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600;">${escapeHtml(point.eventLabel)}</span>`,
|
|
1542
|
+
`<span style="display:inline-flex;align-items:center;border-radius:999px;border:1px solid ${borderColor};padding:1px 6px;font-size:11px;color:${mutedColor};">${escapeHtml(kindText)}</span>`,
|
|
1543
|
+
'</div>',
|
|
1544
|
+
`<div style="margin-top:2px;font-size:11.5px;color:${mutedColor};">${escapeHtml(point.releaseVersionLabel)} · ${escapeHtml(point.promptVersionLabel)} · ${escapeHtml(point.modelName)}</div>`,
|
|
1545
|
+
`<div style="margin-top:8px;">${rows}</div>`,
|
|
1546
|
+
'</div>',
|
|
1547
|
+
].join('');
|
|
1548
|
+
},
|
|
1549
|
+
},
|
|
1550
|
+
xAxis: {
|
|
1551
|
+
type: 'category',
|
|
1552
|
+
boundaryGap: false,
|
|
1553
|
+
data: categoryLabels,
|
|
1554
|
+
axisLine: {
|
|
1555
|
+
show: false,
|
|
1556
|
+
},
|
|
1557
|
+
axisTick: {
|
|
1558
|
+
show: false,
|
|
1559
|
+
},
|
|
1560
|
+
axisLabel: {
|
|
1561
|
+
color: mutedColor,
|
|
1562
|
+
fontSize: 10,
|
|
1563
|
+
fontFamily: 'JetBrains Mono, ui-monospace, monospace',
|
|
1564
|
+
hideOverlap: true,
|
|
1565
|
+
margin: 10,
|
|
1566
|
+
},
|
|
1567
|
+
},
|
|
1568
|
+
yAxis: {
|
|
1569
|
+
type: 'value',
|
|
1570
|
+
min: getQualityPercentAxisMin,
|
|
1571
|
+
max: getQualityPercentAxisMax,
|
|
1572
|
+
scale: true,
|
|
1573
|
+
splitNumber: 4,
|
|
1574
|
+
axisLine: {
|
|
1575
|
+
show: false,
|
|
1576
|
+
},
|
|
1577
|
+
axisTick: {
|
|
1578
|
+
show: false,
|
|
1579
|
+
},
|
|
1580
|
+
axisLabel: {
|
|
1581
|
+
color: mutedColor,
|
|
1582
|
+
fontSize: 10,
|
|
1583
|
+
fontFamily: 'JetBrains Mono, ui-monospace, monospace',
|
|
1584
|
+
formatter: '{value}%',
|
|
1585
|
+
},
|
|
1586
|
+
splitLine: {
|
|
1587
|
+
lineStyle: {
|
|
1588
|
+
color: borderColor,
|
|
1589
|
+
type: 'dashed',
|
|
1590
|
+
opacity: 0.7,
|
|
1591
|
+
},
|
|
1592
|
+
},
|
|
1593
|
+
},
|
|
1594
|
+
dataZoom: axisData.length > 1
|
|
1595
|
+
? [
|
|
1596
|
+
{
|
|
1597
|
+
type: 'inside',
|
|
1598
|
+
xAxisIndex: 0,
|
|
1599
|
+
filterMode: 'filter',
|
|
1600
|
+
zoomOnMouseWheel: true,
|
|
1601
|
+
moveOnMouseWheel: 'shift',
|
|
1602
|
+
moveOnMouseMove: true,
|
|
1603
|
+
preventDefaultMouseMove: true,
|
|
1604
|
+
minValueSpan: 1,
|
|
1605
|
+
},
|
|
1606
|
+
{
|
|
1607
|
+
type: 'slider',
|
|
1608
|
+
xAxisIndex: 0,
|
|
1609
|
+
filterMode: 'filter',
|
|
1610
|
+
bottom: 10,
|
|
1611
|
+
height: 24,
|
|
1612
|
+
showDetail: false,
|
|
1613
|
+
brushSelect: true,
|
|
1614
|
+
minValueSpan: 1,
|
|
1615
|
+
borderColor,
|
|
1616
|
+
fillerColor: 'rgba(59, 130, 246, 0.18)',
|
|
1617
|
+
backgroundColor: 'transparent',
|
|
1618
|
+
dataBackground: {
|
|
1619
|
+
lineStyle: {
|
|
1620
|
+
color: borderColor,
|
|
1621
|
+
},
|
|
1622
|
+
areaStyle: {
|
|
1623
|
+
color: 'rgba(148, 163, 184, 0.14)',
|
|
1624
|
+
},
|
|
1625
|
+
},
|
|
1626
|
+
selectedDataBackground: {
|
|
1627
|
+
lineStyle: {
|
|
1628
|
+
color: primaryColor,
|
|
1629
|
+
},
|
|
1630
|
+
areaStyle: {
|
|
1631
|
+
color: 'rgba(59, 130, 246, 0.22)',
|
|
1632
|
+
},
|
|
1633
|
+
},
|
|
1634
|
+
moveHandleStyle: {
|
|
1635
|
+
color: primaryColor,
|
|
1636
|
+
},
|
|
1637
|
+
handleStyle: {
|
|
1638
|
+
color: cardColor,
|
|
1639
|
+
borderColor: primaryColor,
|
|
1640
|
+
},
|
|
1641
|
+
},
|
|
1642
|
+
]
|
|
1643
|
+
: [],
|
|
1644
|
+
toolbox: {
|
|
1645
|
+
show: axisData.length > 1,
|
|
1646
|
+
right: 8,
|
|
1647
|
+
top: 0,
|
|
1648
|
+
itemSize: 14,
|
|
1649
|
+
iconStyle: {
|
|
1650
|
+
borderColor: mutedColor,
|
|
1651
|
+
},
|
|
1652
|
+
emphasis: {
|
|
1653
|
+
iconStyle: {
|
|
1654
|
+
borderColor: foregroundColor,
|
|
1655
|
+
},
|
|
1656
|
+
},
|
|
1657
|
+
feature: {
|
|
1658
|
+
restore: {
|
|
1659
|
+
title: t('releases.detail.quality.chart.resetView'),
|
|
1660
|
+
},
|
|
1661
|
+
},
|
|
1662
|
+
},
|
|
1663
|
+
series: qualitySeries,
|
|
1664
|
+
};
|
|
1665
|
+
}, [axisData, chartColorVersion, formatDateTimeOrDash, series, t]);
|
|
1666
|
+
useEffect(() => {
|
|
1667
|
+
const element = chartRef.current;
|
|
1668
|
+
if (!element)
|
|
1669
|
+
return undefined;
|
|
1670
|
+
const instance = echarts.init(element, undefined, { renderer: 'svg' });
|
|
1671
|
+
chartInstanceRef.current = instance;
|
|
1672
|
+
const resizeObserver = typeof ResizeObserver === 'undefined'
|
|
1673
|
+
? null
|
|
1674
|
+
: new ResizeObserver(() => {
|
|
1675
|
+
instance.resize();
|
|
1676
|
+
});
|
|
1677
|
+
resizeObserver?.observe(element);
|
|
1678
|
+
return () => {
|
|
1679
|
+
resizeObserver?.disconnect();
|
|
1680
|
+
chartInstanceRef.current = null;
|
|
1681
|
+
instance.dispose();
|
|
1682
|
+
};
|
|
1683
|
+
}, []);
|
|
1684
|
+
useEffect(() => {
|
|
1685
|
+
chartInstanceRef.current?.setOption(chartOption, true);
|
|
1686
|
+
}, [chartOption]);
|
|
1687
|
+
return (_jsx("div", { className: "relative min-w-0 w-full", children: _jsxs("div", { className: "relative h-[360px] min-w-0 w-full", children: [_jsx("div", { ref: chartRef, className: "h-full w-full", role: "img", "aria-label": t('releases.detail.quality.chartTitle'), "data-testid": "release-quality-echarts-chart" }), children ? (_jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center px-4", children: _jsx("div", { className: "pointer-events-auto w-full max-w-[360px] rounded-lg border bg-card/95 px-4 py-4 text-center shadow-sm", children: children }) })) : null] }) }));
|
|
1688
|
+
}
|
|
1689
|
+
function QualityEmptyChartMessage({ annotationHref }) {
|
|
1690
|
+
const { t } = useI18n();
|
|
1691
|
+
return (_jsxs("div", { "data-testid": "release-quality-empty", children: [_jsx("div", { className: "text-[14px] font-semibold", children: t('releases.detail.quality.empty') }), _jsx("p", { className: "mx-auto mt-1 max-w-[300px] text-[12px] leading-5 text-muted-foreground", children: t('releases.detail.quality.emptyDescription') }), _jsx(Button, { asChild: true, size: "sm", className: "mt-3 h-8 gap-1.5", children: _jsxs(Link, { href: annotationHref, children: [_jsx(Plus, { className: "size-3.5", "aria-hidden": true }), t('releases.detail.quality.emptyAction')] }) })] }));
|
|
1692
|
+
}
|
|
1693
|
+
function QualityFilteredEmptyChartMessage() {
|
|
1694
|
+
const { t } = useI18n();
|
|
1695
|
+
return (_jsx("div", { className: "text-[13px] font-medium text-muted-foreground", children: t('releases.detail.quality.filteredEmpty') }));
|
|
904
1696
|
}
|
|
905
|
-
function
|
|
1697
|
+
function formatTemplate(template, values) {
|
|
1698
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => String(values[key] ?? ''));
|
|
1699
|
+
}
|
|
1700
|
+
const RELEASE_CONFIG_OPERATIONS = new Set([
|
|
1701
|
+
'traffic_updated',
|
|
1702
|
+
'mode_updated',
|
|
1703
|
+
'config_changed',
|
|
1704
|
+
]);
|
|
1705
|
+
export function buildHistoryGroups(line, releaseEvents, productionHistory, t) {
|
|
1706
|
+
if (releaseEvents.length > 0 || line.versions.length > 0) {
|
|
1707
|
+
return buildCanonicalHistoryGroups(line, releaseEvents, t);
|
|
1708
|
+
}
|
|
1709
|
+
return buildLegacyHistoryGroups(line, productionHistory, t);
|
|
1710
|
+
}
|
|
1711
|
+
function buildCanonicalHistoryGroups(line, releaseEvents, t) {
|
|
1712
|
+
const eventsByVersion = groupReleaseEventsByVersion(releaseEvents);
|
|
1713
|
+
const rowsByVersion = new Map();
|
|
1714
|
+
const looseRows = [];
|
|
1715
|
+
for (const version of line.versions) {
|
|
1716
|
+
const events = eventsByVersion.get(version.id) ?? [];
|
|
1717
|
+
rowsByVersion.set(version.id, buildHistoryRowFromVersion(line, version, events, t));
|
|
1718
|
+
}
|
|
1719
|
+
for (const [key, events] of eventsByVersion.entries()) {
|
|
1720
|
+
if (rowsByVersion.has(key))
|
|
1721
|
+
continue;
|
|
1722
|
+
looseRows.push(buildHistoryRowFromEvents(line, events, t));
|
|
1723
|
+
}
|
|
1724
|
+
const grouped = new Map();
|
|
1725
|
+
for (const row of [...rowsByVersion.values(), ...looseRows]) {
|
|
1726
|
+
const key = getHistoryGroupKey(row);
|
|
1727
|
+
const group = ensureHistoryGroup(grouped, key, row);
|
|
1728
|
+
if (isProductionHistoryRow(row)) {
|
|
1729
|
+
group.production = chooseNewestHistoryRow(group.production, row);
|
|
1730
|
+
group.productionNumber = row.productionNumber ?? row.targetProductionNumber ?? group.productionNumber;
|
|
1731
|
+
}
|
|
1732
|
+
else {
|
|
1733
|
+
group.candidates.push(row);
|
|
1734
|
+
}
|
|
1735
|
+
group.isLive = group.isLive || isHistoryRowLive(row);
|
|
1736
|
+
group.sortAt = chooseNewestDate(group.sortAt, row.updatedAt ?? row.createdAt);
|
|
1737
|
+
}
|
|
1738
|
+
return [...grouped.values()]
|
|
1739
|
+
.map((group) => ({
|
|
1740
|
+
...group,
|
|
1741
|
+
candidates: group.candidates.sort(compareHistoryRows),
|
|
1742
|
+
}))
|
|
1743
|
+
.sort(compareHistoryGroups);
|
|
1744
|
+
}
|
|
1745
|
+
function buildLegacyHistoryGroups(line, productionHistory, t) {
|
|
1746
|
+
const productionRows = productionHistory.map((item) => buildLegacyProductionHistoryRow(item, t));
|
|
1747
|
+
const canaryRows = (line.canaryHistory.length > 0 ? line.canaryHistory : line.canary ? [line.canary] : []).map((item) => buildLegacyCanaryHistoryRow(item, t));
|
|
1748
|
+
if (productionRows.length === 0 && canaryRows.length === 0)
|
|
1749
|
+
return [];
|
|
1750
|
+
const groups = productionRows.map((row) => ({
|
|
1751
|
+
id: row.id,
|
|
1752
|
+
production: row,
|
|
1753
|
+
candidates: [],
|
|
1754
|
+
isLive: isHistoryRowLive(row),
|
|
1755
|
+
sortAt: row.updatedAt ?? row.createdAt,
|
|
1756
|
+
productionNumber: row.productionNumber,
|
|
1757
|
+
}));
|
|
1758
|
+
const fallbackGroup = groups[0] ??
|
|
1759
|
+
{
|
|
1760
|
+
id: canaryRows[0]?.id ?? 'legacy-canary',
|
|
1761
|
+
production: null,
|
|
1762
|
+
candidates: [],
|
|
1763
|
+
isLive: false,
|
|
1764
|
+
sortAt: canaryRows[0]?.updatedAt ?? canaryRows[0]?.createdAt ?? null,
|
|
1765
|
+
productionNumber: null,
|
|
1766
|
+
};
|
|
1767
|
+
if (groups.length === 0)
|
|
1768
|
+
groups.push(fallbackGroup);
|
|
1769
|
+
for (const row of canaryRows) {
|
|
1770
|
+
fallbackGroup.candidates.push(row);
|
|
1771
|
+
fallbackGroup.isLive = fallbackGroup.isLive || isHistoryRowLive(row);
|
|
1772
|
+
fallbackGroup.sortAt = chooseNewestDate(fallbackGroup.sortAt, row.updatedAt ?? row.createdAt);
|
|
1773
|
+
}
|
|
1774
|
+
return groups
|
|
1775
|
+
.map((group) => ({ ...group, candidates: group.candidates.sort(compareHistoryRows) }))
|
|
1776
|
+
.sort(compareHistoryGroups);
|
|
1777
|
+
}
|
|
1778
|
+
function groupReleaseEventsByVersion(events) {
|
|
1779
|
+
const grouped = new Map();
|
|
1780
|
+
for (const event of events) {
|
|
1781
|
+
const key = event.releaseVersionId ?? event.id;
|
|
1782
|
+
const current = grouped.get(key) ?? [];
|
|
1783
|
+
current.push(event);
|
|
1784
|
+
grouped.set(key, current);
|
|
1785
|
+
}
|
|
1786
|
+
return grouped;
|
|
1787
|
+
}
|
|
1788
|
+
function buildHistoryRowFromVersion(line, version, events, t) {
|
|
1789
|
+
const latest = getLatestReleaseEvent(events);
|
|
1790
|
+
const label = latest?.releaseVersionLabel ?? version.label;
|
|
1791
|
+
return {
|
|
1792
|
+
id: latest?.id ?? version.id,
|
|
1793
|
+
sourceEventId: latest?.id ?? null,
|
|
1794
|
+
releaseVersionId: version.id,
|
|
1795
|
+
releaseVersionKind: version.kind,
|
|
1796
|
+
releaseVersionLabel: label,
|
|
1797
|
+
productionNumber: version.productionVersionNumber,
|
|
1798
|
+
targetProductionNumber: version.targetProductionVersionNumber,
|
|
1799
|
+
candidateNumber: version.candidateNumber,
|
|
1800
|
+
event: latest?.operation ?? (version.kind === 'production' ? 'create_production' : 'create_canary'),
|
|
1801
|
+
laneType: latest?.laneType ?? (version.kind === 'production' ? 'production' : 'canary'),
|
|
1802
|
+
promptName: latest?.promptName ?? version.promptName,
|
|
1803
|
+
promptVersionId: latest?.promptVersionId ?? version.promptVersionId,
|
|
1804
|
+
promptVersionLabel: latest?.promptVersionLabel ??
|
|
1805
|
+
version.promptVersionLabel ??
|
|
1806
|
+
(version.promptVersionNumber ? `v${version.promptVersionNumber}` : formatShortId(version.promptVersionId)),
|
|
1807
|
+
modelId: latest?.modelId ?? version.modelId,
|
|
1808
|
+
modelName: formatHistoryModel(latest?.modelName ?? version.modelName, latest?.modelId ?? version.modelId),
|
|
1809
|
+
modelProvider: latest?.modelProvider ?? version.modelProvider,
|
|
1810
|
+
inputConnectorName: latest?.inputConnectorName ?? line.inputConnectorName,
|
|
1811
|
+
inputConnectorType: latest?.inputConnectorType ?? line.inputConnectorType,
|
|
1812
|
+
outputConnectors: latest?.outputConnectors ?? line.outputConnectors,
|
|
1813
|
+
runConfig: normalizeRunConfig(latest?.runConfig),
|
|
1814
|
+
trafficRatio: latest?.trafficRatio ?? null,
|
|
1815
|
+
trafficMode: latest?.trafficMode ?? null,
|
|
1816
|
+
recordMode: latest?.recordMode ?? null,
|
|
1817
|
+
recordCategories: latest?.recordCategories ?? [],
|
|
1818
|
+
status: latest ? formatReleaseEventStatus(latest) : null,
|
|
1819
|
+
isLive: latest?.status === 'running',
|
|
1820
|
+
countSummary: latest ? formatReleaseEventCounts(latest, t) : null,
|
|
1821
|
+
relations: latest ? formatReleaseEventRelations(latest, t) : null,
|
|
1822
|
+
reason: latest?.submitReason.trim() || null,
|
|
1823
|
+
createdAt: latest?.createdAt ?? version.createdAt,
|
|
1824
|
+
updatedAt: latest?.updatedAt ?? version.updatedAt,
|
|
1825
|
+
configChanges: buildReleaseConfigChanges(events, t),
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
function buildHistoryRowFromEvents(line, events, t) {
|
|
1829
|
+
const latest = getLatestReleaseEvent(events);
|
|
1830
|
+
if (!latest) {
|
|
1831
|
+
return {
|
|
1832
|
+
id: 'empty',
|
|
1833
|
+
sourceEventId: null,
|
|
1834
|
+
releaseVersionId: null,
|
|
1835
|
+
releaseVersionKind: null,
|
|
1836
|
+
releaseVersionLabel: '—',
|
|
1837
|
+
productionNumber: null,
|
|
1838
|
+
targetProductionNumber: null,
|
|
1839
|
+
candidateNumber: null,
|
|
1840
|
+
event: null,
|
|
1841
|
+
laneType: null,
|
|
1842
|
+
promptName: line.promptName,
|
|
1843
|
+
promptVersionId: null,
|
|
1844
|
+
promptVersionLabel: '—',
|
|
1845
|
+
modelId: null,
|
|
1846
|
+
modelName: '—',
|
|
1847
|
+
modelProvider: null,
|
|
1848
|
+
inputConnectorName: line.inputConnectorName,
|
|
1849
|
+
inputConnectorType: line.inputConnectorType,
|
|
1850
|
+
outputConnectors: line.outputConnectors,
|
|
1851
|
+
runConfig: {},
|
|
1852
|
+
trafficRatio: null,
|
|
1853
|
+
trafficMode: null,
|
|
1854
|
+
recordMode: null,
|
|
1855
|
+
recordCategories: [],
|
|
1856
|
+
status: null,
|
|
1857
|
+
isLive: false,
|
|
1858
|
+
countSummary: null,
|
|
1859
|
+
relations: null,
|
|
1860
|
+
reason: null,
|
|
1861
|
+
createdAt: null,
|
|
1862
|
+
updatedAt: null,
|
|
1863
|
+
configChanges: [],
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
return {
|
|
1867
|
+
id: latest.id,
|
|
1868
|
+
sourceEventId: latest.id,
|
|
1869
|
+
releaseVersionId: latest.releaseVersionId,
|
|
1870
|
+
releaseVersionKind: latest.releaseVersionKind,
|
|
1871
|
+
releaseVersionLabel: latest.releaseVersionLabel ?? formatShortId(latest.releaseVersionId),
|
|
1872
|
+
productionNumber: latest.releaseVersionProductionNumber,
|
|
1873
|
+
targetProductionNumber: latest.releaseVersionTargetProductionNumber,
|
|
1874
|
+
candidateNumber: latest.releaseVersionCandidateNumber,
|
|
1875
|
+
event: latest.operation,
|
|
1876
|
+
laneType: latest.laneType,
|
|
1877
|
+
promptName: latest.promptName,
|
|
1878
|
+
promptVersionId: latest.promptVersionId,
|
|
1879
|
+
promptVersionLabel: latest.promptVersionLabel ?? formatShortId(latest.promptVersionId),
|
|
1880
|
+
modelId: latest.modelId,
|
|
1881
|
+
modelName: formatHistoryModel(latest.modelName, latest.modelId),
|
|
1882
|
+
modelProvider: latest.modelProvider,
|
|
1883
|
+
inputConnectorName: latest.inputConnectorName ?? line.inputConnectorName,
|
|
1884
|
+
inputConnectorType: latest.inputConnectorType ?? line.inputConnectorType,
|
|
1885
|
+
outputConnectors: latest.outputConnectors.length > 0 ? latest.outputConnectors : line.outputConnectors,
|
|
1886
|
+
runConfig: normalizeRunConfig(latest.runConfig),
|
|
1887
|
+
trafficRatio: latest.trafficRatio,
|
|
1888
|
+
trafficMode: latest.trafficMode,
|
|
1889
|
+
recordMode: latest.recordMode,
|
|
1890
|
+
recordCategories: latest.recordCategories,
|
|
1891
|
+
status: formatReleaseEventStatus(latest),
|
|
1892
|
+
isLive: latest.status === 'running',
|
|
1893
|
+
countSummary: formatReleaseEventCounts(latest, t),
|
|
1894
|
+
relations: formatReleaseEventRelations(latest, t),
|
|
1895
|
+
reason: latest.submitReason.trim() || null,
|
|
1896
|
+
createdAt: latest.createdAt,
|
|
1897
|
+
updatedAt: latest.updatedAt,
|
|
1898
|
+
configChanges: buildReleaseConfigChanges(events, t),
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
function buildLegacyProductionHistoryRow(item, t) {
|
|
1902
|
+
const configChanges = item.eventType === 'config_change'
|
|
1903
|
+
? [
|
|
1904
|
+
{
|
|
1905
|
+
id: item.id,
|
|
1906
|
+
at: item.updatedAt,
|
|
1907
|
+
event: item.eventType,
|
|
1908
|
+
items: buildLegacyProductionConfigItems(item, t),
|
|
1909
|
+
},
|
|
1910
|
+
]
|
|
1911
|
+
: [];
|
|
1912
|
+
const relations = [
|
|
1913
|
+
item.sourceExperimentId
|
|
1914
|
+
? `${t('releases.detail.history.relation.sourceExperiment')} ${formatShortId(item.sourceExperimentId)}`
|
|
1915
|
+
: null,
|
|
1916
|
+
item.sourceCanaryId
|
|
1917
|
+
? `${t('releases.detail.history.relation.sourceEvent')} ${formatShortId(item.sourceCanaryId)}`
|
|
1918
|
+
: null,
|
|
1919
|
+
item.rollbackTargetEventId
|
|
1920
|
+
? `${t('releases.detail.history.relation.rollbackTarget')} ${formatShortId(item.rollbackTargetEventId)}`
|
|
1921
|
+
: null,
|
|
1922
|
+
].filter((value) => Boolean(value));
|
|
1923
|
+
return {
|
|
1924
|
+
id: item.id,
|
|
1925
|
+
sourceEventId: item.id,
|
|
1926
|
+
releaseVersionId: null,
|
|
1927
|
+
releaseVersionKind: 'production',
|
|
1928
|
+
releaseVersionLabel: item.promptVersionLabel ?? formatShortId(item.id),
|
|
1929
|
+
productionNumber: null,
|
|
1930
|
+
targetProductionNumber: null,
|
|
1931
|
+
candidateNumber: null,
|
|
1932
|
+
event: item.eventType,
|
|
1933
|
+
laneType: 'production',
|
|
1934
|
+
promptName: item.promptVersionLabel ?? '—',
|
|
1935
|
+
promptVersionId: item.promptVersionId,
|
|
1936
|
+
promptVersionLabel: item.promptVersionLabel ?? formatShortId(item.promptVersionId),
|
|
1937
|
+
modelId: item.modelId,
|
|
1938
|
+
modelName: item.modelName ?? formatShortId(item.modelId),
|
|
1939
|
+
modelProvider: null,
|
|
1940
|
+
inputConnectorName: item.inputConnectorName,
|
|
1941
|
+
inputConnectorType: null,
|
|
1942
|
+
outputConnectors: [],
|
|
1943
|
+
runConfig: normalizeRunConfig(item.runConfig),
|
|
1944
|
+
trafficRatio: null,
|
|
1945
|
+
trafficMode: null,
|
|
1946
|
+
recordMode: item.recordMode,
|
|
1947
|
+
recordCategories: item.recordCategories ?? [],
|
|
1948
|
+
status: formatLegacyProductionStatus(item),
|
|
1949
|
+
isLive: item.status === 'running',
|
|
1950
|
+
countSummary: null,
|
|
1951
|
+
relations: relations.length > 0 ? relations.join(' · ') : null,
|
|
1952
|
+
reason: item.submitReason.trim() || null,
|
|
1953
|
+
createdAt: item.createdAt,
|
|
1954
|
+
updatedAt: item.updatedAt,
|
|
1955
|
+
configChanges,
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
function buildLegacyCanaryHistoryRow(canary, t) {
|
|
1959
|
+
return {
|
|
1960
|
+
id: canary.id,
|
|
1961
|
+
sourceEventId: canary.id,
|
|
1962
|
+
releaseVersionId: canary.releaseVersionId,
|
|
1963
|
+
releaseVersionKind: 'candidate',
|
|
1964
|
+
releaseVersionLabel: canary.releaseVersionLabel ?? canary.promptVersionLabel ?? formatShortId(canary.id),
|
|
1965
|
+
productionNumber: null,
|
|
1966
|
+
targetProductionNumber: null,
|
|
1967
|
+
candidateNumber: null,
|
|
1968
|
+
event: canary.status === 'running' ? 'ratio_change' : 'create_canary',
|
|
1969
|
+
laneType: 'canary',
|
|
1970
|
+
promptName: canary.promptName ?? canary.name ?? '—',
|
|
1971
|
+
promptVersionId: canary.promptVersionId,
|
|
1972
|
+
promptVersionLabel: canary.promptVersionLabel ?? formatShortId(canary.promptVersionId),
|
|
1973
|
+
modelId: canary.modelId,
|
|
1974
|
+
modelName: formatHistoryModel(canary.modelName, canary.modelId, canary.modelProvider),
|
|
1975
|
+
modelProvider: canary.modelProvider,
|
|
1976
|
+
inputConnectorName: canary.inputConnectorName,
|
|
1977
|
+
inputConnectorType: canary.inputConnectorType,
|
|
1978
|
+
outputConnectors: canary.outputConnectors,
|
|
1979
|
+
runConfig: normalizeRunConfig(canary.runConfig),
|
|
1980
|
+
trafficRatio: canary.trafficRatio,
|
|
1981
|
+
trafficMode: canary.trafficMode,
|
|
1982
|
+
recordMode: canary.recordMode,
|
|
1983
|
+
recordCategories: canary.recordCategories ?? [],
|
|
1984
|
+
status: canary.status,
|
|
1985
|
+
isLive: canary.status === 'running',
|
|
1986
|
+
countSummary: formatCanaryCounts(canary, t),
|
|
1987
|
+
relations: null,
|
|
1988
|
+
reason: canary.description?.trim() || null,
|
|
1989
|
+
createdAt: canary.createdAt,
|
|
1990
|
+
updatedAt: canary.updatedAt,
|
|
1991
|
+
configChanges: [],
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
function formatReleaseEventStatus(event) {
|
|
906
1995
|
const parts = [
|
|
907
1996
|
event.status,
|
|
908
|
-
event.
|
|
909
|
-
event.
|
|
910
|
-
|
|
1997
|
+
event.terminalReason ? `${event.terminalReason}` : null,
|
|
1998
|
+
event.controlState ? `${event.controlState}` : null,
|
|
1999
|
+
].filter((value) => Boolean(value));
|
|
2000
|
+
return parts.join(' · ') || '—';
|
|
2001
|
+
}
|
|
2002
|
+
function formatLegacyProductionStatus(item) {
|
|
2003
|
+
return [item.status, item.stopReason, item.controlState].filter(Boolean).join(' · ') || '—';
|
|
2004
|
+
}
|
|
2005
|
+
function formatHistoryModel(name, id, provider) {
|
|
2006
|
+
const model = name ?? formatShortId(id);
|
|
2007
|
+
return provider ? `${model} · ${provider}` : model;
|
|
2008
|
+
}
|
|
2009
|
+
function formatRecordMode(mode, t, categories = []) {
|
|
2010
|
+
if (mode === 'all')
|
|
2011
|
+
return t('releases.detail.topology.recordMode.all');
|
|
2012
|
+
if (mode === 'selected_categories' || mode === 'correct_only') {
|
|
2013
|
+
const label = t('releases.detail.topology.recordMode.selectedCategories');
|
|
2014
|
+
return categories.length > 0 ? `${label}: ${categories.join('、')}` : label;
|
|
2015
|
+
}
|
|
2016
|
+
return mode || '—';
|
|
2017
|
+
}
|
|
2018
|
+
function formatReleaseEventCounts(event, t) {
|
|
2019
|
+
return [
|
|
2020
|
+
`${t('releases.detail.metric.received')} ${formatCount(event.totalReceived)}`,
|
|
2021
|
+
`${t('releases.detail.metric.processed')} ${formatCount(event.totalProcessed)}`,
|
|
2022
|
+
`${t('releases.detail.metric.errors')} ${formatCount(event.totalErrors)}`,
|
|
2023
|
+
].join(' · ');
|
|
2024
|
+
}
|
|
2025
|
+
function formatCanaryCounts(canary, t) {
|
|
2026
|
+
return [
|
|
2027
|
+
`${t('releases.detail.metric.received')} ${formatCount(canary.totalReceived)}`,
|
|
2028
|
+
`${t('releases.detail.metric.processed')} ${formatCount(canary.totalProcessed)}`,
|
|
2029
|
+
`${t('releases.detail.metric.errors')} ${formatCount(canary.totalErrors)}`,
|
|
2030
|
+
].join(' · ');
|
|
2031
|
+
}
|
|
2032
|
+
function formatReleaseEventRelations(event, t) {
|
|
2033
|
+
const parts = [
|
|
2034
|
+
event.sourceEventId
|
|
2035
|
+
? `${t('releases.detail.history.relation.sourceEvent')} ${formatShortId(event.sourceEventId)}`
|
|
2036
|
+
: null,
|
|
2037
|
+
event.supersedesEventId
|
|
2038
|
+
? `${t('releases.detail.history.relation.supersedes')} ${formatShortId(event.supersedesEventId)}`
|
|
2039
|
+
: null,
|
|
2040
|
+
event.rollbackTargetEventId
|
|
2041
|
+
? `${t('releases.detail.history.relation.rollbackTarget')} ${formatShortId(event.rollbackTargetEventId)}`
|
|
2042
|
+
: null,
|
|
2043
|
+
event.sourceExperimentId
|
|
2044
|
+
? `${t('releases.detail.history.relation.sourceExperiment')} ${formatShortId(event.sourceExperimentId)}`
|
|
2045
|
+
: null,
|
|
911
2046
|
].filter((value) => Boolean(value));
|
|
912
|
-
return parts.join(' · ')
|
|
2047
|
+
return parts.length > 0 ? parts.join(' · ') : null;
|
|
2048
|
+
}
|
|
2049
|
+
function ensureHistoryGroup(map, id, seed) {
|
|
2050
|
+
const current = map.get(id);
|
|
2051
|
+
if (current)
|
|
2052
|
+
return current;
|
|
2053
|
+
const next = {
|
|
2054
|
+
id,
|
|
2055
|
+
production: null,
|
|
2056
|
+
candidates: [],
|
|
2057
|
+
isLive: false,
|
|
2058
|
+
sortAt: seed.updatedAt ?? seed.createdAt,
|
|
2059
|
+
productionNumber: seed.productionNumber ?? seed.targetProductionNumber,
|
|
2060
|
+
};
|
|
2061
|
+
map.set(id, next);
|
|
2062
|
+
return next;
|
|
2063
|
+
}
|
|
2064
|
+
function chooseNewestHistoryRow(current, next) {
|
|
2065
|
+
if (!current)
|
|
2066
|
+
return next;
|
|
2067
|
+
return timeValue(next.updatedAt ?? next.createdAt) >= timeValue(current.updatedAt ?? current.createdAt)
|
|
2068
|
+
? next
|
|
2069
|
+
: current;
|
|
2070
|
+
}
|
|
2071
|
+
function chooseNewestDate(current, next) {
|
|
2072
|
+
if (!next)
|
|
2073
|
+
return current;
|
|
2074
|
+
if (!current)
|
|
2075
|
+
return next;
|
|
2076
|
+
return timeValue(next) >= timeValue(current) ? next : current;
|
|
2077
|
+
}
|
|
2078
|
+
function compareHistoryRows(left, right) {
|
|
2079
|
+
const candidateDelta = (right.candidateNumber ?? 0) - (left.candidateNumber ?? 0);
|
|
2080
|
+
if (candidateDelta !== 0)
|
|
2081
|
+
return candidateDelta;
|
|
2082
|
+
return timeValue(right.updatedAt ?? right.createdAt) - timeValue(left.updatedAt ?? left.createdAt);
|
|
2083
|
+
}
|
|
2084
|
+
export function compareHistoryGroups(left, right) {
|
|
2085
|
+
// Total order, "newest production first":
|
|
2086
|
+
// 1. Groups WITH a productionNumber rank above those without (candidate-only / legacy),
|
|
2087
|
+
// so a numbered production group never sinks below a null group on timestamp alone.
|
|
2088
|
+
// 2. Within numbered groups: productionNumber descending.
|
|
2089
|
+
// 3. Within null groups (single class): sortAt descending.
|
|
2090
|
+
// 4. Ties: sortAt descending.
|
|
2091
|
+
const leftHasNumber = left.productionNumber !== null;
|
|
2092
|
+
const rightHasNumber = right.productionNumber !== null;
|
|
2093
|
+
if (leftHasNumber !== rightHasNumber)
|
|
2094
|
+
return leftHasNumber ? -1 : 1;
|
|
2095
|
+
if (leftHasNumber && rightHasNumber) {
|
|
2096
|
+
const numberDelta = right.productionNumber - left.productionNumber;
|
|
2097
|
+
if (numberDelta !== 0)
|
|
2098
|
+
return numberDelta;
|
|
2099
|
+
}
|
|
2100
|
+
return timeValue(right.sortAt) - timeValue(left.sortAt);
|
|
2101
|
+
}
|
|
2102
|
+
function getLatestReleaseEvent(events) {
|
|
2103
|
+
return [...events].sort((left, right) => timeValue(right.updatedAt) - timeValue(left.updatedAt))[0] ?? null;
|
|
2104
|
+
}
|
|
2105
|
+
function isProductionHistoryRow(row) {
|
|
2106
|
+
return row.releaseVersionKind === 'production' || row.laneType === 'production';
|
|
2107
|
+
}
|
|
2108
|
+
export function isHistoryRowLive(row) {
|
|
2109
|
+
return row.isLive;
|
|
2110
|
+
}
|
|
2111
|
+
function getHistoryGroupKey(row) {
|
|
2112
|
+
const productionNumber = row.releaseVersionKind === 'candidate'
|
|
2113
|
+
? row.targetProductionNumber
|
|
2114
|
+
: (row.productionNumber ?? row.targetProductionNumber);
|
|
2115
|
+
if (productionNumber !== null)
|
|
2116
|
+
return `production-${productionNumber}`;
|
|
2117
|
+
return row.releaseVersionId ?? row.id;
|
|
2118
|
+
}
|
|
2119
|
+
function normalizeRunConfig(value) {
|
|
2120
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
2121
|
+
return {};
|
|
2122
|
+
return value;
|
|
2123
|
+
}
|
|
2124
|
+
function getRunConfigNumber(config, key) {
|
|
2125
|
+
const value = config[key];
|
|
2126
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
2127
|
+
}
|
|
2128
|
+
function buildReleaseConfigChanges(events, t) {
|
|
2129
|
+
const sorted = [...events].sort((left, right) => timeValue(left.updatedAt) - timeValue(right.updatedAt));
|
|
2130
|
+
const changes = [];
|
|
2131
|
+
sorted.forEach((event, index) => {
|
|
2132
|
+
if (!RELEASE_CONFIG_OPERATIONS.has(event.operation))
|
|
2133
|
+
return;
|
|
2134
|
+
const previous = [...sorted.slice(0, index)].reverse().find((item) => item.releaseVersionId === event.releaseVersionId) ?? null;
|
|
2135
|
+
changes.push({
|
|
2136
|
+
id: event.id,
|
|
2137
|
+
at: event.updatedAt ?? event.createdAt,
|
|
2138
|
+
event: event.operation,
|
|
2139
|
+
items: buildConfigChangeItems(previous, event, t),
|
|
2140
|
+
});
|
|
2141
|
+
});
|
|
2142
|
+
return changes;
|
|
2143
|
+
}
|
|
2144
|
+
function buildLegacyProductionConfigItems(item, t) {
|
|
2145
|
+
const config = normalizeRunConfig(item.runConfig);
|
|
2146
|
+
const changes = [];
|
|
2147
|
+
addConfigChange(changes, t('releases.detail.topology.field.rpmLimit'), '—', formatHistoryNumber(getRunConfigNumber(config, 'rpmLimit')), true);
|
|
2148
|
+
addConfigChange(changes, t('releases.detail.topology.field.tpmLimit'), '—', formatHistoryNumber(getRunConfigNumber(config, 'tpmLimit')), true);
|
|
2149
|
+
addConfigChange(changes, t('releases.detail.topology.field.concurrency'), '—', formatHistoryNumber(getRunConfigNumber(config, 'concurrency')), true);
|
|
2150
|
+
addConfigChange(changes, t('releases.detail.topology.field.temperature'), '—', formatHistoryTemperature(getRunConfigNumber(config, 'temperature')), true);
|
|
2151
|
+
if (changes.length > 0)
|
|
2152
|
+
return changes;
|
|
2153
|
+
return [
|
|
2154
|
+
{
|
|
2155
|
+
field: t('releases.detail.history.field.snapshot'),
|
|
2156
|
+
previous: '—',
|
|
2157
|
+
next: t('releases.detail.history.change.updated'),
|
|
2158
|
+
},
|
|
2159
|
+
];
|
|
2160
|
+
}
|
|
2161
|
+
function buildConfigChangeItems(previous, current, t) {
|
|
2162
|
+
const previousConfig = normalizeRunConfig(previous?.runConfig);
|
|
2163
|
+
const currentConfig = normalizeRunConfig(current.runConfig);
|
|
2164
|
+
const changes = [];
|
|
2165
|
+
const includeInitial = previous === null;
|
|
2166
|
+
addConfigChange(changes, t('releases.detail.history.traffic'), formatRatioValue(previous?.trafficRatio ?? null), formatRatioValue(current.trafficRatio), includeInitial);
|
|
2167
|
+
addConfigChange(changes, t('releases.detail.history.field.trafficMode'), previous?.trafficMode ?? '—', current.trafficMode ?? '—', includeInitial);
|
|
2168
|
+
addConfigChange(changes, t('releases.detail.topology.field.rpmLimit'), formatHistoryNumber(getRunConfigNumber(previousConfig, 'rpmLimit')), formatHistoryNumber(getRunConfigNumber(currentConfig, 'rpmLimit')), includeInitial);
|
|
2169
|
+
addConfigChange(changes, t('releases.detail.topology.field.tpmLimit'), formatHistoryNumber(getRunConfigNumber(previousConfig, 'tpmLimit')), formatHistoryNumber(getRunConfigNumber(currentConfig, 'tpmLimit')), includeInitial);
|
|
2170
|
+
addConfigChange(changes, t('releases.detail.topology.field.concurrency'), formatHistoryNumber(getRunConfigNumber(previousConfig, 'concurrency')), formatHistoryNumber(getRunConfigNumber(currentConfig, 'concurrency')), includeInitial);
|
|
2171
|
+
addConfigChange(changes, t('releases.detail.topology.field.temperature'), formatHistoryTemperature(getRunConfigNumber(previousConfig, 'temperature')), formatHistoryTemperature(getRunConfigNumber(currentConfig, 'temperature')), includeInitial);
|
|
2172
|
+
addConfigChange(changes, t('releases.detail.history.model'), formatHistoryModel(previous?.modelName, previous?.modelId, previous?.modelProvider), formatHistoryModel(current.modelName, current.modelId, current.modelProvider), includeInitial);
|
|
2173
|
+
addConfigChange(changes, t('releases.detail.field.upstream'), formatConnectorLabel(previous?.inputConnectorName ?? null, previous?.inputConnectorType ?? null), formatConnectorLabel(current.inputConnectorName, current.inputConnectorType), includeInitial);
|
|
2174
|
+
addConfigChange(changes, t('releases.detail.history.field.outputConnectors'), formatOutputConnectors(previous?.outputConnectors ?? []), formatOutputConnectors(current.outputConnectors), includeInitial);
|
|
2175
|
+
addConfigChange(changes, t('releases.detail.history.recordMode'), formatRecordMode(previous?.recordMode, t, previous?.recordCategories ?? []), formatRecordMode(current.recordMode, t, current.recordCategories), includeInitial);
|
|
2176
|
+
if (changes.length > 0)
|
|
2177
|
+
return changes;
|
|
2178
|
+
return [
|
|
2179
|
+
{
|
|
2180
|
+
field: t('releases.detail.history.field.snapshot'),
|
|
2181
|
+
previous: '—',
|
|
2182
|
+
next: t('releases.detail.history.change.updated'),
|
|
2183
|
+
},
|
|
2184
|
+
];
|
|
2185
|
+
}
|
|
2186
|
+
function addConfigChange(changes, field, previous, next, includeInitial = false) {
|
|
2187
|
+
if (next === '—')
|
|
2188
|
+
return;
|
|
2189
|
+
if (!includeInitial && previous === next)
|
|
2190
|
+
return;
|
|
2191
|
+
changes.push({ field, previous, next });
|
|
2192
|
+
}
|
|
2193
|
+
function formatRatioValue(value) {
|
|
2194
|
+
return typeof value === 'number' && Number.isFinite(value) ? `${Math.round(value * 100)}%` : '—';
|
|
2195
|
+
}
|
|
2196
|
+
function formatHistoryNumber(value) {
|
|
2197
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
2198
|
+
return '—';
|
|
2199
|
+
return Number.isInteger(value) ? formatCount(value) : formatHistoryTemperature(value);
|
|
2200
|
+
}
|
|
2201
|
+
function formatHistoryTemperature(value) {
|
|
2202
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
2203
|
+
return '—';
|
|
2204
|
+
return value.toFixed(2).replace(/\.?0+$/, '');
|
|
2205
|
+
}
|
|
2206
|
+
function formatConnectorLabel(name, type) {
|
|
2207
|
+
if (!name && !type)
|
|
2208
|
+
return '—';
|
|
2209
|
+
return [type, name].filter((value) => Boolean(value)).join(' · ');
|
|
2210
|
+
}
|
|
2211
|
+
function formatOutputConnectors(connectors) {
|
|
2212
|
+
if (connectors.length === 0)
|
|
2213
|
+
return '—';
|
|
2214
|
+
return connectors.map((connector) => formatConnectorLabel(connector.name, connector.type)).join(' / ');
|
|
2215
|
+
}
|
|
2216
|
+
function formatHistoryVersionLabel(label) {
|
|
2217
|
+
return label.replace(/^v(?=\d)/, 'V');
|
|
2218
|
+
}
|
|
2219
|
+
function buildHistoryRuntimeItems(row, t) {
|
|
2220
|
+
const items = [];
|
|
2221
|
+
const rpm = getRunConfigNumber(row.runConfig, 'rpmLimit');
|
|
2222
|
+
const tpm = getRunConfigNumber(row.runConfig, 'tpmLimit');
|
|
2223
|
+
const concurrency = getRunConfigNumber(row.runConfig, 'concurrency');
|
|
2224
|
+
const temperature = getRunConfigNumber(row.runConfig, 'temperature');
|
|
2225
|
+
if (row.trafficRatio !== null || row.trafficMode) {
|
|
2226
|
+
items.push({ label: t('releases.detail.history.traffic'), value: formatReleaseRowTraffic(row), mono: true });
|
|
2227
|
+
}
|
|
2228
|
+
if (rpm !== null)
|
|
2229
|
+
items.push({ label: t('releases.detail.topology.field.rpmLimit'), value: formatHistoryNumber(rpm), mono: true });
|
|
2230
|
+
if (tpm !== null)
|
|
2231
|
+
items.push({ label: t('releases.detail.topology.field.tpmLimit'), value: formatHistoryNumber(tpm), mono: true });
|
|
2232
|
+
if (concurrency !== null) {
|
|
2233
|
+
items.push({
|
|
2234
|
+
label: t('releases.detail.topology.field.concurrency'),
|
|
2235
|
+
value: formatHistoryNumber(concurrency),
|
|
2236
|
+
mono: true,
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
if (temperature !== null) {
|
|
2240
|
+
items.push({
|
|
2241
|
+
label: t('releases.detail.topology.field.temperature'),
|
|
2242
|
+
value: formatHistoryTemperature(temperature),
|
|
2243
|
+
mono: true,
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
if (row.recordMode) {
|
|
2247
|
+
items.push({
|
|
2248
|
+
label: t('releases.detail.history.recordMode'),
|
|
2249
|
+
value: formatRecordMode(row.recordMode, t, row.recordCategories),
|
|
2250
|
+
mono: true,
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
return items;
|
|
913
2254
|
}
|
|
914
|
-
function
|
|
915
|
-
|
|
2255
|
+
function buildHistoryConnectorItems(row, t) {
|
|
2256
|
+
return [
|
|
2257
|
+
{
|
|
2258
|
+
label: t('releases.detail.field.upstream'),
|
|
2259
|
+
value: formatConnectorLabel(row.inputConnectorName, row.inputConnectorType),
|
|
2260
|
+
},
|
|
2261
|
+
{
|
|
2262
|
+
label: t('releases.detail.field.downstream'),
|
|
2263
|
+
value: row.outputConnectors.length > 0
|
|
2264
|
+
? formatOutputConnectors(row.outputConnectors)
|
|
2265
|
+
: t('releases.detail.history.field.noDownstream'),
|
|
2266
|
+
},
|
|
2267
|
+
];
|
|
2268
|
+
}
|
|
2269
|
+
function buildHistoryReasonItems(row, t) {
|
|
2270
|
+
if (!row.reason)
|
|
2271
|
+
return [];
|
|
2272
|
+
return [{ label: t('releases.detail.history.reason'), value: row.reason }];
|
|
2273
|
+
}
|
|
2274
|
+
function formatReleaseRowTraffic(row) {
|
|
2275
|
+
return ([formatRatioValue(row.trafficRatio), row.trafficMode].filter((value) => value && value !== '—').join(' · ') || '—');
|
|
2276
|
+
}
|
|
2277
|
+
function buildReleaseResultsHref(line, row) {
|
|
2278
|
+
if (!row.releaseVersionId)
|
|
2279
|
+
return null;
|
|
2280
|
+
return `/releases/${encodeURIComponent(line.id)}?tab=results&version=${encodeURIComponent(row.releaseVersionId)}`;
|
|
2281
|
+
}
|
|
2282
|
+
function buildAnnotationHref(line, row) {
|
|
2283
|
+
if (!row.releaseVersionId)
|
|
916
2284
|
return null;
|
|
917
|
-
|
|
918
|
-
const promptVersion = event.promptVersionLabel ?? formatShortId(event.promptVersionId);
|
|
919
|
-
const model = event.modelName ?? formatShortId(event.modelId);
|
|
920
|
-
return `${label} · ${promptVersion} · ${model}`;
|
|
2285
|
+
return `/annotations/new?line=${encodeURIComponent(line.id)}&version=${encodeURIComponent(row.releaseVersionId)}`;
|
|
921
2286
|
}
|
|
922
|
-
function
|
|
2287
|
+
function buildQualityAnnotationHref(line, releaseEvents) {
|
|
2288
|
+
const latestEvent = [...releaseEvents]
|
|
2289
|
+
.filter((event) => Boolean(event.releaseVersionId))
|
|
2290
|
+
.sort((left, right) => timeValue(right.updatedAt ?? right.createdAt) - timeValue(left.updatedAt ?? left.createdAt))[0];
|
|
2291
|
+
const params = new URLSearchParams({ line: line.id });
|
|
2292
|
+
if (latestEvent?.releaseVersionId)
|
|
2293
|
+
params.set('version', latestEvent.releaseVersionId);
|
|
2294
|
+
return `/annotations/new?${params.toString()}`;
|
|
2295
|
+
}
|
|
2296
|
+
function HistoryPane({ projectId, line, productionHistory, releaseEvents, loading, }) {
|
|
923
2297
|
const { t } = useI18n();
|
|
924
2298
|
const formatDateTimeOrDash = useDateTimeOrDash();
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
meta: item.submitReason || item.status,
|
|
942
|
-
variant: null,
|
|
943
|
-
}));
|
|
944
|
-
const canary = line.canary
|
|
945
|
-
? [
|
|
946
|
-
{
|
|
947
|
-
id: line.canary.id,
|
|
948
|
-
event: line.canary.status === 'running' ? 'ratio_change' : 'create_canary',
|
|
949
|
-
title: `${line.canary.promptVersionLabel ?? line.canary.id.slice(0, 8)} · ${Math.round(line.canary.trafficRatio * 100)}%`,
|
|
950
|
-
createdAt: line.canary.updatedAt,
|
|
951
|
-
meta: line.canary.description ?? line.canary.status,
|
|
952
|
-
variant: line.canary.releaseVariantLabel
|
|
953
|
-
? `${line.canary.releaseVariantLabel} · ${line.canary.promptVersionLabel ?? '-'} · ${line.canary.modelName ?? '-'}`
|
|
954
|
-
: null,
|
|
955
|
-
},
|
|
956
|
-
]
|
|
957
|
-
: [];
|
|
958
|
-
return [...canary, ...prod].sort((left, right) => (right.createdAt ? Date.parse(right.createdAt) : 0) - (left.createdAt ? Date.parse(left.createdAt) : 0));
|
|
959
|
-
}, [line.canary, productionHistory, releaseEvents, t]);
|
|
2299
|
+
const restoreToProductionMutation = useRestoreReleaseLineHistoryToProduction(projectId);
|
|
2300
|
+
const restoreToCanaryMutation = useRestoreReleaseLineHistoryToCanary(projectId);
|
|
2301
|
+
const restorePending = restoreToProductionMutation.isPending || restoreToCanaryMutation.isPending;
|
|
2302
|
+
const [restoreFeedback, setRestoreFeedback] = useState(null);
|
|
2303
|
+
const groups = useMemo(() => buildHistoryGroups(line, releaseEvents, productionHistory, t), [line, productionHistory, releaseEvents, t]);
|
|
2304
|
+
const [groupOpen, setGroupOpen] = useState({});
|
|
2305
|
+
const [moreOpen, setMoreOpen] = useState({});
|
|
2306
|
+
const [configOpen, setConfigOpen] = useState({});
|
|
2307
|
+
const historyGroupResetKey = `${line.id}:${groups.length}`;
|
|
2308
|
+
const [visibleGroupState, setVisibleGroupState] = useState(() => ({
|
|
2309
|
+
key: historyGroupResetKey,
|
|
2310
|
+
count: HISTORY_INITIAL_GROUP_LIMIT,
|
|
2311
|
+
}));
|
|
2312
|
+
const [loadingMoreKey, setLoadingMoreKey] = useState(null);
|
|
2313
|
+
const loadingMoreKeyRef = useRef(null);
|
|
2314
|
+
const loadMoreTimerRef = useRef(null);
|
|
960
2315
|
const showLoader = useDelayedLoading(loading);
|
|
2316
|
+
const visibleGroupCount = visibleGroupState.key === historyGroupResetKey ? visibleGroupState.count : HISTORY_INITIAL_GROUP_LIMIT;
|
|
2317
|
+
const visibleGroups = useMemo(() => groups.slice(0, visibleGroupCount), [groups, visibleGroupCount]);
|
|
2318
|
+
const hasMoreGroups = visibleGroupCount < groups.length;
|
|
2319
|
+
const isLoadingMoreGroups = loadingMoreKey === historyGroupResetKey && hasMoreGroups;
|
|
2320
|
+
const restoreHistoryToProduction = useCallback((row, versionLabel) => {
|
|
2321
|
+
if (!row.sourceEventId || line.status === 'archived')
|
|
2322
|
+
return;
|
|
2323
|
+
setRestoreFeedback(null);
|
|
2324
|
+
restoreToProductionMutation.mutate({
|
|
2325
|
+
releaseLineId: line.id,
|
|
2326
|
+
body: {
|
|
2327
|
+
sourceEventId: row.sourceEventId,
|
|
2328
|
+
reason: formatTemplate(t('releases.detail.history.action.restoreToProductionReason'), {
|
|
2329
|
+
version: versionLabel,
|
|
2330
|
+
}),
|
|
2331
|
+
},
|
|
2332
|
+
}, {
|
|
2333
|
+
onSuccess: () => setRestoreFeedback({
|
|
2334
|
+
tone: 'success',
|
|
2335
|
+
message: t('releases.detail.history.action.restoreSuccess'),
|
|
2336
|
+
}),
|
|
2337
|
+
onError: (error) => setRestoreFeedback({
|
|
2338
|
+
tone: 'error',
|
|
2339
|
+
message: getApiErrorMessage(error) ?? t('releases.detail.history.action.restoreFailed'),
|
|
2340
|
+
}),
|
|
2341
|
+
});
|
|
2342
|
+
}, [line.id, line.status, restoreToProductionMutation, t]);
|
|
2343
|
+
const restoreHistoryToCanary = useCallback((row, versionLabel) => {
|
|
2344
|
+
if (!row.sourceEventId || line.status === 'archived')
|
|
2345
|
+
return;
|
|
2346
|
+
setRestoreFeedback(null);
|
|
2347
|
+
restoreToCanaryMutation.mutate({
|
|
2348
|
+
releaseLineId: line.id,
|
|
2349
|
+
body: {
|
|
2350
|
+
sourceEventId: row.sourceEventId,
|
|
2351
|
+
reason: formatTemplate(t('releases.detail.history.action.restoreToCanaryReason'), {
|
|
2352
|
+
version: versionLabel,
|
|
2353
|
+
}),
|
|
2354
|
+
},
|
|
2355
|
+
}, {
|
|
2356
|
+
onSuccess: () => setRestoreFeedback({
|
|
2357
|
+
tone: 'success',
|
|
2358
|
+
message: t('releases.detail.history.action.restoreSuccess'),
|
|
2359
|
+
}),
|
|
2360
|
+
onError: (error) => setRestoreFeedback({
|
|
2361
|
+
tone: 'error',
|
|
2362
|
+
message: getApiErrorMessage(error) ?? t('releases.detail.history.action.restoreFailed'),
|
|
2363
|
+
}),
|
|
2364
|
+
});
|
|
2365
|
+
}, [line.id, line.status, restoreToCanaryMutation, t]);
|
|
2366
|
+
const loadMoreHistoryGroups = useCallback(() => {
|
|
2367
|
+
if (!hasMoreGroups)
|
|
2368
|
+
return;
|
|
2369
|
+
if (loadingMoreKeyRef.current === historyGroupResetKey)
|
|
2370
|
+
return;
|
|
2371
|
+
loadingMoreKeyRef.current = historyGroupResetKey;
|
|
2372
|
+
setLoadingMoreKey(historyGroupResetKey);
|
|
2373
|
+
setVisibleGroupState((current) => {
|
|
2374
|
+
const currentCount = current.key === historyGroupResetKey ? current.count : HISTORY_INITIAL_GROUP_LIMIT;
|
|
2375
|
+
return {
|
|
2376
|
+
key: historyGroupResetKey,
|
|
2377
|
+
count: Math.min(groups.length, currentCount + HISTORY_GROUP_PAGE_SIZE),
|
|
2378
|
+
};
|
|
2379
|
+
});
|
|
2380
|
+
if (loadMoreTimerRef.current)
|
|
2381
|
+
window.clearTimeout(loadMoreTimerRef.current);
|
|
2382
|
+
loadMoreTimerRef.current = window.setTimeout(() => {
|
|
2383
|
+
loadingMoreKeyRef.current = null;
|
|
2384
|
+
setLoadingMoreKey((current) => (current === historyGroupResetKey ? null : current));
|
|
2385
|
+
loadMoreTimerRef.current = null;
|
|
2386
|
+
}, 360);
|
|
2387
|
+
}, [groups.length, hasMoreGroups, historyGroupResetKey]);
|
|
2388
|
+
useEffect(() => {
|
|
2389
|
+
return () => {
|
|
2390
|
+
if (loadMoreTimerRef.current) {
|
|
2391
|
+
window.clearTimeout(loadMoreTimerRef.current);
|
|
2392
|
+
loadMoreTimerRef.current = null;
|
|
2393
|
+
}
|
|
2394
|
+
loadingMoreKeyRef.current = null;
|
|
2395
|
+
};
|
|
2396
|
+
}, []);
|
|
961
2397
|
if (loading) {
|
|
962
2398
|
return showLoader ? _jsx(PlatformLoader, { className: "py-8", size: "sm" }) : null;
|
|
963
2399
|
}
|
|
964
|
-
if (
|
|
2400
|
+
if (groups.length === 0) {
|
|
965
2401
|
return (_jsx("div", { className: "rounded-lg border bg-card p-10 text-center text-sm text-muted-foreground", children: t('releases.detail.history.empty') }));
|
|
966
2402
|
}
|
|
967
|
-
return (_jsxs("
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
2403
|
+
return (_jsxs("section", { className: "w-full", "aria-label": t('releases.detail.history.title'), children: [restoreFeedback ? (_jsxs("div", { role: restoreFeedback.tone === 'error' ? 'alert' : 'status', className: cn('mb-3 flex items-center gap-2 rounded-md border px-3 py-2 text-[12.5px]', restoreFeedback.tone === 'error'
|
|
2404
|
+
? 'border-destructive/35 bg-destructive/5 text-destructive'
|
|
2405
|
+
: 'border-border bg-muted/55 text-muted-foreground'), children: [restoreFeedback.tone === 'error' ? _jsx(AlertTriangle, { className: "size-3.5" }) : _jsx(Check, { className: "size-3.5" }), _jsx("span", { children: restoreFeedback.message })] })) : null, _jsx("div", { className: "relative pl-[30px]", children: visibleGroups.map((group, index) => {
|
|
2406
|
+
const headline = group.production ?? group.candidates[0] ?? null;
|
|
2407
|
+
if (!headline)
|
|
2408
|
+
return null;
|
|
2409
|
+
const children = group.production ? group.candidates : group.candidates.slice(1);
|
|
2410
|
+
const defaultOpen = group.isLive || index === 0;
|
|
2411
|
+
const isOpen = groupOpen[group.id] ?? defaultOpen;
|
|
2412
|
+
return (_jsxs("div", { className: "relative mb-4", children: [_jsx("div", { className: "absolute bottom-3.5 left-[-22px] top-[18px] w-[1.5px] bg-border", "aria-hidden": true }), _jsx(HistoryVersionCard, { line: line, row: headline, variant: isProductionHistoryRow(headline) ? 'production' : 'canary', live: group.isLive && isProductionHistoryRow(headline), hasChildren: children.length > 0, childrenOpen: isOpen, moreOpen: Boolean(moreOpen[headline.id]), configOpen: Boolean(configOpen[headline.id]), onToggleChildren: () => setGroupOpen((current) => ({
|
|
2413
|
+
...current,
|
|
2414
|
+
[group.id]: !(current[group.id] ?? defaultOpen),
|
|
2415
|
+
})), onToggleMore: () => setMoreOpen((current) => ({
|
|
2416
|
+
...current,
|
|
2417
|
+
[headline.id]: !current[headline.id],
|
|
2418
|
+
})), onToggleConfig: () => setConfigOpen((current) => ({
|
|
2419
|
+
...current,
|
|
2420
|
+
[headline.id]: !current[headline.id],
|
|
2421
|
+
})), onRestoreToProduction: restoreHistoryToProduction, onRestoreToCanary: restoreHistoryToCanary, restorePending: restorePending, formatDateTimeOrDash: formatDateTimeOrDash }), isOpen && children.length > 0 ? (_jsx("div", { className: "mb-0.5 mt-2.5 flex flex-col gap-2.5", children: children.map((row) => (_jsx(HistoryVersionCard, { line: line, row: row, variant: "canary", compact: true, live: isHistoryRowLive(row), hasChildren: false, childrenOpen: false, moreOpen: Boolean(moreOpen[row.id]), configOpen: Boolean(configOpen[row.id]), onToggleChildren: () => undefined, onToggleMore: () => setMoreOpen((current) => ({
|
|
2422
|
+
...current,
|
|
2423
|
+
[row.id]: !current[row.id],
|
|
2424
|
+
})), onToggleConfig: () => setConfigOpen((current) => ({
|
|
2425
|
+
...current,
|
|
2426
|
+
[row.id]: !current[row.id],
|
|
2427
|
+
})), onRestoreToProduction: restoreHistoryToProduction, onRestoreToCanary: restoreHistoryToCanary, restorePending: restorePending, formatDateTimeOrDash: formatDateTimeOrDash }, row.id))) })) : null] }, group.id));
|
|
2428
|
+
}) }), hasMoreGroups ? (_jsx(HistoryLoadMoreIndicator, { loading: isLoadingMoreGroups, label: isLoadingMoreGroups ? t('releases.detail.history.loadingMore') : t('releases.detail.history.moreAvailable'), onClick: loadMoreHistoryGroups })) : null] }));
|
|
2429
|
+
}
|
|
2430
|
+
function HistoryLoadMoreIndicator({ loading, label, onClick, }) {
|
|
2431
|
+
if (!loading) {
|
|
2432
|
+
return (_jsx("div", { className: "flex justify-center py-3", children: _jsxs("button", { type: "button", onClick: onClick, className: "inline-flex h-8 items-center gap-1.5 rounded-full border bg-card px-3.5 text-[12px] font-medium text-muted-foreground shadow-sm transition-colors hover:bg-muted/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", children: [_jsx(ChevronDown, { className: "size-3.5", "aria-hidden": true }), _jsx("span", { children: label })] }) }));
|
|
2433
|
+
}
|
|
2434
|
+
return (_jsxs("div", { className: "flex justify-center py-4", role: "status", "aria-live": "polite", "aria-label": label, children: [_jsx("span", { className: "sr-only", children: label }), _jsxs("span", { className: "relative flex h-8 w-28 items-center justify-center", "aria-hidden": true, children: [_jsx("span", { className: "absolute inset-x-0 top-1/2 h-px -translate-y-1/2 bg-gradient-to-r from-transparent via-border to-transparent" }), _jsx("span", { className: "relative inline-flex items-center gap-1.5 rounded-full border bg-card px-3 py-2 shadow-sm", children: [0, 1, 2].map((index) => (_jsx("span", { className: "size-1.5 rounded-full bg-muted-foreground/80 motion-safe:animate-bounce", style: { animationDelay: `${index * 120}ms` } }, index))) })] })] }));
|
|
2435
|
+
}
|
|
2436
|
+
function HistoryVersionCard({ line, row, variant, live, compact = false, hasChildren, childrenOpen, moreOpen, configOpen, onToggleChildren, onToggleMore, onToggleConfig, onRestoreToProduction, onRestoreToCanary, restorePending, formatDateTimeOrDash, }) {
|
|
2437
|
+
const { t } = useI18n();
|
|
2438
|
+
const isProduction = variant === 'production';
|
|
2439
|
+
const runtimeItems = buildHistoryRuntimeItems(row, t);
|
|
2440
|
+
const connectorItems = buildHistoryConnectorItems(row, t);
|
|
2441
|
+
const reasonItems = buildHistoryReasonItems(row, t);
|
|
2442
|
+
const hasConfig = row.configChanges.length > 0;
|
|
2443
|
+
const dateLabel = formatDateTimeOrDash(row.updatedAt ?? row.createdAt);
|
|
2444
|
+
const resultsHref = buildReleaseResultsHref(line, row);
|
|
2445
|
+
const annotationHref = buildAnnotationHref(line, row);
|
|
2446
|
+
const versionLabel = formatHistoryVersionLabel(row.releaseVersionLabel || '—');
|
|
2447
|
+
const expandLabel = formatTemplate(t(childrenOpen ? 'releases.detail.history.action.collapseVersion' : 'releases.detail.history.action.expandVersion'), { version: versionLabel });
|
|
2448
|
+
const moreLabel = formatTemplate(t('releases.detail.history.action.moreInfo'), { version: versionLabel });
|
|
2449
|
+
const configLabel = formatTemplate(t('releases.detail.history.action.configChanges'), { version: versionLabel });
|
|
2450
|
+
const resultLabel = formatTemplate(t('releases.detail.history.action.viewResults'), { version: versionLabel });
|
|
2451
|
+
const annotationLabel = formatTemplate(t('releases.detail.history.action.createAnnotation'), {
|
|
2452
|
+
version: versionLabel,
|
|
2453
|
+
});
|
|
2454
|
+
const actionMenuLabel = formatTemplate(t('releases.detail.history.action.moreActions'), { version: versionLabel });
|
|
2455
|
+
const restoreDisabled = !row.sourceEventId || line.status === 'archived' || restorePending;
|
|
2456
|
+
const restoreHistoryRow = () => {
|
|
2457
|
+
if (isProduction)
|
|
2458
|
+
onRestoreToProduction(row, versionLabel);
|
|
2459
|
+
else
|
|
2460
|
+
onRestoreToCanary(row, versionLabel);
|
|
2461
|
+
};
|
|
2462
|
+
const rowClickProps = hasChildren
|
|
2463
|
+
? {
|
|
2464
|
+
role: 'button',
|
|
2465
|
+
tabIndex: 0,
|
|
2466
|
+
'aria-expanded': childrenOpen,
|
|
2467
|
+
'aria-label': expandLabel,
|
|
2468
|
+
onClick: onToggleChildren,
|
|
2469
|
+
onKeyDown: (event) => {
|
|
2470
|
+
if (event.key !== 'Enter' && event.key !== ' ')
|
|
2471
|
+
return;
|
|
2472
|
+
event.preventDefault();
|
|
2473
|
+
onToggleChildren();
|
|
2474
|
+
},
|
|
2475
|
+
}
|
|
2476
|
+
: {};
|
|
2477
|
+
return (_jsxs("div", { ...rowClickProps, className: cn('relative flex flex-wrap items-center gap-x-3 gap-y-2 rounded-[10px] border bg-card px-[15px] shadow-sm transition-shadow hover:shadow-md', hasChildren && 'cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', compact ? 'py-[11px]' : 'py-[13px]'), style: live && isProduction
|
|
2478
|
+
? {
|
|
2479
|
+
borderColor: 'color-mix(in srgb, var(--src-prod) 35%, var(--border))',
|
|
2480
|
+
boxShadow: '0 1px 3px color-mix(in srgb, var(--src-prod) 12%, transparent)',
|
|
2481
|
+
}
|
|
2482
|
+
: undefined, children: [_jsx("span", { className: cn('absolute z-10 rounded-full bg-card', isProduction
|
|
2483
|
+
? 'left-[-29px] top-[14px] size-4 border-[2.5px]'
|
|
2484
|
+
: 'left-[-26px] top-[15px] size-2.5 border-[2.5px]'), style: {
|
|
2485
|
+
background: live ? (isProduction ? 'var(--src-prod)' : 'var(--src-canary)') : 'var(--card)',
|
|
2486
|
+
borderColor: isProduction ? (live ? 'var(--src-prod)' : 'var(--muted-foreground)') : 'var(--src-canary)',
|
|
2487
|
+
boxShadow: live
|
|
2488
|
+
? `0 0 0 4px color-mix(in srgb, ${isProduction ? 'var(--src-prod)' : 'var(--src-canary)'} 16%, transparent)`
|
|
2489
|
+
: undefined,
|
|
2490
|
+
}, "aria-hidden": true }), _jsx("span", { className: "absolute left-[-14px] top-[19px] h-[1.5px] w-[11px] bg-border", style: !isProduction ? { background: 'color-mix(in srgb, var(--src-canary) 35%, var(--border))' } : undefined, "aria-hidden": true }), _jsx(HistoryVersionBadge, { label: versionLabel, variant: variant, compact: compact }), _jsxs("div", { className: "flex min-w-0 flex-1 items-center", children: [_jsxs("div", { className: "flex w-[200px] min-w-0 shrink-0 items-baseline gap-[7px] whitespace-nowrap", children: [_jsx("span", { className: "shrink-0 text-[11.5px] text-muted-foreground", children: t('releases.detail.history.model') }), _jsx("span", { className: "min-w-0 truncate text-[13.5px] font-medium text-foreground", children: row.modelName })] }), _jsxs("div", { className: "flex w-[212px] min-w-0 shrink-0 items-baseline gap-[7px] whitespace-nowrap", children: [_jsx("span", { className: "shrink-0 text-[11.5px] text-muted-foreground", children: t('releases.detail.history.field.prompt') }), _jsx("span", { className: "min-w-0 truncate text-[13.5px] font-medium text-foreground", children: row.promptName }), _jsx("span", { className: "shrink-0 text-muted-foreground", children: "\u00B7" }), _jsx("span", { className: "shrink-0 font-mono text-[12.5px] font-semibold text-muted-foreground", children: row.promptVersionLabel })] }), _jsx("div", { className: "shrink-0 whitespace-nowrap font-mono text-[11.5px] text-muted-foreground", children: dateLabel })] }), _jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-2", onClick: (event) => event.stopPropagation(), children: [_jsx("button", { type: "button", onClick: onToggleMore, "aria-pressed": moreOpen, "aria-label": moreLabel, title: moreLabel, className: cn('inline-flex size-[30px] items-center justify-center rounded-lg border bg-card p-0 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', moreOpen && 'border-ring bg-muted text-foreground', compact && 'size-7 rounded-md'), children: _jsx(ChevronDown, { className: cn(compact ? 'size-3.5' : 'size-4', 'transition-transform', moreOpen && 'rotate-180') }) }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": actionMenuLabel, title: actionMenuLabel, className: cn('inline-flex size-[30px] items-center justify-center rounded-lg border bg-card p-0 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', compact && 'size-7 rounded-md'), children: _jsx(MoreHorizontal, { className: compact ? 'size-3.5' : 'size-4' }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-56", onClick: (event) => event.stopPropagation(), children: [_jsxs(DropdownMenuItem, { disabled: !hasConfig, onSelect: onToggleConfig, className: "gap-2", children: [_jsx(SlidersHorizontal, { className: "size-3.5" }), configLabel] }), _jsx(HistoryDropdownLink, { href: resultsHref, label: resultLabel, children: _jsx(ScrollText, { className: "size-3.5" }) }), _jsx(HistoryDropdownLink, { href: annotationHref, label: annotationLabel, children: _jsx(Tag, { className: "size-3.5" }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { disabled: restoreDisabled, onSelect: (event) => {
|
|
2491
|
+
event.preventDefault();
|
|
2492
|
+
event.stopPropagation();
|
|
2493
|
+
restoreHistoryRow();
|
|
2494
|
+
}, className: "gap-2", children: [_jsx(RotateCcw, { className: "size-3.5" }), restorePending
|
|
2495
|
+
? t('releases.detail.history.action.restoring')
|
|
2496
|
+
: t('releases.detail.history.action.restore')] })] })] })] }), moreOpen ? (_jsx(HistoryMorePanel, { runtimeItems: runtimeItems, connectorItems: connectorItems, reasonItems: reasonItems })) : null, configOpen && hasConfig ? (_jsx(HistoryConfigPanel, { changes: row.configChanges, formatDateTimeOrDash: formatDateTimeOrDash })) : null] }));
|
|
2497
|
+
}
|
|
2498
|
+
function HistoryDropdownLink({ href, label, children }) {
|
|
2499
|
+
if (!href) {
|
|
2500
|
+
return (_jsxs(DropdownMenuItem, { disabled: true, className: "gap-2", children: [children, label] }));
|
|
2501
|
+
}
|
|
2502
|
+
return (_jsx(DropdownMenuItem, { asChild: true, className: "gap-2", children: _jsxs(Link, { href: href, children: [children, label] }) }));
|
|
2503
|
+
}
|
|
2504
|
+
function HistoryMorePanel({ runtimeItems, connectorItems, reasonItems, }) {
|
|
2505
|
+
const { t } = useI18n();
|
|
2506
|
+
return (_jsxs("div", { className: "mt-3 basis-full rounded-lg border bg-muted/55 px-[13px] py-[11px]", children: [_jsx(HistoryPanelRow, { title: t('releases.detail.history.runtimeSection'), items: runtimeItems }), _jsx(HistoryPanelRow, { title: t('releases.detail.history.connectorSection'), items: connectorItems }), reasonItems.length > 0 ? (_jsx(HistoryPanelRow, { title: t('releases.detail.history.reasonSection'), items: reasonItems })) : null] }));
|
|
2507
|
+
}
|
|
2508
|
+
function HistoryPanelRow({ title, items }) {
|
|
2509
|
+
if (items.length === 0)
|
|
2510
|
+
return null;
|
|
2511
|
+
return (_jsxs("div", { className: "flex flex-col gap-2 py-[7px] first:pt-0 last:pb-0 sm:flex-row sm:items-baseline [&+&]:border-t", children: [_jsx("div", { className: "w-[58px] shrink-0 text-[11.5px] font-semibold text-muted-foreground", children: title }), _jsx("dl", { className: "flex min-w-0 flex-1 flex-wrap gap-x-[30px] gap-y-[9px]", children: items.map((item) => (_jsxs("div", { className: "min-w-0", children: [_jsx("dt", { className: "text-[11px] text-muted-foreground", children: item.label }), _jsx("dd", { className: cn('mt-0.5 max-w-[360px] break-words text-[13px] font-semibold text-foreground', item.mono && 'font-mono'), children: item.value })] }, item.label))) })] }));
|
|
2512
|
+
}
|
|
2513
|
+
function HistoryConfigPanel({ changes, formatDateTimeOrDash, }) {
|
|
2514
|
+
const { t } = useI18n();
|
|
2515
|
+
return (_jsxs("div", { className: "mt-3 basis-full rounded-lg border bg-muted/55 px-[13px] py-[11px]", children: [_jsx("div", { className: "mb-2 text-[11.5px] font-semibold text-muted-foreground", children: t('releases.detail.history.configChanges') }), _jsx("div", { className: "ml-[3px] flex flex-col gap-2.5 border-l-[1.5px] border-dotted border-muted-foreground/60 pl-[13px]", children: changes.map((change) => (_jsxs("div", { className: "relative flex flex-col gap-1.5 sm:flex-row sm:items-baseline sm:gap-4", children: [_jsx("span", { className: "absolute left-[-16.5px] top-1.5 size-1.5 rounded-full bg-muted-foreground", "aria-hidden": true }), _jsx("span", { className: "min-w-[118px] shrink-0 font-mono text-[11.5px] text-muted-foreground", children: formatDateTimeOrDash(change.at) }), _jsx("div", { className: "flex min-w-0 flex-wrap gap-x-0 gap-y-1", children: change.items.map((item) => (_jsxs("span", { className: "inline-flex items-baseline border-l px-3 text-[12.5px] first:border-l-0 first:pl-0", children: [_jsx("span", { className: "mr-2 font-semibold text-foreground", children: item.field }), _jsx("span", { className: "font-mono text-muted-foreground", children: item.previous }), _jsx("span", { className: "mx-1.5 text-muted-foreground", children: "\u2192" }), _jsx("span", { className: "font-mono font-semibold text-foreground", children: item.next })] }, `${change.id}-${item.field}-${item.next}`))) })] }, change.id))) })] }));
|
|
2516
|
+
}
|
|
2517
|
+
function HistoryVersionBadge({ label, variant, compact, }) {
|
|
2518
|
+
const isProduction = variant === 'production';
|
|
2519
|
+
return (_jsx("span", { className: cn('inline-flex h-[26px] w-[54px] shrink-0 items-center justify-center truncate rounded-md border px-2 font-mono text-[13px] font-semibold leading-none', compact && 'h-[25px] text-[12px]'), style: {
|
|
2520
|
+
background: isProduction ? 'var(--src-prod-soft)' : 'var(--src-canary-soft)',
|
|
2521
|
+
color: isProduction ? 'var(--src-prod-fg)' : 'var(--src-canary-fg)',
|
|
2522
|
+
borderColor: isProduction
|
|
2523
|
+
? 'color-mix(in srgb, var(--src-prod) 35%, var(--border))'
|
|
2524
|
+
: 'color-mix(in srgb, var(--src-canary) 35%, var(--border))',
|
|
2525
|
+
}, children: label }));
|
|
972
2526
|
}
|
|
973
2527
|
//# sourceMappingURL=release-line-detail-page.js.map
|