@cat-factory/app 0.54.0 → 0.54.2
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/app/components/bootstrap/BootstrapModal.vue +81 -69
- package/app/components/environments/EnvironmentStatusPanel.vue +37 -14
- package/app/components/focus/BlockFocusView.vue +92 -98
- package/app/components/fragments/FragmentLibraryManager.vue +104 -69
- package/app/components/fragments/FragmentLibraryPanel.vue +2 -1
- package/app/components/kaizen/KaizenPanel.vue +36 -25
- package/app/components/kaizen/KaizenStepStatus.vue +13 -10
- package/app/components/media/ArtifactLightbox.vue +20 -14
- package/app/components/media/ImageCompare.vue +35 -22
- package/app/components/provisioning/ProvisioningLogsDrawer.vue +27 -14
- package/app/components/recurring/RecurrenceEditor.vue +22 -15
- package/app/components/sandbox/SandboxPanel.vue +126 -59
- package/app/pages/index.vue +20 -8
- package/app/stores/workspace.ts +10 -5
- package/i18n/locales/en.json +409 -0
- package/i18n/locales/es.json +400 -0
- package/i18n/locales/fr.json +400 -0
- package/i18n/locales/pl.json +400 -0
- package/i18n/locales/uk.json +400 -0
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ import { agentKindMeta } from '~/utils/catalog'
|
|
|
9
9
|
// grading is scheduled by the engine and run by the background sweep, never from here.
|
|
10
10
|
const ui = useUiStore()
|
|
11
11
|
const kaizen = useKaizenStore()
|
|
12
|
+
const { t, d } = useI18n()
|
|
12
13
|
|
|
13
14
|
const open = computed(() => ui.kaizenScreenOpen)
|
|
14
15
|
|
|
@@ -31,7 +32,7 @@ function meta(kind: string) {
|
|
|
31
32
|
return agentKindMeta(kind)
|
|
32
33
|
}
|
|
33
34
|
function when(ms: number): string {
|
|
34
|
-
return new Date(ms)
|
|
35
|
+
return d(new Date(ms), 'long')
|
|
35
36
|
}
|
|
36
37
|
function gradeTone(g: KaizenGrading): string {
|
|
37
38
|
if (g.status === 'failed') return 'text-slate-500'
|
|
@@ -42,10 +43,10 @@ function gradeTone(g: KaizenGrading): string {
|
|
|
42
43
|
return 'text-rose-400'
|
|
43
44
|
}
|
|
44
45
|
function statusLabel(g: KaizenGrading): string {
|
|
45
|
-
if (g.status === 'scheduled') return '
|
|
46
|
-
if (g.status === 'running') return '
|
|
47
|
-
if (g.status === 'failed') return '
|
|
48
|
-
return g.grade != null ?
|
|
46
|
+
if (g.status === 'scheduled') return t('kaizen.status.scheduled')
|
|
47
|
+
if (g.status === 'running') return t('kaizen.status.grading')
|
|
48
|
+
if (g.status === 'failed') return t('kaizen.status.failed')
|
|
49
|
+
return g.grade != null ? t('kaizen.gradeValue', { grade: g.grade }) : t('kaizen.status.graded')
|
|
49
50
|
}
|
|
50
51
|
</script>
|
|
51
52
|
|
|
@@ -63,9 +64,9 @@ function statusLabel(g: KaizenGrading): string {
|
|
|
63
64
|
<UIcon name="i-lucide-sparkles" class="h-5 w-5 text-teal-400" />
|
|
64
65
|
</div>
|
|
65
66
|
<div class="min-w-0">
|
|
66
|
-
<h1 class="truncate text-base font-semibold text-white">
|
|
67
|
+
<h1 class="truncate text-base font-semibold text-white">{{ t('kaizen.title') }}</h1>
|
|
67
68
|
<p class="truncate text-xs text-slate-500">
|
|
68
|
-
|
|
69
|
+
{{ t('kaizen.subtitle') }}
|
|
69
70
|
</p>
|
|
70
71
|
</div>
|
|
71
72
|
<div class="ml-auto flex items-center gap-2">
|
|
@@ -77,10 +78,10 @@ function statusLabel(g: KaizenGrading): string {
|
|
|
77
78
|
:loading="kaizen.loadingOverview"
|
|
78
79
|
@click="kaizen.loadOverview()"
|
|
79
80
|
>
|
|
80
|
-
|
|
81
|
+
{{ t('kaizen.refresh') }}
|
|
81
82
|
</UButton>
|
|
82
83
|
<UButton icon="i-lucide-x" size="xs" color="neutral" variant="ghost" @click="close">
|
|
83
|
-
|
|
84
|
+
{{ t('common.close') }}
|
|
84
85
|
</UButton>
|
|
85
86
|
</div>
|
|
86
87
|
</header>
|
|
@@ -89,7 +90,7 @@ function statusLabel(g: KaizenGrading): string {
|
|
|
89
90
|
v-if="kaizen.available === false"
|
|
90
91
|
class="flex flex-1 items-center justify-center text-sm text-slate-500"
|
|
91
92
|
>
|
|
92
|
-
|
|
93
|
+
{{ t('kaizen.notConfigured') }}
|
|
93
94
|
</div>
|
|
94
95
|
|
|
95
96
|
<div v-else class="grid flex-1 grid-cols-1 gap-6 overflow-auto p-6 lg:grid-cols-3">
|
|
@@ -97,12 +98,13 @@ function statusLabel(g: KaizenGrading): string {
|
|
|
97
98
|
<section class="lg:col-span-1">
|
|
98
99
|
<h2 class="mb-2 flex items-center gap-2 text-sm font-semibold text-slate-200">
|
|
99
100
|
<UIcon name="i-lucide-badge-check" class="h-4 w-4 text-emerald-400" />
|
|
100
|
-
|
|
101
|
-
<span class="text-xs font-normal text-slate-500">
|
|
101
|
+
{{ t('kaizen.verifiedCombos.title') }}
|
|
102
|
+
<span class="text-xs font-normal text-slate-500">{{
|
|
103
|
+
t('kaizen.verifiedCombos.count', { count: kaizen.verifiedCount })
|
|
104
|
+
}}</span>
|
|
102
105
|
</h2>
|
|
103
106
|
<p class="mb-3 text-[11px] text-slate-500">
|
|
104
|
-
|
|
105
|
-
times in a row. These are no longer graded.
|
|
107
|
+
{{ t('kaizen.verifiedCombos.hint') }}
|
|
106
108
|
</p>
|
|
107
109
|
<ul class="space-y-2">
|
|
108
110
|
<li
|
|
@@ -125,15 +127,20 @@ function statusLabel(g: KaizenGrading): string {
|
|
|
125
127
|
class="ml-auto h-3.5 w-3.5 text-emerald-400"
|
|
126
128
|
/>
|
|
127
129
|
<span v-else class="ml-auto text-[11px] text-slate-500">
|
|
128
|
-
{{ c.consecutiveHighGrades }}
|
|
130
|
+
{{ t('kaizen.verifiedCombos.progress', { count: c.consecutiveHighGrades }) }}
|
|
129
131
|
</span>
|
|
130
132
|
</div>
|
|
131
133
|
<div class="mt-1 truncate text-[11px] text-slate-500" :title="c.model">
|
|
132
|
-
{{
|
|
134
|
+
{{
|
|
135
|
+
t('kaizen.verifiedCombos.modelPrompt', {
|
|
136
|
+
model: c.model,
|
|
137
|
+
version: c.promptVersion,
|
|
138
|
+
})
|
|
139
|
+
}}
|
|
133
140
|
</div>
|
|
134
141
|
</li>
|
|
135
142
|
<li v-if="kaizen.verified.length === 0" class="text-xs text-slate-600">
|
|
136
|
-
|
|
143
|
+
{{ t('kaizen.verifiedCombos.empty') }}
|
|
137
144
|
</li>
|
|
138
145
|
</ul>
|
|
139
146
|
</section>
|
|
@@ -142,17 +149,19 @@ function statusLabel(g: KaizenGrading): string {
|
|
|
142
149
|
<section class="lg:col-span-2">
|
|
143
150
|
<h2 class="mb-2 flex items-center gap-2 text-sm font-semibold text-slate-200">
|
|
144
151
|
<UIcon name="i-lucide-history" class="h-4 w-4 text-teal-400" />
|
|
145
|
-
|
|
152
|
+
{{ t('kaizen.history.title') }}
|
|
146
153
|
</h2>
|
|
147
154
|
<div class="overflow-hidden rounded-lg border border-slate-800">
|
|
148
155
|
<table class="w-full text-left text-xs">
|
|
149
156
|
<thead class="bg-slate-900/60 text-[11px] uppercase tracking-wide text-slate-500">
|
|
150
157
|
<tr>
|
|
151
|
-
<th class="px-3 py-2 font-medium">
|
|
152
|
-
<th class="px-3 py-2 font-medium">
|
|
153
|
-
<th class="px-3 py-2 font-medium">
|
|
154
|
-
<th class="px-3 py-2 font-medium">
|
|
155
|
-
<th class="px-3 py-2 font-medium">
|
|
158
|
+
<th class="px-3 py-2 font-medium">{{ t('kaizen.history.col.when') }}</th>
|
|
159
|
+
<th class="px-3 py-2 font-medium">{{ t('kaizen.history.col.agent') }}</th>
|
|
160
|
+
<th class="px-3 py-2 font-medium">{{ t('kaizen.history.col.model') }}</th>
|
|
161
|
+
<th class="px-3 py-2 font-medium">{{ t('kaizen.history.col.grade') }}</th>
|
|
162
|
+
<th class="px-3 py-2 font-medium">
|
|
163
|
+
{{ t('kaizen.history.col.recommendations') }}
|
|
164
|
+
</th>
|
|
156
165
|
</tr>
|
|
157
166
|
</thead>
|
|
158
167
|
<tbody class="divide-y divide-slate-800/70">
|
|
@@ -168,7 +177,9 @@ function statusLabel(g: KaizenGrading): string {
|
|
|
168
177
|
:style="{ color: meta(g.agentKind).color }"
|
|
169
178
|
/>
|
|
170
179
|
<span class="text-slate-200">{{ meta(g.agentKind).label }}</span>
|
|
171
|
-
<span class="text-slate-600">
|
|
180
|
+
<span class="text-slate-600">{{
|
|
181
|
+
t('kaizen.promptVersion', { version: g.promptVersion })
|
|
182
|
+
}}</span>
|
|
172
183
|
</span>
|
|
173
184
|
</td>
|
|
174
185
|
<td class="max-w-[12rem] truncate px-3 py-2 text-slate-400" :title="g.model">
|
|
@@ -187,7 +198,7 @@ function statusLabel(g: KaizenGrading): string {
|
|
|
187
198
|
</tr>
|
|
188
199
|
<tr v-if="kaizen.history.length === 0">
|
|
189
200
|
<td colspan="5" class="px-3 py-6 text-center text-slate-600">
|
|
190
|
-
|
|
201
|
+
{{ t('kaizen.history.empty') }}
|
|
191
202
|
</td>
|
|
192
203
|
</tr>
|
|
193
204
|
</tbody>
|
|
@@ -13,6 +13,7 @@ const props = defineProps<{
|
|
|
13
13
|
}>()
|
|
14
14
|
|
|
15
15
|
const kaizen = useKaizenStore()
|
|
16
|
+
const { t } = useI18n()
|
|
16
17
|
|
|
17
18
|
const grading = computed(() => {
|
|
18
19
|
if (!props.instanceId || props.stepIndex == null) return null
|
|
@@ -45,50 +46,52 @@ const tone = computed(() => {
|
|
|
45
46
|
<section v-if="grading" class="rounded-xl border border-slate-800 bg-slate-900/50 p-4">
|
|
46
47
|
<div class="flex items-center gap-2">
|
|
47
48
|
<UIcon name="i-lucide-sparkles" class="h-4 w-4 text-teal-400" />
|
|
48
|
-
<h3 class="text-sm font-semibold text-slate-200">
|
|
49
|
+
<h3 class="text-sm font-semibold text-slate-200">{{ t('kaizen.grading.title') }}</h3>
|
|
49
50
|
<span class="ml-auto flex items-center gap-1.5 text-xs">
|
|
50
51
|
<template v-if="grading.status === 'scheduled'">
|
|
51
52
|
<UIcon name="i-lucide-clock" class="h-3.5 w-3.5 text-slate-500" />
|
|
52
|
-
<span class="text-slate-400">
|
|
53
|
+
<span class="text-slate-400">{{ t('kaizen.status.scheduled') }}</span>
|
|
53
54
|
</template>
|
|
54
55
|
<template v-else-if="grading.status === 'running'">
|
|
55
56
|
<UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin text-teal-400" />
|
|
56
|
-
<span class="text-teal-300">
|
|
57
|
+
<span class="text-teal-300">{{ t('kaizen.status.grading') }}</span>
|
|
57
58
|
</template>
|
|
58
59
|
<template v-else-if="grading.status === 'failed'">
|
|
59
60
|
<UIcon name="i-lucide-circle-alert" class="h-3.5 w-3.5 text-rose-400" />
|
|
60
|
-
<span class="text-rose-400">
|
|
61
|
+
<span class="text-rose-400">{{ t('kaizen.status.failed') }}</span>
|
|
61
62
|
</template>
|
|
62
63
|
<template v-else>
|
|
63
|
-
<span class="font-semibold" :class="tone">{{
|
|
64
|
+
<span class="font-semibold" :class="tone">{{
|
|
65
|
+
t('kaizen.gradeValue', { grade: grading.grade })
|
|
66
|
+
}}</span>
|
|
64
67
|
</template>
|
|
65
68
|
</span>
|
|
66
69
|
</div>
|
|
67
70
|
|
|
68
71
|
<p v-if="grading.status === 'scheduled'" class="mt-2 text-[11px] text-slate-500">
|
|
69
|
-
|
|
72
|
+
{{ t('kaizen.scheduledHint') }}
|
|
70
73
|
</p>
|
|
71
74
|
|
|
72
75
|
<template v-else-if="grading.status === 'complete'">
|
|
73
76
|
<p v-if="grading.summary" class="mt-2 text-xs text-slate-300">{{ grading.summary }}</p>
|
|
74
77
|
<div v-if="grading.recommendations.length" class="mt-2">
|
|
75
78
|
<p class="text-[11px] font-medium uppercase tracking-wide text-slate-500">
|
|
76
|
-
|
|
79
|
+
{{ t('kaizen.recommendations') }}
|
|
77
80
|
</p>
|
|
78
81
|
<ul class="mt-1 list-disc space-y-0.5 pl-4 text-xs text-slate-300">
|
|
79
82
|
<li v-for="(r, i) in grading.recommendations" :key="i">{{ r }}</li>
|
|
80
83
|
</ul>
|
|
81
84
|
</div>
|
|
82
85
|
<p v-else class="mt-2 text-[11px] text-emerald-400/80">
|
|
83
|
-
|
|
86
|
+
{{ t('kaizen.noImprovements') }}
|
|
84
87
|
</p>
|
|
85
88
|
<p v-if="grading.graderModel" class="mt-2 text-[10px] text-slate-600">
|
|
86
|
-
|
|
89
|
+
{{ t('kaizen.gradedBy', { model: grading.graderModel }) }}
|
|
87
90
|
</p>
|
|
88
91
|
</template>
|
|
89
92
|
|
|
90
93
|
<p v-else-if="grading.status === 'failed'" class="mt-2 text-[11px] text-rose-400/80">
|
|
91
|
-
{{ grading.error ?? '
|
|
94
|
+
{{ grading.error ?? t('kaizen.failedFallback') }}
|
|
92
95
|
</p>
|
|
93
96
|
</section>
|
|
94
97
|
</template>
|
|
@@ -28,6 +28,8 @@ const emit = defineEmits<{
|
|
|
28
28
|
(e: 'update:index', value: number): void
|
|
29
29
|
}>()
|
|
30
30
|
|
|
31
|
+
const { t, n } = useI18n()
|
|
32
|
+
|
|
31
33
|
const MIN_SCALE = 1
|
|
32
34
|
const MAX_SCALE = 8
|
|
33
35
|
const scale = ref(1)
|
|
@@ -161,32 +163,36 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey, true))
|
|
|
161
163
|
class="fixed inset-0 z-[60] flex flex-col bg-slate-950/95 backdrop-blur-sm focus:outline-none"
|
|
162
164
|
role="dialog"
|
|
163
165
|
aria-modal="true"
|
|
164
|
-
:aria-label="
|
|
166
|
+
:aria-label="
|
|
167
|
+
current
|
|
168
|
+
? t('media.lightbox.ariaLabel', { label: current.label })
|
|
169
|
+
: t('media.lightbox.ariaLabelFallback')
|
|
170
|
+
"
|
|
165
171
|
@click.self="close"
|
|
166
172
|
>
|
|
167
173
|
<!-- Toolbar -->
|
|
168
174
|
<div class="flex items-center gap-3 border-b border-slate-800/60 px-4 py-2.5">
|
|
169
175
|
<span class="min-w-0 flex-1 truncate text-[13px] font-medium text-slate-200">
|
|
170
|
-
{{ current?.label ?? '
|
|
176
|
+
{{ current?.label ?? t('media.lightbox.fallbackTitle') }}
|
|
171
177
|
</span>
|
|
172
178
|
<span v-if="total > 1" class="shrink-0 text-[12px] tabular-nums text-slate-400">
|
|
173
|
-
{{ index + 1
|
|
179
|
+
{{ t('media.lightbox.counter', { current: index + 1, total }) }}
|
|
174
180
|
</span>
|
|
175
181
|
<div class="flex shrink-0 items-center gap-1">
|
|
176
182
|
<button
|
|
177
183
|
class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200 disabled:opacity-40"
|
|
178
|
-
title="
|
|
184
|
+
:title="t('media.lightbox.zoomOut')"
|
|
179
185
|
:disabled="scale <= MIN_SCALE"
|
|
180
186
|
@click="zoomBy(1 / 1.25)"
|
|
181
187
|
>
|
|
182
188
|
<UIcon name="i-lucide-zoom-out" class="h-4 w-4" />
|
|
183
189
|
</button>
|
|
184
|
-
<span class="w-10 text-center text-[11px] tabular-nums text-slate-500"
|
|
185
|
-
|
|
186
|
-
>
|
|
190
|
+
<span class="w-10 text-center text-[11px] tabular-nums text-slate-500">{{
|
|
191
|
+
n(scale, 'percent')
|
|
192
|
+
}}</span>
|
|
187
193
|
<button
|
|
188
194
|
class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200 disabled:opacity-40"
|
|
189
|
-
title="
|
|
195
|
+
:title="t('media.lightbox.zoomIn')"
|
|
190
196
|
:disabled="scale >= MAX_SCALE"
|
|
191
197
|
@click="zoomBy(1.25)"
|
|
192
198
|
>
|
|
@@ -194,14 +200,14 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey, true))
|
|
|
194
200
|
</button>
|
|
195
201
|
<button
|
|
196
202
|
class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
|
|
197
|
-
title="
|
|
203
|
+
:title="t('media.lightbox.reset')"
|
|
198
204
|
@click="resetView"
|
|
199
205
|
>
|
|
200
206
|
<UIcon name="i-lucide-maximize" class="h-4 w-4" />
|
|
201
207
|
</button>
|
|
202
208
|
<button
|
|
203
209
|
class="ml-1 rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
|
|
204
|
-
title="
|
|
210
|
+
:title="t('media.lightbox.close')"
|
|
205
211
|
@click="close"
|
|
206
212
|
>
|
|
207
213
|
<UIcon name="i-lucide-x" class="h-4 w-4" />
|
|
@@ -218,7 +224,7 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey, true))
|
|
|
218
224
|
<button
|
|
219
225
|
v-if="total > 1"
|
|
220
226
|
class="absolute left-3 top-1/2 z-10 -translate-y-1/2 rounded-full bg-slate-900/80 p-2 text-slate-300 hover:bg-slate-800 hover:text-white"
|
|
221
|
-
title="
|
|
227
|
+
:title="t('media.lightbox.prev')"
|
|
222
228
|
@click="go(-1)"
|
|
223
229
|
>
|
|
224
230
|
<UIcon name="i-lucide-chevron-left" class="h-5 w-5" />
|
|
@@ -248,21 +254,21 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey, true))
|
|
|
248
254
|
:class="state === 'error' ? '' : 'animate-spin'"
|
|
249
255
|
/>
|
|
250
256
|
<p class="text-[12px]">
|
|
251
|
-
{{ state === 'error' ? '
|
|
257
|
+
{{ state === 'error' ? t('media.lightbox.failed') : t('media.lightbox.loading') }}
|
|
252
258
|
</p>
|
|
253
259
|
<button
|
|
254
260
|
v-if="state === 'error' && current"
|
|
255
261
|
class="text-[12px] text-amber-300 hover:underline"
|
|
256
262
|
@click="props.blobs.retry(current.artifactId)"
|
|
257
263
|
>
|
|
258
|
-
|
|
264
|
+
{{ t('common.retry') }}
|
|
259
265
|
</button>
|
|
260
266
|
</div>
|
|
261
267
|
|
|
262
268
|
<button
|
|
263
269
|
v-if="total > 1"
|
|
264
270
|
class="absolute right-3 top-1/2 z-10 -translate-y-1/2 rounded-full bg-slate-900/80 p-2 text-slate-300 hover:bg-slate-800 hover:text-white"
|
|
265
|
-
title="
|
|
271
|
+
:title="t('media.lightbox.next')"
|
|
266
272
|
@click="go(1)"
|
|
267
273
|
>
|
|
268
274
|
<UIcon name="i-lucide-chevron-right" class="h-5 w-5" />
|
|
@@ -23,6 +23,8 @@ const emit = defineEmits<{
|
|
|
23
23
|
(e: 'uploadReference', file: File): void
|
|
24
24
|
}>()
|
|
25
25
|
|
|
26
|
+
const { t } = useI18n()
|
|
27
|
+
|
|
26
28
|
type Mode = 'side-by-side' | 'overlay' | 'swipe' | 'diff'
|
|
27
29
|
|
|
28
30
|
const actualUrl = computed(() => props.blobs.urlFor(props.actualId))
|
|
@@ -32,14 +34,15 @@ const hasBoth = computed(() => !!actualUrl.value && !!refUrl.value)
|
|
|
32
34
|
const diffFailed = ref(false)
|
|
33
35
|
const MODES = computed<{ id: Mode; icon: string; label: string }[]>(() => {
|
|
34
36
|
const base: { id: Mode; icon: string; label: string }[] = [
|
|
35
|
-
{ id: 'side-by-side', icon: 'i-lucide-columns-2', label: '
|
|
37
|
+
{ id: 'side-by-side', icon: 'i-lucide-columns-2', label: t('media.compare.mode.sideBySide') },
|
|
36
38
|
]
|
|
37
39
|
if (hasBoth.value) {
|
|
38
40
|
base.push(
|
|
39
|
-
{ id: 'overlay', icon: 'i-lucide-layers', label: '
|
|
40
|
-
{ id: 'swipe', icon: 'i-lucide-flip-horizontal-2', label: '
|
|
41
|
+
{ id: 'overlay', icon: 'i-lucide-layers', label: t('media.compare.mode.overlay') },
|
|
42
|
+
{ id: 'swipe', icon: 'i-lucide-flip-horizontal-2', label: t('media.compare.mode.swipe') },
|
|
41
43
|
)
|
|
42
|
-
if (!diffFailed.value)
|
|
44
|
+
if (!diffFailed.value)
|
|
45
|
+
base.push({ id: 'diff', icon: 'i-lucide-contrast', label: t('media.compare.mode.diff') })
|
|
43
46
|
}
|
|
44
47
|
return base
|
|
45
48
|
})
|
|
@@ -162,13 +165,19 @@ function onRefInput(e: Event) {
|
|
|
162
165
|
<!-- SIDE BY SIDE -->
|
|
163
166
|
<div v-if="mode === 'side-by-side'" class="grid grid-cols-2 gap-3">
|
|
164
167
|
<figure class="space-y-1">
|
|
165
|
-
<figcaption class="text-[10px] uppercase tracking-wide text-slate-500">
|
|
168
|
+
<figcaption class="text-[10px] uppercase tracking-wide text-slate-500">
|
|
169
|
+
{{ t('media.compare.actual') }}
|
|
170
|
+
</figcaption>
|
|
166
171
|
<button
|
|
167
172
|
v-if="actualUrl"
|
|
168
173
|
class="block w-full overflow-hidden rounded border border-slate-800 hover:border-slate-600"
|
|
169
174
|
@click="actualId && emit('expand', actualId)"
|
|
170
175
|
>
|
|
171
|
-
<img
|
|
176
|
+
<img
|
|
177
|
+
:src="actualUrl"
|
|
178
|
+
:alt="t('media.compare.actualAlt', { view })"
|
|
179
|
+
class="w-full cursor-zoom-in"
|
|
180
|
+
/>
|
|
172
181
|
</button>
|
|
173
182
|
<div
|
|
174
183
|
v-else
|
|
@@ -176,29 +185,33 @@ function onRefInput(e: Event) {
|
|
|
176
185
|
>
|
|
177
186
|
{{
|
|
178
187
|
props.blobs.statusFor(actualId) === 'error'
|
|
179
|
-
? '
|
|
188
|
+
? t('media.compare.failedToLoad')
|
|
180
189
|
: actualId
|
|
181
|
-
? '
|
|
182
|
-
: '
|
|
190
|
+
? t('media.compare.loading')
|
|
191
|
+
: t('media.compare.notCaptured')
|
|
183
192
|
}}
|
|
184
193
|
</div>
|
|
185
194
|
</figure>
|
|
186
195
|
|
|
187
196
|
<figure class="space-y-1">
|
|
188
197
|
<figcaption class="text-[10px] uppercase tracking-wide text-slate-500">
|
|
189
|
-
|
|
198
|
+
{{ t('media.compare.reference') }}
|
|
190
199
|
</figcaption>
|
|
191
200
|
<button
|
|
192
201
|
v-if="refUrl"
|
|
193
202
|
class="group relative block w-full overflow-hidden rounded border border-slate-800 hover:border-slate-600"
|
|
194
203
|
@click="referenceId && emit('expand', referenceId)"
|
|
195
204
|
>
|
|
196
|
-
<img
|
|
205
|
+
<img
|
|
206
|
+
:src="refUrl"
|
|
207
|
+
:alt="t('media.compare.referenceAlt', { view })"
|
|
208
|
+
class="w-full cursor-zoom-in"
|
|
209
|
+
/>
|
|
197
210
|
<span
|
|
198
211
|
class="absolute bottom-1 right-1 rounded bg-slate-950/80 px-1.5 py-0.5 text-[10px] text-slate-300 opacity-0 group-hover:opacity-100"
|
|
199
212
|
@click.stop="refInput?.click()"
|
|
200
213
|
>
|
|
201
|
-
|
|
214
|
+
{{ t('media.compare.replace') }}
|
|
202
215
|
</span>
|
|
203
216
|
</button>
|
|
204
217
|
<!-- Drop zone when no reference yet -->
|
|
@@ -216,7 +229,7 @@ function onRefInput(e: Event) {
|
|
|
216
229
|
@drop.prevent="onDrop"
|
|
217
230
|
>
|
|
218
231
|
<UIcon name="i-lucide-image-up" class="h-5 w-5" />
|
|
219
|
-
<span>
|
|
232
|
+
<span>{{ t('media.compare.dropHint') }}</span>
|
|
220
233
|
</div>
|
|
221
234
|
</figure>
|
|
222
235
|
</div>
|
|
@@ -224,17 +237,17 @@ function onRefInput(e: Event) {
|
|
|
224
237
|
<!-- OVERLAY (onion-skin) -->
|
|
225
238
|
<div v-else-if="mode === 'overlay'" class="space-y-2">
|
|
226
239
|
<div class="relative w-full overflow-hidden rounded border border-slate-800">
|
|
227
|
-
<img :src="refUrl" :alt="
|
|
240
|
+
<img :src="refUrl" :alt="t('media.compare.referenceAlt', { view })" class="w-full" />
|
|
228
241
|
<!-- object-contain so a differing aspect ratio onion-skins undistorted over the reference. -->
|
|
229
242
|
<img
|
|
230
243
|
:src="actualUrl"
|
|
231
|
-
:alt="
|
|
244
|
+
:alt="t('media.compare.actualAlt', { view })"
|
|
232
245
|
class="absolute inset-0 h-full w-full object-contain"
|
|
233
246
|
:style="{ opacity: overlayOpacity / 100 }"
|
|
234
247
|
/>
|
|
235
248
|
</div>
|
|
236
249
|
<div class="flex items-center gap-2 text-[10px] uppercase tracking-wide text-slate-500">
|
|
237
|
-
<span>
|
|
250
|
+
<span>{{ t('media.compare.reference') }}</span>
|
|
238
251
|
<input
|
|
239
252
|
v-model.number="overlayOpacity"
|
|
240
253
|
type="range"
|
|
@@ -242,7 +255,7 @@ function onRefInput(e: Event) {
|
|
|
242
255
|
max="100"
|
|
243
256
|
class="flex-1 accent-amber-500"
|
|
244
257
|
/>
|
|
245
|
-
<span>
|
|
258
|
+
<span>{{ t('media.compare.actual') }}</span>
|
|
246
259
|
</div>
|
|
247
260
|
</div>
|
|
248
261
|
|
|
@@ -256,7 +269,7 @@ function onRefInput(e: Event) {
|
|
|
256
269
|
@pointerup="onSwipeUp"
|
|
257
270
|
@pointercancel="onSwipeUp"
|
|
258
271
|
>
|
|
259
|
-
<img :src="refUrl" :alt="
|
|
272
|
+
<img :src="refUrl" :alt="t('media.compare.referenceAlt', { view })" class="block w-full" />
|
|
260
273
|
<div
|
|
261
274
|
class="absolute inset-0 overflow-hidden"
|
|
262
275
|
:style="{ clipPath: `inset(0 ${100 - splitPct}% 0 0)` }"
|
|
@@ -265,7 +278,7 @@ function onRefInput(e: Event) {
|
|
|
265
278
|
ratio doesn't stretch it; the split then compares like-for-like. -->
|
|
266
279
|
<img
|
|
267
280
|
:src="actualUrl"
|
|
268
|
-
:alt="
|
|
281
|
+
:alt="t('media.compare.actualAlt', { view })"
|
|
269
282
|
class="absolute inset-0 block h-full w-full object-contain"
|
|
270
283
|
/>
|
|
271
284
|
</div>
|
|
@@ -278,18 +291,18 @@ function onRefInput(e: Event) {
|
|
|
278
291
|
</div>
|
|
279
292
|
<span
|
|
280
293
|
class="absolute left-1 top-1 rounded bg-slate-950/70 px-1 text-[9px] uppercase text-slate-300"
|
|
281
|
-
>
|
|
294
|
+
>{{ t('media.compare.actual') }}</span
|
|
282
295
|
>
|
|
283
296
|
<span
|
|
284
297
|
class="absolute right-1 top-1 rounded bg-slate-950/70 px-1 text-[9px] uppercase text-slate-300"
|
|
285
|
-
>
|
|
298
|
+
>{{ t('media.compare.reference') }}</span
|
|
286
299
|
>
|
|
287
300
|
</div>
|
|
288
301
|
|
|
289
302
|
<!-- DIFF (canvas) -->
|
|
290
303
|
<div v-else-if="mode === 'diff'" class="space-y-1">
|
|
291
304
|
<canvas ref="diffCanvas" class="w-full rounded border border-slate-800 bg-black" />
|
|
292
|
-
<p class="text-[10px] text-slate-500">
|
|
305
|
+
<p class="text-[10px] text-slate-500">{{ t('media.compare.diffHint') }}</p>
|
|
293
306
|
</div>
|
|
294
307
|
|
|
295
308
|
<!-- Hidden file input shared by replace/drop zone -->
|
|
@@ -6,10 +6,16 @@
|
|
|
6
6
|
// panels' drawer, or `executionId` for a run's "Infrastructure attempts" drawer (which
|
|
7
7
|
// surfaces that run's container/runner/env attempts). Loaded on mount + re-loadable.
|
|
8
8
|
import { onMounted } from 'vue'
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
ProvisioningOperation,
|
|
11
|
+
ProvisioningOutcome,
|
|
12
|
+
ProvisioningSubsystem,
|
|
13
|
+
} from '~/types/provisioningLogs'
|
|
10
14
|
|
|
11
15
|
const props = defineProps<{ subsystem?: ProvisioningSubsystem; executionId?: string }>()
|
|
12
16
|
|
|
17
|
+
const { t, d } = useI18n()
|
|
18
|
+
|
|
13
19
|
const store = useProvisioningLogsStore()
|
|
14
20
|
const state = computed(() =>
|
|
15
21
|
props.executionId
|
|
@@ -24,17 +30,24 @@ function reload() {
|
|
|
24
30
|
|
|
25
31
|
onMounted(reload)
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// Exhaustive enum→label maps of literal `t(...)` keys (keeps the typed-key drift guard
|
|
34
|
+
// live for these runtime-indexed lookups).
|
|
35
|
+
const OPERATION_LABEL = computed<Record<ProvisioningOperation, string>>(() => ({
|
|
36
|
+
provision: t('provisioning.operation.provision'),
|
|
37
|
+
teardown: t('provisioning.operation.teardown'),
|
|
38
|
+
status: t('provisioning.operation.status'),
|
|
39
|
+
dispatch: t('provisioning.operation.dispatch'),
|
|
40
|
+
release: t('provisioning.operation.release'),
|
|
41
|
+
'poll-failure': t('provisioning.operation.poll-failure'),
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
const OUTCOME_LABEL = computed<Record<ProvisioningOutcome, string>>(() => ({
|
|
45
|
+
success: t('provisioning.outcome.success'),
|
|
46
|
+
failure: t('provisioning.outcome.failure'),
|
|
47
|
+
}))
|
|
35
48
|
|
|
36
49
|
function when(epochMs: number): string {
|
|
37
|
-
return new Date(epochMs)
|
|
50
|
+
return d(new Date(epochMs), 'long')
|
|
38
51
|
}
|
|
39
52
|
</script>
|
|
40
53
|
|
|
@@ -42,7 +55,7 @@ function when(epochMs: number): string {
|
|
|
42
55
|
<div class="rounded-lg border border-slate-700 bg-slate-900/50">
|
|
43
56
|
<div class="flex items-center justify-between border-b border-slate-800 px-3 py-2">
|
|
44
57
|
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
45
|
-
|
|
58
|
+
{{ t('provisioning.title') }}
|
|
46
59
|
</p>
|
|
47
60
|
<UButton
|
|
48
61
|
icon="i-lucide-rotate-ccw"
|
|
@@ -51,7 +64,7 @@ function when(epochMs: number): string {
|
|
|
51
64
|
:loading="state.loading"
|
|
52
65
|
@click="reload"
|
|
53
66
|
>
|
|
54
|
-
|
|
67
|
+
{{ t('provisioning.refresh') }}
|
|
55
68
|
</UButton>
|
|
56
69
|
</div>
|
|
57
70
|
|
|
@@ -60,7 +73,7 @@ function when(epochMs: number): string {
|
|
|
60
73
|
v-else-if="!state.loading && state.entries.length === 0"
|
|
61
74
|
class="px-3 py-3 text-[12px] text-slate-500"
|
|
62
75
|
>
|
|
63
|
-
|
|
76
|
+
{{ t('provisioning.empty') }}
|
|
64
77
|
</p>
|
|
65
78
|
|
|
66
79
|
<ul v-else class="max-h-80 divide-y divide-slate-800 overflow-auto">
|
|
@@ -79,7 +92,7 @@ function when(epochMs: number): string {
|
|
|
79
92
|
? 'bg-emerald-950/60 text-emerald-300'
|
|
80
93
|
: 'bg-rose-950/60 text-rose-300'
|
|
81
94
|
"
|
|
82
|
-
>{{ entry.outcome }}</span
|
|
95
|
+
>{{ OUTCOME_LABEL[entry.outcome] }}</span
|
|
83
96
|
>
|
|
84
97
|
<span class="ml-auto text-[11px] text-slate-500">{{ when(entry.createdAt) }}</span>
|
|
85
98
|
</div>
|