@cat-factory/app 0.47.9 → 0.47.10

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.
@@ -22,6 +22,7 @@ import type {
22
22
  const board = useBoardStore()
23
23
  const clarity = useClarityStore()
24
24
  const toast = useToast()
25
+ const { t } = useI18n()
25
26
 
26
27
  // Draft replies, keyed by item id, so editing one item doesn't disturb others.
27
28
  const drafts = ref<Record<string, string>>({})
@@ -112,6 +113,27 @@ const STATUS_COLOR = {
112
113
  recommend_requested: 'primary',
113
114
  } as const satisfies Record<ReviewItemStatus, string>
114
115
 
116
+ // Exhaustive enum→label maps (literal keys keep the typed-key drift guard live).
117
+ const SEVERITY_LABELS: Record<ReviewItemSeverity, string> = {
118
+ low: 'clarity.severity.low',
119
+ medium: 'clarity.severity.medium',
120
+ high: 'clarity.severity.high',
121
+ }
122
+ const CATEGORY_LABELS: Record<ReviewItemCategory, string> = {
123
+ gap: 'clarity.category.gap',
124
+ clarification: 'clarity.category.clarification',
125
+ assumption: 'clarity.category.assumption',
126
+ risk: 'clarity.category.risk',
127
+ question: 'clarity.category.question',
128
+ }
129
+ const STATUS_LABELS: Record<ReviewItemStatus, string> = {
130
+ open: 'clarity.itemStatus.open',
131
+ answered: 'clarity.itemStatus.answered',
132
+ resolved: 'clarity.itemStatus.resolved',
133
+ dismissed: 'clarity.itemStatus.dismissed',
134
+ recommend_requested: 'clarity.itemStatus.recommend_requested',
135
+ }
136
+
115
137
  function notifyError(title: string, e: unknown) {
116
138
  toast.add({
117
139
  title,
@@ -129,7 +151,7 @@ async function submitReply(item: ClarityReviewItem) {
129
151
  await clarity.reply(review.value, item.id, text)
130
152
  drafts.value = { ...drafts.value, [item.id]: '' }
131
153
  } catch (e) {
132
- notifyError('Could not save the answer', e)
154
+ notifyError(t('clarity.error.saveAnswer'), e)
133
155
  }
134
156
  }
135
157
 
@@ -138,7 +160,7 @@ async function setStatus(item: ClarityReviewItem, itemStatus: ClarityItemStatus)
138
160
  try {
139
161
  await clarity.setItemStatus(review.value, item.id, itemStatus)
140
162
  } catch (e) {
141
- notifyError('Could not update the finding', e)
163
+ notifyError(t('clarity.error.updateFinding'), e)
142
164
  }
143
165
  }
144
166
 
@@ -147,7 +169,7 @@ async function incorporate(feedback?: string) {
147
169
  try {
148
170
  await clarity.incorporate(review.value, feedback)
149
171
  } catch (e) {
150
- notifyError('Could not incorporate the answers', e)
172
+ notifyError(t('clarity.error.incorporate'), e)
151
173
  return
152
174
  }
153
175
  redoComment.value = ''
@@ -155,8 +177,8 @@ async function incorporate(feedback?: string) {
155
177
  // The fold + re-review now run in the durable driver. Hand the user back to the board;
156
178
  // a notification calls them back only if the re-review needs more input.
157
179
  toast.add({
158
- title: 'Clarifying the bug report in the background',
159
- description: "You're back on the board — we'll notify you only if more input is needed.",
180
+ title: t('clarity.toast.clarifyingTitle'),
181
+ description: t('clarity.toast.clarifyingDescription'),
160
182
  icon: 'i-lucide-wand-sparkles',
161
183
  })
162
184
  close()
@@ -166,17 +188,18 @@ async function reReview() {
166
188
  if (!blockId.value) return
167
189
  try {
168
190
  const updated = await clarity.reReview(blockId.value)
191
+ const newFindings = clarity.openCount(updated)
169
192
  toast.add({
170
193
  title:
171
194
  updated.status === 'incorporated'
172
- ? 'Reviewer is satisfied — continuing the pipeline'
195
+ ? t('clarity.toast.reReviewSatisfied')
173
196
  : updated.status === 'exceeded'
174
- ? 'Iteration limit reached — choose how to proceed'
175
- : `${clarity.openCount(updated)} new finding(s) to react to`,
197
+ ? t('clarity.toast.reReviewExceeded')
198
+ : t('clarity.toast.reReviewNewFindings', { count: newFindings }, newFindings),
176
199
  icon: 'i-lucide-sparkles',
177
200
  })
178
201
  } catch (e) {
179
- notifyError('Could not re-review the bug report', e)
202
+ notifyError(t('clarity.error.reReview'), e)
180
203
  }
181
204
  }
182
205
 
@@ -185,9 +208,9 @@ async function proceed() {
185
208
  acting.value = true
186
209
  try {
187
210
  await clarity.proceed(blockId.value)
188
- toast.add({ title: 'Proceeding to the next phase', icon: 'i-lucide-arrow-right' })
211
+ toast.add({ title: t('clarity.toast.proceeding'), icon: 'i-lucide-arrow-right' })
189
212
  } catch (e) {
190
- notifyError('Could not proceed', e)
213
+ notifyError(t('clarity.error.proceed'), e)
191
214
  } finally {
192
215
  acting.value = false
193
216
  }
@@ -199,15 +222,15 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
199
222
  try {
200
223
  await clarity.resolveExceeded(blockId.value, choice)
201
224
  if (choice === 'stop-reset') {
202
- toast.add({ title: 'Task reset — edit the bug report and resubmit', icon: 'i-lucide-undo' })
225
+ toast.add({ title: t('clarity.toast.taskReset'), icon: 'i-lucide-undo' })
203
226
  close()
204
227
  } else if (choice === 'proceed') {
205
- toast.add({ title: 'Proceeding to the next phase', icon: 'i-lucide-arrow-right' })
228
+ toast.add({ title: t('clarity.toast.proceeding'), icon: 'i-lucide-arrow-right' })
206
229
  } else {
207
- toast.add({ title: 'One more review round granted', icon: 'i-lucide-rotate-cw' })
230
+ toast.add({ title: t('clarity.toast.extraRoundGranted'), icon: 'i-lucide-rotate-cw' })
208
231
  }
209
232
  } catch (e) {
210
- notifyError('Could not resolve the review', e)
233
+ notifyError(t('clarity.error.resolve'), e)
211
234
  } finally {
212
235
  acting.value = false
213
236
  }
@@ -232,12 +255,12 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
232
255
  <UIcon name="i-lucide-bug" class="h-5 w-5 text-indigo-300" />
233
256
  </div>
234
257
  <div class="min-w-0">
235
- <h1 class="truncate text-base font-semibold text-white">Bug-report triage</h1>
258
+ <h1 class="truncate text-base font-semibold text-white">{{ t('clarity.title') }}</h1>
236
259
  <p v-if="block" class="truncate text-xs text-slate-500">{{ block.title }}</p>
237
260
  </div>
238
261
  <div class="ml-auto flex items-center gap-1.5">
239
262
  <UBadge v-if="review" color="neutral" variant="subtle" size="sm">
240
- Iteration {{ iteration }} / {{ maxIterations }}
263
+ {{ t('clarity.iteration', { current: iteration, max: maxIterations }) }}
241
264
  </UBadge>
242
265
  <UButton icon="i-lucide-x" color="neutral" variant="ghost" size="sm" @click="close" />
243
266
  </div>
@@ -247,11 +270,15 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
247
270
  <!-- main column -->
248
271
  <div class="min-w-0 flex-1 overflow-y-auto px-6 py-5">
249
272
  <p class="mb-4 text-sm text-slate-400">
250
- An AI reviewer triaged this {{ block?.level ?? 'item' }}’s bug report for fixability —
251
- its description plus any linked context — and raised the questions below.
252
- <span class="text-slate-300">Answer</span> the relevant ones and
253
- <span class="text-slate-300">dismiss</span> the irrelevant, then incorporate them; the
254
- reviewer re-reviews until the bug report is clear enough to fix.
273
+ <i18n-t keypath="clarity.intro" tag="span" scope="global">
274
+ <template #level>{{ block?.level ?? t('clarity.itemFallback') }}</template>
275
+ <template #answer
276
+ ><span class="text-slate-300">{{ t('clarity.introAnswer') }}</span></template
277
+ >
278
+ <template #dismiss
279
+ ><span class="text-slate-300">{{ t('clarity.introDismiss') }}</span></template
280
+ >
281
+ </i18n-t>
255
282
  </p>
256
283
 
257
284
  <!-- empty state — the reviewer runs automatically as the first pipeline
@@ -260,8 +287,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
260
287
  v-if="!review && !busy && !loading"
261
288
  class="rounded-lg border border-dashed border-slate-700 p-8 text-center text-sm text-slate-500"
262
289
  >
263
- No review yet. The reviewer runs automatically as the first step when this task's
264
- pipeline starts.
290
+ {{ t('clarity.empty') }}
265
291
  </div>
266
292
 
267
293
  <!-- working state (initial fetch on open, or a reviewer pass running) -->
@@ -270,7 +296,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
270
296
  class="flex items-center justify-center gap-2 p-8 text-sm text-slate-400"
271
297
  >
272
298
  <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
273
- {{ loading && !busy ? 'Loading the review…' : 'Triaging the bug report…' }}
299
+ {{ loading && !busy ? t('clarity.loadingReview') : t('clarity.triaging') }}
274
300
  </div>
275
301
 
276
302
  <template v-else-if="review">
@@ -280,16 +306,15 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
280
306
  class="mb-4 flex items-center gap-2 rounded-lg border border-emerald-900/60 bg-emerald-950/30 p-4 text-sm text-emerald-300"
281
307
  >
282
308
  <UIcon name="i-lucide-circle-check" class="h-5 w-5 shrink-0" />
283
- The bug report is clarified. The report below is what every downstream agent step
284
- uses.
309
+ {{ t('clarity.converged') }}
285
310
  </div>
286
311
 
287
312
  <!-- iteration cap hit -->
288
313
  <IterationCapPrompt
289
314
  v-else-if="exceeded"
290
315
  class="mb-4"
291
- :heading="`Reached the ${maxIterations}-iteration limit with findings still open.`"
292
- detail="Do one more review round, proceed to the next phase with the last clarified bug report anyway, or stop and reset the task so you can rework the bug report and resubmit."
316
+ :heading="t('clarity.capHeading', { max: maxIterations })"
317
+ :detail="t('clarity.capDetail')"
293
318
  :loading="acting"
294
319
  @resolve="resolveExceeded"
295
320
  />
@@ -302,12 +327,10 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
302
327
  >
303
328
  <UIcon name="i-lucide-loader-circle" class="h-5 w-5 shrink-0 animate-spin" />
304
329
  <span v-if="incorporating">
305
- Incorporating your answers into a clarified bug report… You can close this — we’ll
306
- notify you only if more input is needed.
330
+ {{ t('clarity.incorporatingStage') }}
307
331
  </span>
308
332
  <span v-else>
309
- Re-reviewing the updated bug report… You can close this — we’ll notify you only if
310
- more input is needed.
333
+ {{ t('clarity.reReviewingStage') }}
311
334
  </span>
312
335
  </div>
313
336
 
@@ -328,10 +351,10 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
328
351
  <div class="flex flex-wrap items-center gap-1.5">
329
352
  <span class="text-sm font-medium text-white">{{ item.title }}</span>
330
353
  <UBadge size="xs" variant="subtle" :color="SEVERITY_COLOR[item.severity]">
331
- {{ item.severity }}
354
+ {{ t(SEVERITY_LABELS[item.severity]) }}
332
355
  </UBadge>
333
356
  <UBadge size="xs" variant="outline" color="neutral">
334
- {{ item.category }}
357
+ {{ t(CATEGORY_LABELS[item.category]) }}
335
358
  </UBadge>
336
359
  <UBadge
337
360
  size="xs"
@@ -339,7 +362,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
339
362
  :color="STATUS_COLOR[item.status]"
340
363
  class="ml-auto"
341
364
  >
342
- {{ item.status }}
365
+ {{ t(STATUS_LABELS[item.status]) }}
343
366
  </UBadge>
344
367
  </div>
345
368
  <p class="mt-1 whitespace-pre-line text-sm text-slate-400">
@@ -352,7 +375,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
352
375
  class="mt-2 rounded-md border-l-2 border-slate-700 bg-slate-950/40 px-3 py-1.5 text-sm text-slate-300"
353
376
  >
354
377
  <span class="text-[10px] uppercase tracking-wide text-slate-500">
355
- Answer
378
+ {{ t('clarity.answerLabel') }}
356
379
  </span>
357
380
  <p class="whitespace-pre-line">{{ item.reply }}</p>
358
381
  </div>
@@ -366,7 +389,11 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
366
389
  autoresize
367
390
  size="sm"
368
391
  class="mt-2 w-full"
369
- :placeholder="item.reply ? 'Refine your answer…' : 'Answer this finding…'"
392
+ :placeholder="
393
+ item.reply
394
+ ? t('clarity.refineAnswerPlaceholder')
395
+ : t('clarity.answerPlaceholder')
396
+ "
370
397
  :disabled="frozen"
371
398
  />
372
399
  <div class="mt-2 flex flex-wrap items-center gap-2">
@@ -378,7 +405,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
378
405
  :disabled="!(drafts[item.id] ?? '').trim() || frozen"
379
406
  @click="submitReply(item)"
380
407
  >
381
- Save answer
408
+ {{ t('clarity.saveAnswer') }}
382
409
  </UButton>
383
410
  <UButton
384
411
  color="neutral"
@@ -388,7 +415,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
388
415
  :disabled="frozen"
389
416
  @click="setStatus(item, 'dismissed')"
390
417
  >
391
- Dismiss as irrelevant
418
+ {{ t('clarity.dismissIrrelevant') }}
392
419
  </UButton>
393
420
  </div>
394
421
  </template>
@@ -403,7 +430,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
403
430
  :disabled="frozen"
404
431
  @click="setStatus(item, 'open')"
405
432
  >
406
- Reopen
433
+ {{ t('clarity.reopen') }}
407
434
  </UButton>
408
435
  </div>
409
436
  </div>
@@ -416,7 +443,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
416
443
  <div class="mb-3 flex items-center gap-1.5 text-[11px] text-emerald-400">
417
444
  <UIcon name="i-lucide-file-check-2" class="h-3.5 w-3.5" />
418
445
  <span class="font-semibold uppercase tracking-wide">
419
- {{ incorporated ? 'Clarified bug report' : 'Clarified bug report (draft)' }}
446
+ {{ incorporated ? t('clarity.docHeading') : t('clarity.docHeadingDraft') }}
420
447
  </span>
421
448
  </div>
422
449
  <div v-for="s in outline.sections" :key="s.id" class="mb-2">
@@ -451,19 +478,19 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
451
478
  <div class="flex flex-col gap-4 px-4 py-5">
452
479
  <div v-if="review" class="space-y-2 text-xs text-slate-400">
453
480
  <div class="flex items-center justify-between">
454
- <span>Findings</span>
481
+ <span>{{ t('clarity.rail.findings') }}</span>
455
482
  <span class="text-slate-300">{{ review.items.length }}</span>
456
483
  </div>
457
484
  <div class="flex items-center justify-between">
458
- <span>Open</span>
485
+ <span>{{ t('clarity.rail.open') }}</span>
459
486
  <span class="text-slate-300">{{ openCount }}</span>
460
487
  </div>
461
488
  <div class="flex items-center justify-between">
462
- <span>Answered</span>
489
+ <span>{{ t('clarity.rail.answered') }}</span>
463
490
  <span class="text-slate-300">{{ answeredCount }}</span>
464
491
  </div>
465
492
  <div v-if="review.model" class="flex items-center justify-between">
466
- <span>Model</span>
493
+ <span>{{ t('clarity.rail.model') }}</span>
467
494
  <span class="truncate pl-2 text-slate-500">{{ review.model }}</span>
468
495
  </div>
469
496
  </div>
@@ -482,7 +509,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
482
509
  :loading="acting"
483
510
  @click="proceed"
484
511
  >
485
- Proceed (nothing to incorporate)
512
+ {{ t('clarity.proceedNothing') }}
486
513
  </UButton>
487
514
  <UButton
488
515
  v-else
@@ -494,17 +521,16 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
494
521
  :disabled="!canIncorporate"
495
522
  @click="incorporate()"
496
523
  >
497
- Incorporate answers
524
+ {{ t('clarity.incorporateAnswers') }}
498
525
  </UButton>
499
526
  <p class="text-[11px] leading-relaxed text-slate-500">
500
527
  <template v-if="canProceed">
501
- Every finding is dismissed — proceed to the next phase without reworking.
528
+ {{ t('clarity.hint.proceed') }}
502
529
  </template>
503
530
  <template v-else-if="canIncorporate">
504
- Folds your answers into one clarified bug report, then re-reviews it
505
- automatically.
531
+ {{ t('clarity.hint.incorporate') }}
506
532
  </template>
507
- <template v-else> Answer or dismiss every finding to continue. </template>
533
+ <template v-else> {{ t('clarity.hint.answerAll') }} </template>
508
534
  </p>
509
535
  </div>
510
536
 
@@ -518,7 +544,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
518
544
  :loading="busy"
519
545
  @click="reReview"
520
546
  >
521
- {{ busy ? 'Re-reviewing…' : 'Looks good — re-review' }}
547
+ {{ busy ? t('clarity.reReviewing') : t('clarity.looksGoodReReview') }}
522
548
  </UButton>
523
549
  <UButton
524
550
  color="neutral"
@@ -528,7 +554,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
528
554
  icon="i-lucide-pencil"
529
555
  @click="showRedo = !showRedo"
530
556
  >
531
- Redo incorporation
557
+ {{ t('clarity.redoIncorporation') }}
532
558
  </UButton>
533
559
  <div v-if="showRedo" class="space-y-2">
534
560
  <UTextarea
@@ -537,7 +563,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
537
563
  autoresize
538
564
  size="sm"
539
565
  class="w-full"
540
- placeholder="What should the merge do differently?"
566
+ :placeholder="t('clarity.redoPlaceholder')"
541
567
  />
542
568
  <UButton
543
569
  color="primary"
@@ -549,12 +575,11 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
549
575
  :disabled="!redoComment.trim()"
550
576
  @click="incorporate(redoComment.trim())"
551
577
  >
552
- Redo with this direction
578
+ {{ t('clarity.redoWithDirection') }}
553
579
  </UButton>
554
580
  </div>
555
581
  <p class="text-[11px] leading-relaxed text-slate-500">
556
- Re-review runs the reviewer against this report. If you’re unhappy with how it was
557
- merged, redo it with a comment instead.
582
+ {{ t('clarity.redoHint') }}
558
583
  </p>
559
584
  </div>
560
585
 
@@ -562,7 +587,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
562
587
  v-if="review && incorporated"
563
588
  class="border-t border-slate-800 pt-4 text-[11px] leading-relaxed text-slate-500"
564
589
  >
565
- Bug report clarified — the pipeline is continuing with the report on the left.
590
+ {{ t('clarity.incorporatedFooter') }}
566
591
  </div>
567
592
  </div>
568
593
  </aside>
@@ -9,6 +9,8 @@
9
9
  import { computed } from 'vue'
10
10
  import type { ConsensusContribution, ConsensusSession } from '~/types/consensus'
11
11
 
12
+ const { t, n } = useI18n()
13
+
12
14
  const board = useBoardStore()
13
15
  const consensus = useConsensusStore()
14
16
 
@@ -24,36 +26,60 @@ const session = computed<ConsensusSession | null>(() =>
24
26
  )
25
27
  const loading = computed(() => (blockId.value ? consensus.isLoading(blockId.value) : false))
26
28
 
27
- const STRATEGY_LABEL: Record<string, string> = {
28
- 'specialist-panel': 'Specialist panel',
29
- debate: 'Debate',
30
- 'ranked-voting': 'Ranked voting',
29
+ // Exhaustive enum→key maps (literal key strings keep the typed-key drift guard live,
30
+ // vs a runtime-built `consensus.strategy.${value}`).
31
+ const STRATEGY_LABEL_KEYS: Record<string, string> = {
32
+ 'specialist-panel': 'consensus.strategy.specialistPanel',
33
+ debate: 'consensus.strategy.debate',
34
+ 'ranked-voting': 'consensus.strategy.rankedVoting',
35
+ }
36
+ const ROUND_LABEL_KEYS: Record<string, string> = {
37
+ draft: 'consensus.round.draft',
38
+ critique: 'consensus.round.critique',
39
+ score: 'consensus.round.score',
40
+ }
41
+ const STATUS_LABEL_KEYS: Record<string, string> = {
42
+ running: 'consensus.status.running',
43
+ synthesizing: 'consensus.status.synthesizing',
44
+ done: 'consensus.status.done',
45
+ failed: 'consensus.status.failed',
31
46
  }
32
- const ROUND_LABEL: Record<string, string> = {
33
- draft: 'Independent drafts',
34
- critique: 'Critique & revision',
35
- score: 'Scoring',
47
+ const STATUS_CLASS: Record<string, string> = {
48
+ running: 'bg-sky-500/15 text-sky-300',
49
+ synthesizing: 'bg-indigo-500/15 text-indigo-300',
50
+ done: 'bg-emerald-500/15 text-emerald-300',
51
+ failed: 'bg-rose-500/15 text-rose-300',
36
52
  }
37
53
 
38
- const STATUS_META: Record<string, { label: string; class: string }> = {
39
- running: { label: 'Running', class: 'bg-sky-500/15 text-sky-300' },
40
- synthesizing: { label: 'Synthesizing', class: 'bg-indigo-500/15 text-indigo-300' },
41
- done: { label: 'Done', class: 'bg-emerald-500/15 text-emerald-300' },
42
- failed: { label: 'Failed', class: 'bg-rose-500/15 text-rose-300' },
54
+ function strategyLabel(strategy: string): string {
55
+ const key = STRATEGY_LABEL_KEYS[strategy]
56
+ return key ? t(key) : strategy
57
+ }
58
+ function roundLabel(kind: string | null | undefined): string {
59
+ if (!kind) return t('consensus.round.contributions')
60
+ const key = ROUND_LABEL_KEYS[kind]
61
+ return key ? t(key) : kind
62
+ }
63
+ function statusLabel(status: string): string {
64
+ const key = STATUS_LABEL_KEYS[status]
65
+ return key ? t(key) : status
43
66
  }
44
67
 
45
68
  /** Anonymous label (Expert A/B/…) for a participant, matching the backend's ordering. */
46
69
  function anonLabel(participantId: string): string {
47
70
  const idx = session.value?.participants.findIndex((p) => p.id === participantId) ?? -1
48
- return `Expert ${String.fromCharCode(65 + (idx < 0 ? 0 : idx % 26))}`
71
+ return t('consensus.expert', { letter: String.fromCharCode(65 + (idx < 0 ? 0 : idx % 26)) })
49
72
  }
50
73
 
51
74
  function roleFor(participantId: string): string {
52
- return session.value?.participants.find((p) => p.id === participantId)?.role ?? 'Participant'
75
+ return (
76
+ session.value?.participants.find((p) => p.id === participantId)?.role ??
77
+ t('consensus.participant')
78
+ )
53
79
  }
54
80
 
55
- function pct(n: number | null | undefined): string {
56
- return n == null ? '—' : `${Math.round(n * 100)}%`
81
+ function pct(value: number | null | undefined): string {
82
+ return value == null ? '—' : n(value, 'percent')
57
83
  }
58
84
 
59
85
  function topScore(c: ConsensusContribution): { label: string; value: number } | null {
@@ -80,20 +106,27 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
80
106
  </div>
81
107
  <div class="min-w-0 flex-1">
82
108
  <h2 class="truncate text-sm font-semibold text-slate-100">
83
- Consensus ·
84
- {{ session ? (STRATEGY_LABEL[session.strategy] ?? session.strategy) : '' }}
109
+ {{ t('consensus.titlePrefix') }} ·
110
+ {{ session ? strategyLabel(session.strategy) : '' }}
85
111
  <span v-if="block" class="font-normal text-slate-400">— {{ block.title }}</span>
86
112
  </h2>
87
113
  <p v-if="session" class="text-xs text-slate-500">
88
- {{ session.agentKind }} · {{ session.participants.length }} participants
114
+ {{ session.agentKind }} ·
115
+ {{
116
+ t(
117
+ 'consensus.participantCount',
118
+ { count: session.participants.length },
119
+ session.participants.length,
120
+ )
121
+ }}
89
122
  </p>
90
123
  </div>
91
124
  <span
92
125
  v-if="session"
93
126
  class="rounded-full px-2.5 py-1 text-xs font-medium"
94
- :class="STATUS_META[session.status]?.class ?? 'bg-slate-700 text-slate-300'"
127
+ :class="STATUS_CLASS[session.status] ?? 'bg-slate-700 text-slate-300'"
95
128
  >
96
- {{ STATUS_META[session.status]?.label ?? session.status }}
129
+ {{ statusLabel(session.status) }}
97
130
  </span>
98
131
  <button
99
132
  class="rounded-lg p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
@@ -105,10 +138,10 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
105
138
 
106
139
  <div class="flex-1 overflow-y-auto px-6 py-5">
107
140
  <div v-if="loading && !session" class="py-16 text-center text-sm text-slate-500">
108
- Loading consensus session…
141
+ {{ t('consensus.loading') }}
109
142
  </div>
110
143
  <div v-else-if="!session" class="py-16 text-center text-sm text-slate-500">
111
- No consensus session has run for this step yet.
144
+ {{ t('consensus.empty') }}
112
145
  </div>
113
146
  <template v-else>
114
147
  <!-- failure -->
@@ -116,19 +149,19 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
116
149
  v-if="session.status === 'failed'"
117
150
  class="mb-5 rounded-lg border border-rose-800/60 bg-rose-950/40 px-4 py-3 text-sm text-rose-200"
118
151
  >
119
- Consensus failed: {{ session.error ?? 'unknown error' }}
152
+ {{ t('consensus.failed', { error: session.error ?? t('consensus.unknownError') }) }}
120
153
  </div>
121
154
 
122
155
  <!-- synthesized result -->
123
156
  <section v-if="session.synthesis" class="mb-6">
124
157
  <div class="mb-2 flex items-center gap-2">
125
158
  <h3 class="text-xs font-semibold uppercase tracking-wide text-slate-400">
126
- Synthesized result
159
+ {{ t('consensus.synthesizedResult') }}
127
160
  </h3>
128
161
  <span
129
162
  v-if="session.confidence != null"
130
163
  class="rounded bg-emerald-500/15 px-1.5 py-0.5 text-xs text-emerald-300"
131
- >confidence {{ pct(session.confidence) }}</span
164
+ >{{ t('consensus.confidence', { pct: pct(session.confidence) }) }}</span
132
165
  >
133
166
  </div>
134
167
  <pre
@@ -150,7 +183,7 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
150
183
  <!-- participants -->
151
184
  <section class="mb-6">
152
185
  <h3 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400">
153
- Panel
186
+ {{ t('consensus.panel') }}
154
187
  </h3>
155
188
  <div class="flex flex-wrap gap-2">
156
189
  <div
@@ -158,9 +191,9 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
158
191
  :key="p.id"
159
192
  class="rounded-lg border border-slate-800 bg-slate-950/40 px-3 py-1.5 text-xs"
160
193
  >
161
- <span class="font-medium text-slate-200"
162
- >Expert {{ String.fromCharCode(65 + i) }}</span
163
- >
194
+ <span class="font-medium text-slate-200">{{
195
+ t('consensus.expert', { letter: String.fromCharCode(65 + i) })
196
+ }}</span>
164
197
  <span class="text-slate-400"> · {{ p.role }}</span>
165
198
  <span v-if="p.modelId" class="ml-1 text-slate-500">({{ p.modelId }})</span>
166
199
  </div>
@@ -170,8 +203,8 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
170
203
  <!-- rounds -->
171
204
  <section v-for="round in session.rounds" :key="round.index" class="mb-5">
172
205
  <h3 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400">
173
- Round {{ round.index + 1 }} ·
174
- {{ round.kind ? (ROUND_LABEL[round.kind] ?? round.kind) : 'Contributions' }}
206
+ {{ t('consensus.round.heading', { n: round.index + 1 }) }} ·
207
+ {{ roundLabel(round.kind) }}
175
208
  </h3>
176
209
  <div class="space-y-3">
177
210
  <div
@@ -187,7 +220,12 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
187
220
  <span
188
221
  v-if="topScore(c)"
189
222
  class="ml-auto rounded bg-slate-800 px-1.5 py-0.5 text-xs text-slate-300"
190
- >top {{ topScore(c)!.label }} {{ pct(topScore(c)!.value) }}</span
223
+ >{{
224
+ t('consensus.topScore', {
225
+ label: topScore(c)!.label,
226
+ pct: pct(topScore(c)!.value),
227
+ })
228
+ }}</span
191
229
  >
192
230
  </div>
193
231
  <pre class="whitespace-pre-wrap text-sm text-slate-300">{{ c.text }}</pre>