@cat-factory/app 0.164.0 → 0.166.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 +12 -36
- package/app/components/panels/inspector/TaskRunSettings.vue +40 -32
- package/app/components/requirements/RequirementsReviewWindow.logic.spec.ts +145 -0
- package/app/components/requirements/RequirementsReviewWindow.logic.ts +105 -0
- package/app/components/requirements/RequirementsReviewWindow.vue +303 -215
- package/app/components/riskPolicy/RiskPolicyPicker.logic.spec.ts +75 -0
- package/app/components/riskPolicy/RiskPolicyPicker.logic.ts +47 -0
- package/app/components/riskPolicy/RiskPolicyPicker.vue +159 -0
- package/app/components/riskPolicy/RiskPolicyPreview.vue +83 -0
- package/app/components/settings/RiskPolicyPanel.vue +37 -54
- package/app/utils/riskPolicy.spec.ts +53 -0
- package/app/utils/riskPolicy.ts +35 -14
- package/i18n/locales/de.json +29 -4
- package/i18n/locales/en.json +29 -4
- package/i18n/locales/es.json +29 -4
- package/i18n/locales/fr.json +29 -4
- package/i18n/locales/he.json +29 -4
- package/i18n/locales/it.json +29 -4
- package/i18n/locales/ja.json +29 -4
- package/i18n/locales/pl.json +29 -4
- package/i18n/locales/tr.json +29 -4
- package/i18n/locales/uk.json +29 -4
- package/package.json +1 -1
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
import { parseOutputOutline } from '~/utils/agentOutput'
|
|
11
11
|
import IterationCapPrompt from '~/components/pipeline/IterationCapPrompt.vue'
|
|
12
12
|
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
13
|
+
import {
|
|
14
|
+
orderFindings,
|
|
15
|
+
reconcileFindingOrder,
|
|
16
|
+
type FindingAttention,
|
|
17
|
+
type OrderedFinding,
|
|
18
|
+
} from './RequirementsReviewWindow.logic'
|
|
13
19
|
import type {
|
|
14
20
|
RequirementRecommendation,
|
|
15
21
|
RequirementReview,
|
|
@@ -48,6 +54,11 @@ const showRedo = ref(false)
|
|
|
48
54
|
// Human's explicit collapse choice for the whole incorporated-requirements section; null = follow
|
|
49
55
|
// the default (collapse it while there's still work to do so it doesn't dominate the window).
|
|
50
56
|
const docCollapsedOverride = ref<boolean | null>(null)
|
|
57
|
+
// State of the floating findings order (the section that computes it, further down, explains the
|
|
58
|
+
// pinning rule). Declared up here because `onOpen` fires SYNCHRONOUSLY during setup and resets
|
|
59
|
+
// them — a ref declared below would be in its temporal dead zone at that point.
|
|
60
|
+
const pinnedOrder = ref<OrderedFinding[] | null>(null)
|
|
61
|
+
const editingFinding = ref(false)
|
|
51
62
|
|
|
52
63
|
// The seam contract (open/blockId/close + Escape handling + load-on-open) lives in
|
|
53
64
|
// `useResultView`, so this window can't drift from the others. Declaring `onOpen` makes the
|
|
@@ -64,6 +75,10 @@ const { open, blockId, instanceId, stepIndex, close } = useResultView('requireme
|
|
|
64
75
|
redoComment.value = ''
|
|
65
76
|
showRedo.value = false
|
|
66
77
|
docCollapsedOverride.value = null
|
|
78
|
+
// Re-open is the settle point that matters most: whatever is still outstanding floats back to
|
|
79
|
+
// the top rather than inheriting the order the previous session was pinned to.
|
|
80
|
+
pinnedOrder.value = null
|
|
81
|
+
editingFinding.value = false
|
|
67
82
|
void requirements.load(blockId)
|
|
68
83
|
},
|
|
69
84
|
// Closing the window (X, backdrop, Escape) must not silently drop an answer the user typed
|
|
@@ -84,14 +99,6 @@ const reworking = computed(() =>
|
|
|
84
99
|
)
|
|
85
100
|
const acting = ref(false)
|
|
86
101
|
|
|
87
|
-
const SEVERITY_RANK: Record<ReviewItemSeverity, number> = { high: 0, medium: 1, low: 2 }
|
|
88
|
-
const sortedItems = computed<RequirementReviewItem[]>(() => {
|
|
89
|
-
if (!review.value) return []
|
|
90
|
-
return [...review.value.items].sort(
|
|
91
|
-
(a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity],
|
|
92
|
-
)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
102
|
const openCount = computed(() => (review.value ? requirements.openCount(review.value) : 0))
|
|
96
103
|
const answeredCount = computed(() => (review.value ? requirements.answeredCount(review.value) : 0))
|
|
97
104
|
const status = computed(() => review.value?.status ?? null)
|
|
@@ -312,6 +319,71 @@ function readyRecFor(item: RequirementReviewItem) {
|
|
|
312
319
|
function pendingRecFor(item: RequirementReviewItem) {
|
|
313
320
|
return recFor(item, 'pending')
|
|
314
321
|
}
|
|
322
|
+
|
|
323
|
+
// --- Floating findings order ------------------------------------------------
|
|
324
|
+
// Findings the human still owes a reaction float to the top, then the ones the Writer is still
|
|
325
|
+
// thinking about, then everything already handled — so a long review (or a re-review, which
|
|
326
|
+
// re-raises unresolved findings alongside new ones) doesn't leave the outstanding work scattered
|
|
327
|
+
// between settled cards. Severity remains the order WITHIN a bucket. Ranking is pure
|
|
328
|
+
// (`RequirementsReviewWindow.logic.ts`); the window only decides WHEN to apply it.
|
|
329
|
+
const desiredOrder = computed<OrderedFinding[]>(() =>
|
|
330
|
+
orderFindings(review.value?.items ?? [], (item) => ({
|
|
331
|
+
pending: !!pendingRecFor(item),
|
|
332
|
+
ready: !!readyRecFor(item),
|
|
333
|
+
})),
|
|
334
|
+
)
|
|
335
|
+
// `pinnedOrder` (declared at the top of the script, since `onOpen` resets it) holds the order
|
|
336
|
+
// actually rendered; null means "use the computed one". It is pinned while `editingFinding` — focus
|
|
337
|
+
// inside one of a finding's text boxes — is true, because an answer auto-saves on BLUR, which
|
|
338
|
+
// re-buckets that finding: re-sorting right then would slide the card the user just clicked into
|
|
339
|
+
// out from under their cursor. Focus moving straight from one box to the next never lets the flag
|
|
340
|
+
// settle to `false`, so the list holds still for a whole editing burst and the remaining work
|
|
341
|
+
// floats back up the moment they leave it.
|
|
342
|
+
function isTextEntry(target: EventTarget | null): boolean {
|
|
343
|
+
return target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement
|
|
344
|
+
}
|
|
345
|
+
function onFindingFocusIn(event: FocusEvent) {
|
|
346
|
+
if (isTextEntry(event.target)) editingFinding.value = true
|
|
347
|
+
}
|
|
348
|
+
function onFindingFocusOut(event: FocusEvent) {
|
|
349
|
+
if (isTextEntry(event.target)) editingFinding.value = false
|
|
350
|
+
}
|
|
351
|
+
watch(
|
|
352
|
+
[desiredOrder, editingFinding],
|
|
353
|
+
() => {
|
|
354
|
+
if (!editingFinding.value) pinnedOrder.value = desiredOrder.value
|
|
355
|
+
},
|
|
356
|
+
{ immediate: true },
|
|
357
|
+
)
|
|
358
|
+
const orderedFindings = computed<{ item: RequirementReviewItem; attention: FindingAttention }[]>(
|
|
359
|
+
() => {
|
|
360
|
+
const byId = new Map((review.value?.items ?? []).map((item) => [item.id, item]))
|
|
361
|
+
return reconcileFindingOrder(desiredOrder.value, pinnedOrder.value).flatMap((entry) => {
|
|
362
|
+
const item = byId.get(entry.id)
|
|
363
|
+
return item ? [{ item, attention: entry.attention }] : []
|
|
364
|
+
})
|
|
365
|
+
},
|
|
366
|
+
)
|
|
367
|
+
// Label the buckets only once the list actually spans more than one — on a fresh review every
|
|
368
|
+
// finding is outstanding, and a lone "Needs your reaction" heading over all of them is noise.
|
|
369
|
+
const attentionGroupsShown = computed(
|
|
370
|
+
() => new Set(orderedFindings.value.map((entry) => entry.attention)).size > 1,
|
|
371
|
+
)
|
|
372
|
+
function startsAttentionGroup(index: number): boolean {
|
|
373
|
+
if (!attentionGroupsShown.value) return false
|
|
374
|
+
const entries = orderedFindings.value
|
|
375
|
+
return index === 0 || entries[index - 1]?.attention !== entries[index]?.attention
|
|
376
|
+
}
|
|
377
|
+
const ATTENTION_LABELS = computed<Record<FindingAttention, string>>(() => ({
|
|
378
|
+
action: t('requirements.group.action'),
|
|
379
|
+
waiting: t('requirements.group.waiting'),
|
|
380
|
+
settled: t('requirements.group.settled'),
|
|
381
|
+
}))
|
|
382
|
+
const ATTENTION_LABEL_COLOR = {
|
|
383
|
+
action: 'text-amber-300',
|
|
384
|
+
waiting: 'text-indigo-300',
|
|
385
|
+
settled: 'text-slate-500',
|
|
386
|
+
} as const satisfies Record<FindingAttention, string>
|
|
315
387
|
// Whether a finding's recorded reply is the human's OWN answer (vs an untouched auto-generated
|
|
316
388
|
// recommended default). Drives the "User answered" marker on the Answer option.
|
|
317
389
|
function isUserAnswered(item: RequirementReviewItem): boolean {
|
|
@@ -643,249 +715,265 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
643
715
|
</span>
|
|
644
716
|
</div>
|
|
645
717
|
|
|
646
|
-
<!-- findings to react to
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
718
|
+
<!-- findings to react to — ordered so anything still owed a reaction sits at the
|
|
719
|
+
top (see the floating-order section in the script), with the buckets labelled
|
|
720
|
+
once the list spans more than one of them -->
|
|
721
|
+
<div
|
|
722
|
+
v-if="review.items.length"
|
|
723
|
+
class="flex flex-col gap-3"
|
|
724
|
+
@focusin="onFindingFocusIn"
|
|
725
|
+
@focusout="onFindingFocusOut"
|
|
726
|
+
>
|
|
727
|
+
<template v-for="({ item, attention }, index) in orderedFindings" :key="item.id">
|
|
728
|
+
<div v-if="startsAttentionGroup(index)" class="flex items-center gap-2 pt-1">
|
|
729
|
+
<span
|
|
730
|
+
class="text-[11px] font-semibold uppercase tracking-wide"
|
|
731
|
+
:class="ATTENTION_LABEL_COLOR[attention]"
|
|
732
|
+
>
|
|
733
|
+
{{ ATTENTION_LABELS[attention] }}
|
|
734
|
+
</span>
|
|
735
|
+
<span class="h-px flex-1 bg-slate-800" />
|
|
736
|
+
</div>
|
|
737
|
+
<div
|
|
738
|
+
class="rounded-lg border border-slate-800 bg-slate-900/60 p-3"
|
|
739
|
+
:class="{ 'opacity-60': item.status === 'dismissed' }"
|
|
740
|
+
>
|
|
741
|
+
<div class="flex items-start gap-2">
|
|
742
|
+
<UIcon
|
|
743
|
+
:name="CATEGORY_ICON[item.category]"
|
|
744
|
+
class="mt-0.5 h-4 w-4 shrink-0 text-slate-400"
|
|
745
|
+
/>
|
|
746
|
+
<div class="min-w-0 flex-1">
|
|
747
|
+
<div class="flex flex-wrap items-center gap-1.5">
|
|
748
|
+
<span class="text-sm font-medium text-white">{{ item.title }}</span>
|
|
749
|
+
<UBadge size="xs" variant="subtle" :color="SEVERITY_COLOR[item.severity]">
|
|
750
|
+
{{ SEVERITY_LABELS[item.severity] }}
|
|
751
|
+
</UBadge>
|
|
752
|
+
<UBadge size="xs" variant="outline" color="neutral">
|
|
753
|
+
{{ CATEGORY_LABELS[item.category] }}
|
|
754
|
+
</UBadge>
|
|
755
|
+
<!-- Once the automation has pre-answered some findings, flag the ones it
|
|
669
756
|
left open as the genuine business decisions that need the human. -->
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
757
|
+
<UBadge
|
|
758
|
+
v-if="
|
|
759
|
+
hasAutoDefaults && item.status === 'open' && item.autoAnswerable === false
|
|
760
|
+
"
|
|
761
|
+
size="xs"
|
|
762
|
+
variant="subtle"
|
|
763
|
+
color="warning"
|
|
764
|
+
>
|
|
765
|
+
{{ t('requirements.needsYourInput') }}
|
|
766
|
+
</UBadge>
|
|
767
|
+
<UBadge
|
|
768
|
+
size="xs"
|
|
769
|
+
variant="soft"
|
|
770
|
+
:color="STATUS_COLOR[item.status]"
|
|
771
|
+
class="ms-auto"
|
|
772
|
+
>
|
|
773
|
+
{{ STATUS_LABELS[item.status] }}
|
|
774
|
+
</UBadge>
|
|
775
|
+
</div>
|
|
776
|
+
<p class="mt-1 whitespace-pre-line text-sm text-slate-400">
|
|
777
|
+
{{ item.detail }}
|
|
778
|
+
</p>
|
|
692
779
|
|
|
693
|
-
|
|
780
|
+
<!-- recorded answer (only for non-editable findings — for editable
|
|
694
781
|
ones the answer lives in the textarea below, seeded from the reply) -->
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
782
|
+
<div
|
|
783
|
+
v-if="item.reply && item.status !== 'open' && item.status !== 'answered'"
|
|
784
|
+
class="mt-2 rounded-md border-s-2 border-slate-700 bg-slate-950/40 px-3 py-1.5 text-sm text-slate-300"
|
|
785
|
+
>
|
|
786
|
+
<span class="text-[10px] uppercase tracking-wide text-slate-500">
|
|
787
|
+
{{ t('requirements.answerLabel') }}
|
|
788
|
+
</span>
|
|
789
|
+
<p class="whitespace-pre-line">{{ item.reply }}</p>
|
|
790
|
+
</div>
|
|
704
791
|
|
|
705
|
-
|
|
792
|
+
<!-- per-finding 3-way selector: Answer (write it) / Dismiss (irrelevant) /
|
|
706
793
|
Recommend (let the Requirement Writer suggest one). The active mode
|
|
707
794
|
drives the content below, IN PLACE — no separate section. Disabled once
|
|
708
795
|
the requirements are settled / a cycle is running; hidden for a
|
|
709
796
|
`resolved` finding (its recorded answer shows above). -->
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
size="xs"
|
|
718
|
-
:icon="opt.icon"
|
|
719
|
-
:disabled="frozen"
|
|
720
|
-
@click="setMode(item, opt.mode)"
|
|
721
|
-
>
|
|
722
|
-
{{ t(opt.labelKey) }}
|
|
723
|
-
<UIcon
|
|
724
|
-
v-if="opt.mode === 'answer' && isUserAnswered(item)"
|
|
725
|
-
name="i-lucide-check"
|
|
726
|
-
class="h-3.5 w-3.5 text-emerald-400"
|
|
727
|
-
/>
|
|
728
|
-
</UButton>
|
|
729
|
-
</div>
|
|
730
|
-
|
|
731
|
-
<!-- ANSWER: type the answer directly (auto-saves on blur) -->
|
|
732
|
-
<template v-if="modeFor(item) === 'answer'">
|
|
733
|
-
<!-- Auto-generated recommended default: the automation pre-filled this
|
|
734
|
-
answer; the human can keep it, edit it, or switch modes. -->
|
|
735
|
-
<div
|
|
736
|
-
v-if="autoDefaults.get(item.id)"
|
|
737
|
-
class="mt-2 flex flex-wrap items-center gap-1.5 text-xs text-indigo-300"
|
|
738
|
-
>
|
|
739
|
-
<UIcon name="i-lucide-sparkles" class="h-3.5 w-3.5 shrink-0" />
|
|
740
|
-
<span>{{ t('requirements.recommendedDefault') }}</span>
|
|
741
|
-
<UBadge
|
|
742
|
-
v-if="autoDefaults.get(item.id)!.groundedInFragment"
|
|
797
|
+
<template v-if="item.status !== 'resolved'">
|
|
798
|
+
<div class="mt-2 flex flex-wrap items-center gap-1">
|
|
799
|
+
<UButton
|
|
800
|
+
v-for="opt in FINDING_MODES"
|
|
801
|
+
:key="opt.mode"
|
|
802
|
+
:color="modeFor(item) === opt.mode ? 'primary' : 'neutral'"
|
|
803
|
+
:variant="modeFor(item) === opt.mode ? 'soft' : 'ghost'"
|
|
743
804
|
size="xs"
|
|
744
|
-
|
|
745
|
-
|
|
805
|
+
:icon="opt.icon"
|
|
806
|
+
:disabled="frozen"
|
|
807
|
+
@click="setMode(item, opt.mode)"
|
|
746
808
|
>
|
|
747
|
-
{{
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
809
|
+
{{ t(opt.labelKey) }}
|
|
810
|
+
<UIcon
|
|
811
|
+
v-if="opt.mode === 'answer' && isUserAnswered(item)"
|
|
812
|
+
name="i-lucide-check"
|
|
813
|
+
class="h-3.5 w-3.5 text-emerald-400"
|
|
814
|
+
/>
|
|
815
|
+
</UButton>
|
|
753
816
|
</div>
|
|
754
|
-
<UTextarea
|
|
755
|
-
v-model="drafts[item.id]"
|
|
756
|
-
:rows="2"
|
|
757
|
-
autoresize
|
|
758
|
-
size="sm"
|
|
759
|
-
class="mt-2 w-full"
|
|
760
|
-
:placeholder="t('requirements.answerPlaceholder')"
|
|
761
|
-
:disabled="frozen"
|
|
762
|
-
@blur="persistDraft(item)"
|
|
763
|
-
/>
|
|
764
|
-
<p
|
|
765
|
-
v-if="isUserAnswered(item)"
|
|
766
|
-
class="mt-1 flex items-center gap-1 text-[11px] text-emerald-400"
|
|
767
|
-
>
|
|
768
|
-
<UIcon name="i-lucide-check" class="h-3 w-3 shrink-0" />
|
|
769
|
-
{{ t('requirements.userAnswered') }}
|
|
770
|
-
</p>
|
|
771
|
-
</template>
|
|
772
817
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
v-if="pendingRecFor(item)"
|
|
778
|
-
class="mt-2 flex items-center gap-1.5 text-xs text-indigo-300"
|
|
779
|
-
>
|
|
780
|
-
<UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin" />
|
|
781
|
-
{{ t('requirements.generatingSuggestion') }}
|
|
782
|
-
</div>
|
|
783
|
-
<template v-else-if="readyRecFor(item)">
|
|
818
|
+
<!-- ANSWER: type the answer directly (auto-saves on blur) -->
|
|
819
|
+
<template v-if="modeFor(item) === 'answer'">
|
|
820
|
+
<!-- Auto-generated recommended default: the automation pre-filled this
|
|
821
|
+
answer; the human can keep it, edit it, or switch modes. -->
|
|
784
822
|
<div
|
|
785
|
-
v-
|
|
786
|
-
|
|
787
|
-
class="mt-2 rounded-lg border border-indigo-900/50 bg-indigo-950/20 p-3"
|
|
823
|
+
v-if="autoDefaults.get(item.id)"
|
|
824
|
+
class="mt-2 flex flex-wrap items-center gap-1.5 text-xs text-indigo-300"
|
|
788
825
|
>
|
|
826
|
+
<UIcon name="i-lucide-sparkles" class="h-3.5 w-3.5 shrink-0" />
|
|
827
|
+
<span>{{ t('requirements.recommendedDefault') }}</span>
|
|
789
828
|
<UBadge
|
|
790
|
-
v-if="
|
|
829
|
+
v-if="autoDefaults.get(item.id)!.groundedInFragment"
|
|
791
830
|
size="xs"
|
|
792
831
|
variant="subtle"
|
|
793
|
-
color="
|
|
794
|
-
icon="i-lucide-badge-check"
|
|
832
|
+
color="primary"
|
|
795
833
|
>
|
|
796
834
|
{{
|
|
797
835
|
t('requirements.currentStandard', {
|
|
798
|
-
title:
|
|
836
|
+
title: autoDefaults.get(item.id)!.groundedInFragment!.title,
|
|
799
837
|
})
|
|
800
838
|
}}
|
|
801
839
|
</UBadge>
|
|
802
|
-
<p class="mt-1 whitespace-pre-line text-sm text-slate-300">
|
|
803
|
-
{{ rec.recommendedText }}
|
|
804
|
-
</p>
|
|
805
|
-
<div class="mt-2 flex flex-wrap items-center gap-2">
|
|
806
|
-
<UButton
|
|
807
|
-
color="primary"
|
|
808
|
-
variant="soft"
|
|
809
|
-
size="xs"
|
|
810
|
-
icon="i-lucide-check"
|
|
811
|
-
:disabled="frozen || !access.canExecuteRuns.value"
|
|
812
|
-
:title="
|
|
813
|
-
access.canExecuteRuns.value ? undefined : t('access.noRunExecute')
|
|
814
|
-
"
|
|
815
|
-
@click="acceptRecommendation(rec)"
|
|
816
|
-
>
|
|
817
|
-
{{ t('requirements.accept') }}
|
|
818
|
-
</UButton>
|
|
819
|
-
<UButton
|
|
820
|
-
color="neutral"
|
|
821
|
-
variant="ghost"
|
|
822
|
-
size="xs"
|
|
823
|
-
icon="i-lucide-x"
|
|
824
|
-
:disabled="frozen || !access.canExecuteRuns.value"
|
|
825
|
-
:title="
|
|
826
|
-
access.canExecuteRuns.value ? undefined : t('access.noRunExecute')
|
|
827
|
-
"
|
|
828
|
-
@click="rejectRecommendation(rec)"
|
|
829
|
-
>
|
|
830
|
-
{{ t('requirements.reject') }}
|
|
831
|
-
</UButton>
|
|
832
|
-
</div>
|
|
833
|
-
<div class="mt-2 flex items-start gap-2">
|
|
834
|
-
<UTextarea
|
|
835
|
-
v-model="reRequestNotes[rec.id]"
|
|
836
|
-
:rows="1"
|
|
837
|
-
autoresize
|
|
838
|
-
size="sm"
|
|
839
|
-
class="flex-1"
|
|
840
|
-
:placeholder="t('requirements.reRequestPlaceholder')"
|
|
841
|
-
:disabled="frozen || recommending"
|
|
842
|
-
/>
|
|
843
|
-
<UButton
|
|
844
|
-
color="neutral"
|
|
845
|
-
variant="soft"
|
|
846
|
-
size="xs"
|
|
847
|
-
icon="i-lucide-rotate-cw"
|
|
848
|
-
:loading="recommending"
|
|
849
|
-
:disabled="
|
|
850
|
-
!(reRequestNotes[rec.id] ?? '').trim() ||
|
|
851
|
-
frozen ||
|
|
852
|
-
!access.canExecuteRuns.value
|
|
853
|
-
"
|
|
854
|
-
:title="
|
|
855
|
-
access.canExecuteRuns.value ? undefined : t('access.noRunExecute')
|
|
856
|
-
"
|
|
857
|
-
@click="reRequestRecommendation(rec)"
|
|
858
|
-
>
|
|
859
|
-
{{ t('requirements.reRequest') }}
|
|
860
|
-
</UButton>
|
|
861
|
-
</div>
|
|
862
840
|
</div>
|
|
863
|
-
</template>
|
|
864
|
-
<template v-else>
|
|
865
841
|
<UTextarea
|
|
866
|
-
v-model="
|
|
842
|
+
v-model="drafts[item.id]"
|
|
867
843
|
:rows="2"
|
|
868
844
|
autoresize
|
|
869
845
|
size="sm"
|
|
870
846
|
class="mt-2 w-full"
|
|
871
|
-
:placeholder="t('requirements.
|
|
847
|
+
:placeholder="t('requirements.answerPlaceholder')"
|
|
872
848
|
:disabled="frozen"
|
|
849
|
+
@blur="persistDraft(item)"
|
|
873
850
|
/>
|
|
874
|
-
<p
|
|
875
|
-
|
|
876
|
-
|
|
851
|
+
<p
|
|
852
|
+
v-if="isUserAnswered(item)"
|
|
853
|
+
class="mt-1 flex items-center gap-1 text-[11px] text-emerald-400"
|
|
854
|
+
>
|
|
855
|
+
<UIcon name="i-lucide-check" class="h-3 w-3 shrink-0" />
|
|
856
|
+
{{ t('requirements.userAnswered') }}
|
|
877
857
|
</p>
|
|
878
858
|
</template>
|
|
879
|
-
</template>
|
|
880
859
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
860
|
+
<!-- RECOMMEND: generating / the ready suggestion / a guidance box, all
|
|
861
|
+
rendered inline where the question was asked. -->
|
|
862
|
+
<template v-else-if="modeFor(item) === 'recommend'">
|
|
863
|
+
<div
|
|
864
|
+
v-if="pendingRecFor(item)"
|
|
865
|
+
class="mt-2 flex items-center gap-1.5 text-xs text-indigo-300"
|
|
866
|
+
>
|
|
867
|
+
<UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin" />
|
|
868
|
+
{{ t('requirements.generatingSuggestion') }}
|
|
869
|
+
</div>
|
|
870
|
+
<template v-else-if="readyRecFor(item)">
|
|
871
|
+
<div
|
|
872
|
+
v-for="rec in [readyRecFor(item)!]"
|
|
873
|
+
:key="rec.id"
|
|
874
|
+
class="mt-2 rounded-lg border border-indigo-900/50 bg-indigo-950/20 p-3"
|
|
875
|
+
>
|
|
876
|
+
<UBadge
|
|
877
|
+
v-if="rec.groundedInFragment"
|
|
878
|
+
size="xs"
|
|
879
|
+
variant="subtle"
|
|
880
|
+
color="success"
|
|
881
|
+
icon="i-lucide-badge-check"
|
|
882
|
+
>
|
|
883
|
+
{{
|
|
884
|
+
t('requirements.currentStandard', {
|
|
885
|
+
title: rec.groundedInFragment.title,
|
|
886
|
+
})
|
|
887
|
+
}}
|
|
888
|
+
</UBadge>
|
|
889
|
+
<p class="mt-1 whitespace-pre-line text-sm text-slate-300">
|
|
890
|
+
{{ rec.recommendedText }}
|
|
891
|
+
</p>
|
|
892
|
+
<div class="mt-2 flex flex-wrap items-center gap-2">
|
|
893
|
+
<UButton
|
|
894
|
+
color="primary"
|
|
895
|
+
variant="soft"
|
|
896
|
+
size="xs"
|
|
897
|
+
icon="i-lucide-check"
|
|
898
|
+
:disabled="frozen || !access.canExecuteRuns.value"
|
|
899
|
+
:title="
|
|
900
|
+
access.canExecuteRuns.value ? undefined : t('access.noRunExecute')
|
|
901
|
+
"
|
|
902
|
+
@click="acceptRecommendation(rec)"
|
|
903
|
+
>
|
|
904
|
+
{{ t('requirements.accept') }}
|
|
905
|
+
</UButton>
|
|
906
|
+
<UButton
|
|
907
|
+
color="neutral"
|
|
908
|
+
variant="ghost"
|
|
909
|
+
size="xs"
|
|
910
|
+
icon="i-lucide-x"
|
|
911
|
+
:disabled="frozen || !access.canExecuteRuns.value"
|
|
912
|
+
:title="
|
|
913
|
+
access.canExecuteRuns.value ? undefined : t('access.noRunExecute')
|
|
914
|
+
"
|
|
915
|
+
@click="rejectRecommendation(rec)"
|
|
916
|
+
>
|
|
917
|
+
{{ t('requirements.reject') }}
|
|
918
|
+
</UButton>
|
|
919
|
+
</div>
|
|
920
|
+
<div class="mt-2 flex items-start gap-2">
|
|
921
|
+
<UTextarea
|
|
922
|
+
v-model="reRequestNotes[rec.id]"
|
|
923
|
+
:rows="1"
|
|
924
|
+
autoresize
|
|
925
|
+
size="sm"
|
|
926
|
+
class="flex-1"
|
|
927
|
+
:placeholder="t('requirements.reRequestPlaceholder')"
|
|
928
|
+
:disabled="frozen || recommending"
|
|
929
|
+
/>
|
|
930
|
+
<UButton
|
|
931
|
+
color="neutral"
|
|
932
|
+
variant="soft"
|
|
933
|
+
size="xs"
|
|
934
|
+
icon="i-lucide-rotate-cw"
|
|
935
|
+
:loading="recommending"
|
|
936
|
+
:disabled="
|
|
937
|
+
!(reRequestNotes[rec.id] ?? '').trim() ||
|
|
938
|
+
frozen ||
|
|
939
|
+
!access.canExecuteRuns.value
|
|
940
|
+
"
|
|
941
|
+
:title="
|
|
942
|
+
access.canExecuteRuns.value ? undefined : t('access.noRunExecute')
|
|
943
|
+
"
|
|
944
|
+
@click="reRequestRecommendation(rec)"
|
|
945
|
+
>
|
|
946
|
+
{{ t('requirements.reRequest') }}
|
|
947
|
+
</UButton>
|
|
948
|
+
</div>
|
|
949
|
+
</div>
|
|
950
|
+
</template>
|
|
951
|
+
<template v-else>
|
|
952
|
+
<UTextarea
|
|
953
|
+
v-model="guidanceDrafts[item.id]"
|
|
954
|
+
:rows="2"
|
|
955
|
+
autoresize
|
|
956
|
+
size="sm"
|
|
957
|
+
class="mt-2 w-full"
|
|
958
|
+
:placeholder="t('requirements.guidancePlaceholder')"
|
|
959
|
+
:disabled="frozen"
|
|
960
|
+
/>
|
|
961
|
+
<p class="mt-1 flex items-center gap-1 text-[11px] text-indigo-300/80">
|
|
962
|
+
<UIcon name="i-lucide-wand-2" class="h-3 w-3 shrink-0" />
|
|
963
|
+
{{ t('requirements.guidanceHint') }}
|
|
964
|
+
</p>
|
|
965
|
+
</template>
|
|
966
|
+
</template>
|
|
967
|
+
|
|
968
|
+
<!-- DISMISS: nothing to fill in — a short note explains the effect -->
|
|
969
|
+
<p v-else class="mt-2 text-[11px] text-slate-500">
|
|
970
|
+
{{ t('requirements.dismissedHint') }}
|
|
971
|
+
</p>
|
|
972
|
+
</template>
|
|
973
|
+
</div>
|
|
886
974
|
</div>
|
|
887
975
|
</div>
|
|
888
|
-
</
|
|
976
|
+
</template>
|
|
889
977
|
</div>
|
|
890
978
|
|
|
891
979
|
<!-- incorporated document: the standard-format requirements. The whole section
|