@cat-factory/app 0.109.4 → 0.110.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/board/AddTaskModal.vue +15 -15
- package/app/components/panels/inspector/TaskRunSettings.vue +12 -12
- package/app/components/pipeline/PipelineBuilder.vue +22 -0
- package/app/components/requirements/RequirementsReviewWindow.vue +300 -203
- package/app/components/settings/{MergePresetHealthModal.vue → RiskPolicyHealthModal.vue} +19 -19
- package/app/components/settings/{MergeThresholdsPanel.vue → RiskPolicyPanel.vue} +46 -46
- package/app/components/settings/UsageSettings.vue +142 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +14 -2
- package/app/composables/api/board.ts +1 -1
- package/app/composables/api/execution.ts +5 -0
- package/app/composables/api/presets.ts +18 -18
- package/app/composables/api/reviews.ts +7 -6
- package/app/composables/{useMergePresetHealth.ts → useRiskPolicyHealth.ts} +10 -10
- package/app/pages/index.vue +9 -9
- package/app/stores/board.ts +2 -2
- package/app/stores/pipelines.ts +41 -1
- package/app/stores/requirements.ts +9 -13
- package/app/stores/{mergePresets.ts → riskPolicies.ts} +13 -13
- package/app/stores/ui.ts +17 -17
- package/app/stores/usage.ts +60 -0
- package/app/stores/workspace.spec.ts +1 -1
- package/app/stores/workspace.ts +4 -4
- package/app/types/merge.ts +3 -3
- package/app/types/requirements.ts +1 -0
- package/app/utils/{mergePreset.ts → riskPolicy.ts} +4 -4
- package/i18n/locales/de.json +52 -27
- package/i18n/locales/en.json +52 -27
- package/i18n/locales/es.json +52 -27
- package/i18n/locales/fr.json +53 -28
- package/i18n/locales/he.json +53 -28
- package/i18n/locales/it.json +53 -28
- package/i18n/locales/ja.json +52 -27
- package/i18n/locales/pl.json +53 -28
- package/i18n/locales/tr.json +52 -27
- package/i18n/locales/uk.json +53 -28
- package/package.json +2 -2
|
@@ -32,9 +32,14 @@ const drafts = ref<Record<string, string>>({})
|
|
|
32
32
|
// a draft when the recorded reply changes server-side (e.g. accepting a recommendation sets the
|
|
33
33
|
// finding's answer) WITHOUT clobbering a reply the human is actively editing.
|
|
34
34
|
const seededReply = ref<Record<string, string>>({})
|
|
35
|
-
// Findings the human
|
|
36
|
-
// click "Request recommendations"
|
|
37
|
-
|
|
35
|
+
// Findings the human switched to "recommend" mode (the per-finding 3-way selector's third
|
|
36
|
+
// option), batched until they click "Request recommendations" so the Writer runs once over the
|
|
37
|
+
// whole batch.
|
|
38
|
+
const recommendMode = ref<Set<string>>(new Set())
|
|
39
|
+
// Per-finding guidance for the Writer, keyed by finding id: whatever the human typed in the
|
|
40
|
+
// answer box before choosing "recommend" is carried here as steering guidance for THAT finding's
|
|
41
|
+
// recommendation (it steers the suggestion, it is NOT the answer itself).
|
|
42
|
+
const guidanceDrafts = ref<Record<string, string>>({})
|
|
38
43
|
// Re-request "do it differently" notes, keyed by recommendation id.
|
|
39
44
|
const reRequestNotes = ref<Record<string, string>>({})
|
|
40
45
|
// Freeform "do it differently" comment when redoing a merge the human was unhappy with.
|
|
@@ -53,7 +58,8 @@ const { open, blockId, instanceId, stepIndex, close } = useResultView('requireme
|
|
|
53
58
|
onOpen: (id) => {
|
|
54
59
|
drafts.value = {}
|
|
55
60
|
seededReply.value = {}
|
|
56
|
-
|
|
61
|
+
recommendMode.value = new Set()
|
|
62
|
+
guidanceDrafts.value = {}
|
|
57
63
|
reRequestNotes.value = {}
|
|
58
64
|
redoComment.value = ''
|
|
59
65
|
showRedo.value = false
|
|
@@ -194,6 +200,9 @@ async function flushDrafts() {
|
|
|
194
200
|
const r = review.value
|
|
195
201
|
if (!r) return
|
|
196
202
|
for (const item of r.items) {
|
|
203
|
+
// A finding switched to "recommend" mode holds GUIDANCE in its box, not an answer — never
|
|
204
|
+
// persist that as the reply.
|
|
205
|
+
if (recommendMode.value.has(item.id)) continue
|
|
197
206
|
if (item.status === 'open' || item.status === 'answered') await persistDraft(item, r)
|
|
198
207
|
}
|
|
199
208
|
}
|
|
@@ -262,16 +271,55 @@ const readyRecommendations = computed<RequirementRecommendation[]>(() =>
|
|
|
262
271
|
const generatingRecommendations = computed<RequirementRecommendation[]>(() =>
|
|
263
272
|
(review.value?.recommendations ?? []).filter((r) => r.status === 'pending'),
|
|
264
273
|
)
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
+
// Findings whose current answer is an AUTO-generated recommended default (an accepted `auto`
|
|
275
|
+
// recommendation) — keyed by finding id. These are pre-filled by the auto-recommendation
|
|
276
|
+
// automation for findings the reviewer judged answerable without a product owner; the human can
|
|
277
|
+
// keep, edit or dismiss them. Matched by the snapshotted itemId first (findings churn across
|
|
278
|
+
// re-reviews), then by title+detail. Precomputed so the template doesn't re-scan per finding.
|
|
279
|
+
const autoDefaults = computed(() => {
|
|
280
|
+
const recs = (review.value?.recommendations ?? []).filter(
|
|
281
|
+
(r) => r.auto === true && r.status === 'accepted',
|
|
282
|
+
)
|
|
283
|
+
const map = new Map<string, RequirementRecommendation>()
|
|
284
|
+
for (const item of review.value?.items ?? []) {
|
|
285
|
+
const rec = recs.find(
|
|
286
|
+
(r) =>
|
|
287
|
+
r.sourceFinding.itemId === item.id ||
|
|
288
|
+
(r.sourceFinding.title === item.title && r.sourceFinding.detail === item.detail),
|
|
289
|
+
)
|
|
290
|
+
if (rec) map.set(item.id, rec)
|
|
291
|
+
}
|
|
292
|
+
return map
|
|
274
293
|
})
|
|
294
|
+
// True once the automation has pre-answered at least one finding — used to flag the REMAINING
|
|
295
|
+
// open findings (the genuine business decisions the reviewer left for the human) as needing input.
|
|
296
|
+
const hasAutoDefaults = computed(() => autoDefaults.value.size > 0)
|
|
297
|
+
// Recommendations rendered INLINE inside their source finding card (rather than in a separate
|
|
298
|
+
// section): the human-requested ones that are `ready` (awaiting accept/reject) or still `pending`
|
|
299
|
+
// (generating). Keyed by finding id, matched by snapshotted itemId first then title+detail.
|
|
300
|
+
function recFor(item: RequirementReviewItem, status: 'ready' | 'pending') {
|
|
301
|
+
return (review.value?.recommendations ?? []).find(
|
|
302
|
+
(r) =>
|
|
303
|
+
r.status === status &&
|
|
304
|
+
r.auto !== true &&
|
|
305
|
+
(r.sourceFinding.itemId === item.id ||
|
|
306
|
+
(r.sourceFinding.title === item.title && r.sourceFinding.detail === item.detail)),
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
function readyRecFor(item: RequirementReviewItem) {
|
|
310
|
+
return recFor(item, 'ready')
|
|
311
|
+
}
|
|
312
|
+
function pendingRecFor(item: RequirementReviewItem) {
|
|
313
|
+
return recFor(item, 'pending')
|
|
314
|
+
}
|
|
315
|
+
// Whether a finding's recorded reply is the human's OWN answer (vs an untouched auto-generated
|
|
316
|
+
// recommended default). Drives the "User answered" marker on the Answer option.
|
|
317
|
+
function isUserAnswered(item: RequirementReviewItem): boolean {
|
|
318
|
+
const reply = (item.reply ?? '').trim()
|
|
319
|
+
if (!reply) return false
|
|
320
|
+
const auto = autoDefaults.value.get(item.id)
|
|
321
|
+
return !auto || auto.recommendedText.trim() !== reply
|
|
322
|
+
}
|
|
275
323
|
// Whether the human still has something to act on (findings to answer/dismiss or recommendations
|
|
276
324
|
// to decide). Drives the incorporated-document default collapse so the reference doc doesn't push
|
|
277
325
|
// the actionable findings/recommendations off-screen while there's still work.
|
|
@@ -307,27 +355,77 @@ watch(status, () => {
|
|
|
307
355
|
docCollapsedOverride.value = null
|
|
308
356
|
})
|
|
309
357
|
|
|
310
|
-
|
|
311
|
-
|
|
358
|
+
// The per-finding 3-way selector: which of Answer / Dismiss / Recommend is currently active.
|
|
359
|
+
// Derived from the finding's status plus the local `recommendMode` set (so a freshly-toggled
|
|
360
|
+
// finding shows "recommend" before any request has fired).
|
|
361
|
+
type FindingMode = 'answer' | 'dismiss' | 'recommend'
|
|
362
|
+
const FINDING_MODES = [
|
|
363
|
+
{ mode: 'answer', labelKey: 'requirements.mode.answer', icon: 'i-lucide-pencil-line' },
|
|
364
|
+
{ mode: 'dismiss', labelKey: 'requirements.mode.dismiss', icon: 'i-lucide-x' },
|
|
365
|
+
{ mode: 'recommend', labelKey: 'requirements.mode.recommend', icon: 'i-lucide-wand-2' },
|
|
366
|
+
] as const satisfies ReadonlyArray<{ mode: FindingMode; labelKey: string; icon: string }>
|
|
367
|
+
function modeFor(item: RequirementReviewItem): FindingMode {
|
|
368
|
+
if (item.status === 'dismissed') return 'dismiss'
|
|
369
|
+
if (recommendMode.value.has(item.id) || item.status === 'recommend_requested') return 'recommend'
|
|
370
|
+
return 'answer'
|
|
312
371
|
}
|
|
313
|
-
function
|
|
314
|
-
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
372
|
+
async function setMode(item: RequirementReviewItem, mode: FindingMode) {
|
|
373
|
+
if (frozen.value || modeFor(item) === mode) return
|
|
374
|
+
if (mode === 'dismiss') {
|
|
375
|
+
dropRecommendMode(item.id)
|
|
376
|
+
await setStatus(item, 'dismissed')
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
if (mode === 'answer') {
|
|
380
|
+
dropRecommendMode(item.id)
|
|
381
|
+
// Re-open a dismissed finding so its answer box is editable again.
|
|
382
|
+
if (item.status === 'dismissed') await setStatus(item, 'open')
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
// Recommend: carry whatever the human typed in the answer box over as the starting guidance,
|
|
386
|
+
// then flag the finding for the batch request (fired from the action rail).
|
|
387
|
+
const carried = (drafts.value[item.id] ?? '').trim()
|
|
388
|
+
if (carried && !(guidanceDrafts.value[item.id] ?? '').trim()) {
|
|
389
|
+
guidanceDrafts.value = { ...guidanceDrafts.value, [item.id]: carried }
|
|
390
|
+
}
|
|
391
|
+
recommendMode.value = new Set(recommendMode.value).add(item.id)
|
|
318
392
|
}
|
|
393
|
+
function dropRecommendMode(id: string) {
|
|
394
|
+
if (!recommendMode.value.has(id)) return
|
|
395
|
+
const next = new Set(recommendMode.value)
|
|
396
|
+
next.delete(id)
|
|
397
|
+
recommendMode.value = next
|
|
398
|
+
}
|
|
399
|
+
// Findings currently flagged for a recommendation request (recommend mode, not yet requested).
|
|
400
|
+
const pendingRecommendRequests = computed(() =>
|
|
401
|
+
(review.value?.items ?? []).filter(
|
|
402
|
+
(i) =>
|
|
403
|
+
recommendMode.value.has(i.id) &&
|
|
404
|
+
i.status !== 'dismissed' &&
|
|
405
|
+
!pendingRecFor(i) &&
|
|
406
|
+
!readyRecFor(i),
|
|
407
|
+
),
|
|
408
|
+
)
|
|
319
409
|
|
|
320
410
|
// Fire the Writer over the whole marked batch (grounded on the project's best-practice
|
|
321
411
|
// standards, specs/tech-specs and web search). ASYNCHRONOUS: it returns at once with `pending`
|
|
322
412
|
// placeholders that fill in live; the user can close the window and is notified when the batch
|
|
323
413
|
// is ready. Flush any typed-but-unblurred answers first so nothing the human entered is lost.
|
|
324
414
|
async function requestRecommendations() {
|
|
325
|
-
if (!blockId.value
|
|
326
|
-
const
|
|
415
|
+
if (!blockId.value) return
|
|
416
|
+
const targets = pendingRecommendRequests.value
|
|
417
|
+
if (targets.length === 0) return
|
|
418
|
+
// Each finding carries its own guidance (transformed from what the human typed in its box);
|
|
419
|
+
// an empty guidance is omitted so the Writer falls back to grounding alone.
|
|
420
|
+
const items = targets.map((item) => {
|
|
421
|
+
const note = (guidanceDrafts.value[item.id] ?? '').trim()
|
|
422
|
+
return note ? { itemId: item.id, note } : { itemId: item.id }
|
|
423
|
+
})
|
|
424
|
+
const ids = targets.map((i) => i.id)
|
|
327
425
|
try {
|
|
328
426
|
await flushDrafts()
|
|
329
|
-
const updated = await requirements.requestRecommendations(blockId.value,
|
|
330
|
-
|
|
427
|
+
const updated = await requirements.requestRecommendations(blockId.value, items)
|
|
428
|
+
recommendMode.value = new Set()
|
|
331
429
|
const n = ids.length
|
|
332
430
|
// On a parked run the request returns at once with `pending` placeholders the durable driver
|
|
333
431
|
// fills in the background; off-path (no active pipeline) there is no driver, so the Writer
|
|
@@ -594,6 +692,20 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
594
692
|
<UBadge size="xs" variant="outline" color="neutral">
|
|
595
693
|
{{ CATEGORY_LABELS[item.category] }}
|
|
596
694
|
</UBadge>
|
|
695
|
+
<!-- Once the automation has pre-answered some findings, flag the ones it
|
|
696
|
+
left open as the genuine business decisions that need the human. -->
|
|
697
|
+
<UBadge
|
|
698
|
+
v-if="
|
|
699
|
+
hasAutoDefaults &&
|
|
700
|
+
item.status === 'open' &&
|
|
701
|
+
item.autoAnswerable === false
|
|
702
|
+
"
|
|
703
|
+
size="xs"
|
|
704
|
+
variant="subtle"
|
|
705
|
+
color="warning"
|
|
706
|
+
>
|
|
707
|
+
{{ t('requirements.needsYourInput') }}
|
|
708
|
+
</UBadge>
|
|
597
709
|
<UBadge
|
|
598
710
|
size="xs"
|
|
599
711
|
variant="soft"
|
|
@@ -619,196 +731,179 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
619
731
|
<p class="whitespace-pre-line">{{ item.reply }}</p>
|
|
620
732
|
</div>
|
|
621
733
|
|
|
622
|
-
<!--
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
autoresize
|
|
630
|
-
size="sm"
|
|
631
|
-
class="mt-2 w-full"
|
|
632
|
-
:placeholder="t('requirements.answerPlaceholder')"
|
|
633
|
-
:disabled="frozen"
|
|
634
|
-
@blur="persistDraft(item)"
|
|
635
|
-
/>
|
|
636
|
-
<div class="mt-2 flex flex-wrap items-center gap-2">
|
|
734
|
+
<!-- per-finding 3-way selector: Answer (write it) / Dismiss (irrelevant) /
|
|
735
|
+
Recommend (let the Requirement Writer suggest one). The active mode
|
|
736
|
+
drives the content below, IN PLACE — no separate section. Disabled once
|
|
737
|
+
the requirements are settled / a cycle is running; hidden for a
|
|
738
|
+
`resolved` finding (its recorded answer shows above). -->
|
|
739
|
+
<template v-if="item.status !== 'resolved'">
|
|
740
|
+
<div class="mt-2 flex flex-wrap items-center gap-1">
|
|
637
741
|
<UButton
|
|
638
|
-
|
|
639
|
-
|
|
742
|
+
v-for="opt in FINDING_MODES"
|
|
743
|
+
:key="opt.mode"
|
|
744
|
+
:color="modeFor(item) === opt.mode ? 'primary' : 'neutral'"
|
|
745
|
+
:variant="modeFor(item) === opt.mode ? 'soft' : 'ghost'"
|
|
640
746
|
size="xs"
|
|
641
|
-
icon="
|
|
747
|
+
:icon="opt.icon"
|
|
642
748
|
:disabled="frozen"
|
|
643
|
-
@click="
|
|
749
|
+
@click="setMode(item, opt.mode)"
|
|
644
750
|
>
|
|
645
|
-
{{ t(
|
|
751
|
+
{{ t(opt.labelKey) }}
|
|
752
|
+
<UIcon
|
|
753
|
+
v-if="opt.mode === 'answer' && isUserAnswered(item)"
|
|
754
|
+
name="i-lucide-check"
|
|
755
|
+
class="h-3.5 w-3.5 text-emerald-400"
|
|
756
|
+
/>
|
|
646
757
|
</UButton>
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
758
|
+
</div>
|
|
759
|
+
|
|
760
|
+
<!-- ANSWER: type the answer directly (auto-saves on blur) -->
|
|
761
|
+
<template v-if="modeFor(item) === 'answer'">
|
|
762
|
+
<!-- Auto-generated recommended default: the automation pre-filled this
|
|
763
|
+
answer; the human can keep it, edit it, or switch modes. -->
|
|
764
|
+
<div
|
|
765
|
+
v-if="autoDefaults.get(item.id)"
|
|
766
|
+
class="mt-2 flex flex-wrap items-center gap-1.5 text-xs text-indigo-300"
|
|
767
|
+
>
|
|
768
|
+
<UIcon name="i-lucide-sparkles" class="h-3.5 w-3.5 shrink-0" />
|
|
769
|
+
<span>{{ t('requirements.recommendedDefault') }}</span>
|
|
770
|
+
<UBadge
|
|
771
|
+
v-if="autoDefaults.get(item.id)!.groundedInFragment"
|
|
772
|
+
size="xs"
|
|
773
|
+
variant="subtle"
|
|
774
|
+
color="primary"
|
|
775
|
+
>
|
|
776
|
+
{{
|
|
777
|
+
t('requirements.currentStandard', {
|
|
778
|
+
title: autoDefaults.get(item.id)!.groundedInFragment!.title,
|
|
779
|
+
})
|
|
780
|
+
}}
|
|
781
|
+
</UBadge>
|
|
782
|
+
</div>
|
|
783
|
+
<UTextarea
|
|
784
|
+
v-model="drafts[item.id]"
|
|
785
|
+
:rows="2"
|
|
786
|
+
autoresize
|
|
787
|
+
size="sm"
|
|
788
|
+
class="mt-2 w-full"
|
|
789
|
+
:placeholder="t('requirements.answerPlaceholder')"
|
|
652
790
|
:disabled="frozen"
|
|
653
|
-
@
|
|
791
|
+
@blur="persistDraft(item)"
|
|
792
|
+
/>
|
|
793
|
+
<p
|
|
794
|
+
v-if="isUserAnswered(item)"
|
|
795
|
+
class="mt-1 flex items-center gap-1 text-[11px] text-emerald-400"
|
|
654
796
|
>
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}}
|
|
660
|
-
</UButton>
|
|
661
|
-
</div>
|
|
662
|
-
</template>
|
|
797
|
+
<UIcon name="i-lucide-check" class="h-3 w-3 shrink-0" />
|
|
798
|
+
{{ t('requirements.userAnswered') }}
|
|
799
|
+
</p>
|
|
800
|
+
</template>
|
|
663
801
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
v-else-if="item
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
802
|
+
<!-- RECOMMEND: generating / the ready suggestion / a guidance box, all
|
|
803
|
+
rendered inline where the question was asked. -->
|
|
804
|
+
<template v-else-if="modeFor(item) === 'recommend'">
|
|
805
|
+
<div
|
|
806
|
+
v-if="pendingRecFor(item)"
|
|
807
|
+
class="mt-2 flex items-center gap-1.5 text-xs text-indigo-300"
|
|
808
|
+
>
|
|
809
|
+
<UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin" />
|
|
810
|
+
{{ t('requirements.generatingSuggestion') }}
|
|
811
|
+
</div>
|
|
812
|
+
<template v-else-if="readyRecFor(item)">
|
|
813
|
+
<div
|
|
814
|
+
v-for="rec in [readyRecFor(item)!]"
|
|
815
|
+
:key="rec.id"
|
|
816
|
+
class="mt-2 rounded-lg border border-indigo-900/50 bg-indigo-950/20 p-3"
|
|
817
|
+
>
|
|
818
|
+
<UBadge
|
|
819
|
+
v-if="rec.groundedInFragment"
|
|
820
|
+
size="xs"
|
|
821
|
+
variant="subtle"
|
|
822
|
+
color="success"
|
|
823
|
+
icon="i-lucide-badge-check"
|
|
824
|
+
>
|
|
825
|
+
{{
|
|
826
|
+
t('requirements.currentStandard', {
|
|
827
|
+
title: rec.groundedInFragment.title,
|
|
828
|
+
})
|
|
829
|
+
}}
|
|
830
|
+
</UBadge>
|
|
831
|
+
<p class="mt-1 whitespace-pre-line text-sm text-slate-300">
|
|
832
|
+
{{ rec.recommendedText }}
|
|
833
|
+
</p>
|
|
834
|
+
<div class="mt-2 flex flex-wrap items-center gap-2">
|
|
835
|
+
<UButton
|
|
836
|
+
color="primary"
|
|
837
|
+
variant="soft"
|
|
838
|
+
size="xs"
|
|
839
|
+
icon="i-lucide-check"
|
|
840
|
+
:disabled="frozen"
|
|
841
|
+
@click="acceptRecommendation(rec)"
|
|
842
|
+
>
|
|
843
|
+
{{ t('requirements.accept') }}
|
|
844
|
+
</UButton>
|
|
845
|
+
<UButton
|
|
846
|
+
color="neutral"
|
|
847
|
+
variant="ghost"
|
|
848
|
+
size="xs"
|
|
849
|
+
icon="i-lucide-x"
|
|
850
|
+
:disabled="frozen"
|
|
851
|
+
@click="rejectRecommendation(rec)"
|
|
852
|
+
>
|
|
853
|
+
{{ t('requirements.reject') }}
|
|
854
|
+
</UButton>
|
|
855
|
+
</div>
|
|
856
|
+
<div class="mt-2 flex items-start gap-2">
|
|
857
|
+
<UTextarea
|
|
858
|
+
v-model="reRequestNotes[rec.id]"
|
|
859
|
+
:rows="1"
|
|
860
|
+
autoresize
|
|
861
|
+
size="sm"
|
|
862
|
+
class="flex-1"
|
|
863
|
+
:placeholder="t('requirements.reRequestPlaceholder')"
|
|
864
|
+
:disabled="frozen || recommending"
|
|
865
|
+
/>
|
|
866
|
+
<UButton
|
|
867
|
+
color="neutral"
|
|
868
|
+
variant="soft"
|
|
869
|
+
size="xs"
|
|
870
|
+
icon="i-lucide-rotate-cw"
|
|
871
|
+
:loading="recommending"
|
|
872
|
+
:disabled="!(reRequestNotes[rec.id] ?? '').trim() || frozen"
|
|
873
|
+
@click="reRequestRecommendation(rec)"
|
|
874
|
+
>
|
|
875
|
+
{{ t('requirements.reRequest') }}
|
|
876
|
+
</UButton>
|
|
877
|
+
</div>
|
|
878
|
+
</div>
|
|
879
|
+
</template>
|
|
880
|
+
<template v-else>
|
|
881
|
+
<UTextarea
|
|
882
|
+
v-model="guidanceDrafts[item.id]"
|
|
883
|
+
:rows="2"
|
|
884
|
+
autoresize
|
|
885
|
+
size="sm"
|
|
886
|
+
class="mt-2 w-full"
|
|
887
|
+
:placeholder="t('requirements.guidancePlaceholder')"
|
|
888
|
+
:disabled="frozen"
|
|
889
|
+
/>
|
|
890
|
+
<p class="mt-1 flex items-center gap-1 text-[11px] text-indigo-300/80">
|
|
891
|
+
<UIcon name="i-lucide-wand-2" class="h-3 w-3 shrink-0" />
|
|
892
|
+
{{ t('requirements.guidanceHint') }}
|
|
893
|
+
</p>
|
|
894
|
+
</template>
|
|
895
|
+
</template>
|
|
672
896
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
size="xs"
|
|
679
|
-
icon="i-lucide-rotate-ccw"
|
|
680
|
-
:disabled="frozen"
|
|
681
|
-
@click="setStatus(item, 'open')"
|
|
682
|
-
>
|
|
683
|
-
{{ t('requirements.reopen') }}
|
|
684
|
-
</UButton>
|
|
685
|
-
</div>
|
|
897
|
+
<!-- DISMISS: nothing to fill in — a short note explains the effect -->
|
|
898
|
+
<p v-else class="mt-2 text-[11px] text-slate-500">
|
|
899
|
+
{{ t('requirements.dismissedHint') }}
|
|
900
|
+
</p>
|
|
901
|
+
</template>
|
|
686
902
|
</div>
|
|
687
903
|
</div>
|
|
688
904
|
</div>
|
|
689
905
|
</div>
|
|
690
906
|
|
|
691
|
-
<!-- Requirement-Writer recommendations: awaiting a human decision (`ready`) and/or
|
|
692
|
-
still generating in the background (`pending`) -->
|
|
693
|
-
<section
|
|
694
|
-
v-if="readyRecommendations.length || generatingRecommendations.length"
|
|
695
|
-
class="mt-6 border-t border-slate-800 pt-5"
|
|
696
|
-
>
|
|
697
|
-
<div class="mb-3 flex items-center gap-2 text-[11px] text-indigo-300">
|
|
698
|
-
<UIcon name="i-lucide-wand-2" class="h-3.5 w-3.5" />
|
|
699
|
-
<span class="font-semibold uppercase tracking-wide">{{
|
|
700
|
-
t('requirements.recommendedAnswers')
|
|
701
|
-
}}</span>
|
|
702
|
-
<span
|
|
703
|
-
v-if="recommendationProgress"
|
|
704
|
-
class="ms-auto flex items-center gap-1.5 normal-case text-indigo-300/80"
|
|
705
|
-
>
|
|
706
|
-
<UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin" />
|
|
707
|
-
{{
|
|
708
|
-
t('requirements.recommendationProgress', {
|
|
709
|
-
ready: recommendationProgress.ready,
|
|
710
|
-
total: recommendationProgress.total,
|
|
711
|
-
})
|
|
712
|
-
}}
|
|
713
|
-
</span>
|
|
714
|
-
</div>
|
|
715
|
-
|
|
716
|
-
<!-- still-generating placeholders (one per requested finding) -->
|
|
717
|
-
<div v-if="generatingRecommendations.length" class="mb-3 flex flex-col gap-3">
|
|
718
|
-
<div
|
|
719
|
-
v-for="rec in generatingRecommendations"
|
|
720
|
-
:key="rec.id"
|
|
721
|
-
class="flex items-start gap-2 rounded-lg border border-dashed border-indigo-900/50 bg-indigo-950/10 p-3"
|
|
722
|
-
>
|
|
723
|
-
<UIcon
|
|
724
|
-
name="i-lucide-loader-circle"
|
|
725
|
-
class="mt-0.5 h-4 w-4 shrink-0 animate-spin text-indigo-300"
|
|
726
|
-
/>
|
|
727
|
-
<div class="min-w-0">
|
|
728
|
-
<span class="text-sm font-medium text-white">{{
|
|
729
|
-
rec.sourceFinding.title
|
|
730
|
-
}}</span>
|
|
731
|
-
<p class="text-xs text-indigo-300/70">
|
|
732
|
-
{{ t('requirements.generatingSuggestion') }}
|
|
733
|
-
</p>
|
|
734
|
-
</div>
|
|
735
|
-
</div>
|
|
736
|
-
</div>
|
|
737
|
-
|
|
738
|
-
<div class="flex flex-col gap-3">
|
|
739
|
-
<div
|
|
740
|
-
v-for="rec in readyRecommendations"
|
|
741
|
-
:key="rec.id"
|
|
742
|
-
class="rounded-lg border border-indigo-900/50 bg-indigo-950/20 p-3"
|
|
743
|
-
>
|
|
744
|
-
<div class="flex flex-wrap items-center gap-1.5">
|
|
745
|
-
<span class="text-sm font-medium text-white">{{
|
|
746
|
-
rec.sourceFinding.title
|
|
747
|
-
}}</span>
|
|
748
|
-
<UBadge
|
|
749
|
-
v-if="rec.groundedInFragment"
|
|
750
|
-
size="xs"
|
|
751
|
-
variant="subtle"
|
|
752
|
-
color="success"
|
|
753
|
-
icon="i-lucide-badge-check"
|
|
754
|
-
>
|
|
755
|
-
{{
|
|
756
|
-
t('requirements.currentStandard', { title: rec.groundedInFragment.title })
|
|
757
|
-
}}
|
|
758
|
-
</UBadge>
|
|
759
|
-
</div>
|
|
760
|
-
<p class="mt-2 whitespace-pre-line text-sm text-slate-300">
|
|
761
|
-
{{ rec.recommendedText }}
|
|
762
|
-
</p>
|
|
763
|
-
<div class="mt-2 flex flex-wrap items-center gap-2">
|
|
764
|
-
<UButton
|
|
765
|
-
color="primary"
|
|
766
|
-
variant="soft"
|
|
767
|
-
size="xs"
|
|
768
|
-
icon="i-lucide-check"
|
|
769
|
-
:disabled="frozen"
|
|
770
|
-
@click="acceptRecommendation(rec)"
|
|
771
|
-
>
|
|
772
|
-
{{ t('requirements.accept') }}
|
|
773
|
-
</UButton>
|
|
774
|
-
<UButton
|
|
775
|
-
color="neutral"
|
|
776
|
-
variant="ghost"
|
|
777
|
-
size="xs"
|
|
778
|
-
icon="i-lucide-x"
|
|
779
|
-
:disabled="frozen"
|
|
780
|
-
@click="rejectRecommendation(rec)"
|
|
781
|
-
>
|
|
782
|
-
{{ t('requirements.reject') }}
|
|
783
|
-
</UButton>
|
|
784
|
-
</div>
|
|
785
|
-
<!-- re-request with a note (an alternative to rejecting outright) -->
|
|
786
|
-
<div class="mt-2 flex items-start gap-2">
|
|
787
|
-
<UTextarea
|
|
788
|
-
v-model="reRequestNotes[rec.id]"
|
|
789
|
-
:rows="1"
|
|
790
|
-
autoresize
|
|
791
|
-
size="sm"
|
|
792
|
-
class="flex-1"
|
|
793
|
-
:placeholder="t('requirements.reRequestPlaceholder')"
|
|
794
|
-
:disabled="frozen || recommending"
|
|
795
|
-
/>
|
|
796
|
-
<UButton
|
|
797
|
-
color="neutral"
|
|
798
|
-
variant="soft"
|
|
799
|
-
size="xs"
|
|
800
|
-
icon="i-lucide-rotate-cw"
|
|
801
|
-
:loading="recommending"
|
|
802
|
-
:disabled="!(reRequestNotes[rec.id] ?? '').trim() || frozen"
|
|
803
|
-
@click="reRequestRecommendation(rec)"
|
|
804
|
-
>
|
|
805
|
-
{{ t('requirements.reRequest') }}
|
|
806
|
-
</UButton>
|
|
807
|
-
</div>
|
|
808
|
-
</div>
|
|
809
|
-
</div>
|
|
810
|
-
</section>
|
|
811
|
-
|
|
812
907
|
<!-- incorporated document: the standard-format requirements. The whole section
|
|
813
908
|
collapses as a unit (a long doc otherwise pushes the findings/recommendations
|
|
814
909
|
off-screen); the per-heading toggles below still work when it's expanded. -->
|
|
@@ -918,7 +1013,9 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
918
1013
|
bare `!frozen`, which would also expose it in `exceeded`, where the run is parked
|
|
919
1014
|
on the cap decision and a fresh recommendation batch has no path to settle). -->
|
|
920
1015
|
<div
|
|
921
|
-
v-if="
|
|
1016
|
+
v-if="
|
|
1017
|
+
review && pendingRecommendRequests.length > 0 && (status === 'ready' || merged)
|
|
1018
|
+
"
|
|
922
1019
|
class="border-t border-slate-800 pt-4"
|
|
923
1020
|
>
|
|
924
1021
|
<UButton
|
|
@@ -933,8 +1030,8 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
933
1030
|
{{
|
|
934
1031
|
t(
|
|
935
1032
|
'requirements.actions.requestRecommendations',
|
|
936
|
-
{ count:
|
|
937
|
-
|
|
1033
|
+
{ count: pendingRecommendRequests.length },
|
|
1034
|
+
pendingRecommendRequests.length,
|
|
938
1035
|
)
|
|
939
1036
|
}}
|
|
940
1037
|
</UButton>
|