@cat-factory/app 1.0.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/LICENSE +21 -0
- package/README.md +88 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +11 -0
- package/app/assets/css/main.css +100 -0
- package/app/components/auth/AuthGate.vue +24 -0
- package/app/components/auth/LoginScreen.vue +18 -0
- package/app/components/auth/UserMenu.vue +39 -0
- package/app/components/board/AgentFailureCard.vue +97 -0
- package/app/components/board/AgentStopButton.vue +61 -0
- package/app/components/board/BoardCanvas.vue +146 -0
- package/app/components/board/TaskDependencyEdges.vue +132 -0
- package/app/components/board/nodes/AgentChip.vue +59 -0
- package/app/components/board/nodes/BlockNode.vue +347 -0
- package/app/components/board/nodes/DecisionBadge.vue +21 -0
- package/app/components/board/nodes/DraggableTask.vue +69 -0
- package/app/components/board/nodes/ModuleFrame.vue +70 -0
- package/app/components/board/nodes/TaskCard.vue +237 -0
- package/app/components/bootstrap/BootstrapModal.vue +665 -0
- package/app/components/documents/DocumentImportModal.vue +161 -0
- package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
- package/app/components/documents/SpawnPreviewModal.vue +161 -0
- package/app/components/documents/TaskContextDocs.vue +83 -0
- package/app/components/focus/BlockFocusView.vue +161 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
- package/app/components/github/GitHubConnect.vue +183 -0
- package/app/components/github/GitHubPanel.vue +584 -0
- package/app/components/layout/BoardSwitcher.vue +202 -0
- package/app/components/layout/BoardToolbar.vue +109 -0
- package/app/components/layout/SideBar.vue +193 -0
- package/app/components/layout/SpendWarningBanner.vue +107 -0
- package/app/components/palettes/AgentPalette.vue +33 -0
- package/app/components/palettes/BlockPalette.vue +41 -0
- package/app/components/palettes/PipelinePalette.vue +74 -0
- package/app/components/panels/DecisionModal.vue +71 -0
- package/app/components/panels/InspectorPanel.vue +296 -0
- package/app/components/panels/inspector/ContainerSummary.vue +74 -0
- package/app/components/panels/inspector/TaskDependencies.vue +70 -0
- package/app/components/panels/inspector/TaskExecution.vue +175 -0
- package/app/components/panels/inspector/TaskModelSettings.vue +128 -0
- package/app/components/panels/inspector/TaskStructure.vue +139 -0
- package/app/components/pipeline/PipelineBuilder.vue +227 -0
- package/app/components/pipeline/PipelineProgress.vue +246 -0
- package/app/components/requirements/RequirementReviewModal.vue +328 -0
- package/app/components/scenarios/FeatureScenarios.vue +162 -0
- package/app/components/scenarios/ScenarioCard.vue +109 -0
- package/app/components/tasks/TaskContextIssues.vue +88 -0
- package/app/components/tasks/TaskImportModal.vue +140 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +122 -0
- package/app/composables/useApi.ts +535 -0
- package/app/composables/useBlockDrag.ts +75 -0
- package/app/composables/useBlockQueries.ts +136 -0
- package/app/composables/useBoardFlow.ts +11 -0
- package/app/composables/useDepLabels.ts +26 -0
- package/app/composables/useSemanticZoom.ts +16 -0
- package/app/composables/useWorkspaceStream.ts +125 -0
- package/app/docs/architecture.md +31 -0
- package/app/pages/index.vue +80 -0
- package/app/stores/accounts.ts +64 -0
- package/app/stores/agentRuns.ts +117 -0
- package/app/stores/agents.ts +40 -0
- package/app/stores/auth.ts +97 -0
- package/app/stores/board.spec.ts +197 -0
- package/app/stores/board.ts +147 -0
- package/app/stores/bootstrap.ts +97 -0
- package/app/stores/documents.ts +165 -0
- package/app/stores/execution.ts +115 -0
- package/app/stores/fragmentLibrary.ts +147 -0
- package/app/stores/fragments.ts +40 -0
- package/app/stores/github.ts +291 -0
- package/app/stores/models.ts +48 -0
- package/app/stores/pipelines.ts +77 -0
- package/app/stores/requirements.ts +133 -0
- package/app/stores/scenarios.spec.ts +82 -0
- package/app/stores/scenarios.ts +196 -0
- package/app/stores/tasks.spec.ts +71 -0
- package/app/stores/tasks.ts +149 -0
- package/app/stores/ui.ts +204 -0
- package/app/stores/workspace.ts +201 -0
- package/app/types/accounts.ts +38 -0
- package/app/types/bootstrap.ts +83 -0
- package/app/types/documents.ts +92 -0
- package/app/types/domain.ts +216 -0
- package/app/types/execution.ts +110 -0
- package/app/types/fragments.ts +72 -0
- package/app/types/github.ts +153 -0
- package/app/types/models.ts +48 -0
- package/app/types/requirements.ts +38 -0
- package/app/types/scenarios.ts +36 -0
- package/app/types/tasks.ts +67 -0
- package/app/utils/catalog.spec.ts +82 -0
- package/app/utils/catalog.ts +185 -0
- package/app/utils/dnd.ts +29 -0
- package/nuxt.config.ts +43 -0
- package/package.json +43 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { AgentState, ExecutionInstance } from '~/types/domain'
|
|
3
|
+
import { AGENT_BY_KIND } from '~/utils/catalog'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{ instance: ExecutionInstance }>()
|
|
6
|
+
const emit = defineEmits<{ openDecision: [decisionId: string] }>()
|
|
7
|
+
|
|
8
|
+
const models = useModelsStore()
|
|
9
|
+
|
|
10
|
+
/** Visual language for an individual agent's runtime state. */
|
|
11
|
+
const STATE_META: Record<AgentState, { label: string; color: string; icon: string }> = {
|
|
12
|
+
pending: { label: 'Pending', color: '#64748b', icon: 'i-lucide-circle-dashed' },
|
|
13
|
+
working: { label: 'Working', color: '#6366f1', icon: 'i-lucide-loader' },
|
|
14
|
+
waiting_decision: { label: 'Needs decision', color: '#f59e0b', icon: 'i-lucide-circle-help' },
|
|
15
|
+
done: { label: 'Done', color: '#22c55e', icon: 'i-lucide-circle-check' },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Visual language for the pipeline instance as a whole. */
|
|
19
|
+
const STATUS_META: Record<ExecutionInstance['status'], { label: string; chip: string }> = {
|
|
20
|
+
running: { label: 'Running', chip: 'primary' },
|
|
21
|
+
blocked: { label: 'Blocked on decision', chip: 'warning' },
|
|
22
|
+
paused: { label: 'Paused (budget)', chip: 'neutral' },
|
|
23
|
+
done: { label: 'Completed', chip: 'success' },
|
|
24
|
+
failed: { label: 'Failed', chip: 'error' },
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const steps = computed(() => props.instance.steps)
|
|
28
|
+
const total = computed(() => steps.value.length)
|
|
29
|
+
|
|
30
|
+
/** A step counts as fully complete only once its state is `done`. */
|
|
31
|
+
const completedCount = computed(() => steps.value.filter((s) => s.state === 'done').length)
|
|
32
|
+
|
|
33
|
+
/** Effective 0..1 progress per step (a done step is always 100%). */
|
|
34
|
+
function stepProgress(state: AgentState, progress: number) {
|
|
35
|
+
return state === 'done' ? 1 : progress
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Overall pipeline progress: the mean of every step's effective progress. */
|
|
39
|
+
const overallProgress = computed(() => {
|
|
40
|
+
if (!total.value) return 0
|
|
41
|
+
const sum = steps.value.reduce((acc, s) => acc + stepProgress(s.state, s.progress), 0)
|
|
42
|
+
return sum / total.value
|
|
43
|
+
})
|
|
44
|
+
const overallPct = computed(() => Math.round(overallProgress.value * 100))
|
|
45
|
+
|
|
46
|
+
const statusMeta = computed(() => STATUS_META[props.instance.status])
|
|
47
|
+
|
|
48
|
+
/** The agent the pipeline is currently centred on (for the summary line). */
|
|
49
|
+
const currentAgent = computed(() => {
|
|
50
|
+
const s = steps.value[props.instance.currentStep]
|
|
51
|
+
return s ? AGENT_BY_KIND[s.agentKind].label : null
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
/** Connector below a step is "lit" once that step has completed. */
|
|
55
|
+
function connectorDone(index: number) {
|
|
56
|
+
return steps.value[index]?.state === 'done'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const legend: { state: AgentState }[] = [
|
|
60
|
+
{ state: 'done' },
|
|
61
|
+
{ state: 'working' },
|
|
62
|
+
{ state: 'waiting_decision' },
|
|
63
|
+
{ state: 'pending' },
|
|
64
|
+
]
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<template>
|
|
68
|
+
<div class="flex flex-col gap-5">
|
|
69
|
+
<!-- summary -->
|
|
70
|
+
<div class="rounded-xl border border-slate-800 bg-slate-900/60 p-4">
|
|
71
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
72
|
+
<UBadge :color="statusMeta.chip as any" variant="subtle">{{ statusMeta.label }}</UBadge>
|
|
73
|
+
<span class="text-sm text-slate-300">
|
|
74
|
+
<span class="font-semibold text-white">{{ completedCount }}</span>
|
|
75
|
+
/ {{ total }} agents complete
|
|
76
|
+
</span>
|
|
77
|
+
<span v-if="currentAgent && instance.status === 'running'" class="text-xs text-slate-500">
|
|
78
|
+
· currently {{ currentAgent }}
|
|
79
|
+
</span>
|
|
80
|
+
<span class="ml-auto font-mono text-sm tabular-nums text-slate-200">{{ overallPct }}%</span>
|
|
81
|
+
</div>
|
|
82
|
+
<UProgress :model-value="overallPct" class="mt-3" />
|
|
83
|
+
|
|
84
|
+
<!-- legend -->
|
|
85
|
+
<div class="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1">
|
|
86
|
+
<span
|
|
87
|
+
v-for="l in legend"
|
|
88
|
+
:key="l.state"
|
|
89
|
+
class="inline-flex items-center gap-1.5 text-[11px] text-slate-400"
|
|
90
|
+
>
|
|
91
|
+
<span
|
|
92
|
+
class="h-2 w-2 rounded-full"
|
|
93
|
+
:style="{ backgroundColor: STATE_META[l.state].color }"
|
|
94
|
+
/>
|
|
95
|
+
{{ STATE_META[l.state].label }}
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<!-- agent chain as a vertical timeline -->
|
|
101
|
+
<ol class="flex flex-col">
|
|
102
|
+
<li v-for="(s, i) in steps" :key="i" class="relative flex gap-4 pb-5 last:pb-0">
|
|
103
|
+
<!-- connector line to the next step -->
|
|
104
|
+
<span
|
|
105
|
+
v-if="i < steps.length - 1"
|
|
106
|
+
class="absolute top-9 bottom-0 left-[17px] w-0.5 -translate-x-1/2"
|
|
107
|
+
:class="connectorDone(i) ? 'bg-emerald-500/60' : 'bg-slate-700'"
|
|
108
|
+
/>
|
|
109
|
+
|
|
110
|
+
<!-- rail node -->
|
|
111
|
+
<span
|
|
112
|
+
class="relative z-10 flex h-9 w-9 shrink-0 items-center justify-center rounded-full border-2 bg-slate-950"
|
|
113
|
+
:class="s.state === 'working' ? 'step-active' : ''"
|
|
114
|
+
:style="{ borderColor: STATE_META[s.state].color }"
|
|
115
|
+
>
|
|
116
|
+
<UIcon
|
|
117
|
+
:name="STATE_META[s.state].icon"
|
|
118
|
+
class="h-4 w-4"
|
|
119
|
+
:class="s.state === 'working' ? 'animate-spin' : ''"
|
|
120
|
+
:style="{ color: STATE_META[s.state].color }"
|
|
121
|
+
/>
|
|
122
|
+
</span>
|
|
123
|
+
|
|
124
|
+
<!-- step content card -->
|
|
125
|
+
<div
|
|
126
|
+
class="flex-1 rounded-xl border p-4 transition"
|
|
127
|
+
:class="[
|
|
128
|
+
i === instance.currentStep && instance.status !== 'done'
|
|
129
|
+
? 'border-indigo-500/70 bg-slate-900 shadow-lg shadow-indigo-500/10'
|
|
130
|
+
: 'border-slate-800 bg-slate-900/50',
|
|
131
|
+
s.state === 'pending' ? 'opacity-60' : '',
|
|
132
|
+
]"
|
|
133
|
+
>
|
|
134
|
+
<div class="flex items-center gap-2">
|
|
135
|
+
<div
|
|
136
|
+
class="flex h-8 w-8 items-center justify-center rounded-lg"
|
|
137
|
+
:style="{ backgroundColor: AGENT_BY_KIND[s.agentKind].color + '22' }"
|
|
138
|
+
>
|
|
139
|
+
<UIcon
|
|
140
|
+
:name="AGENT_BY_KIND[s.agentKind].icon"
|
|
141
|
+
class="h-4 w-4"
|
|
142
|
+
:style="{ color: AGENT_BY_KIND[s.agentKind].color }"
|
|
143
|
+
/>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="min-w-0">
|
|
146
|
+
<div class="truncate text-sm font-semibold text-white">
|
|
147
|
+
{{ AGENT_BY_KIND[s.agentKind].label }}
|
|
148
|
+
</div>
|
|
149
|
+
<div class="text-[10px] uppercase tracking-wide text-slate-500">
|
|
150
|
+
Step {{ i + 1 }} of {{ total }}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
<span
|
|
154
|
+
class="ml-auto shrink-0 text-[11px] font-medium"
|
|
155
|
+
:style="{ color: STATE_META[s.state].color }"
|
|
156
|
+
>
|
|
157
|
+
{{ STATE_META[s.state].label }}
|
|
158
|
+
</span>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<!-- per-step progress (only while it has meaningful progress) -->
|
|
162
|
+
<UProgress
|
|
163
|
+
v-if="s.state === 'working' || s.state === 'done'"
|
|
164
|
+
:model-value="Math.round(stepProgress(s.state, s.progress) * 100)"
|
|
165
|
+
size="xs"
|
|
166
|
+
class="mt-3"
|
|
167
|
+
/>
|
|
168
|
+
|
|
169
|
+
<!-- live subtask counts from the agent's todo list -->
|
|
170
|
+
<div v-if="s.subtasks && s.subtasks.total > 0" class="mt-2">
|
|
171
|
+
<div class="flex items-center justify-between text-[10px] text-slate-400">
|
|
172
|
+
<span>
|
|
173
|
+
{{ s.subtasks.completed }}/{{ s.subtasks.total }} subtasks
|
|
174
|
+
<span v-if="s.subtasks.inProgress > 0" class="text-indigo-300">
|
|
175
|
+
· {{ s.subtasks.inProgress }} in progress
|
|
176
|
+
</span>
|
|
177
|
+
</span>
|
|
178
|
+
</div>
|
|
179
|
+
<div class="mt-1 h-1 overflow-hidden rounded-full bg-slate-700/60">
|
|
180
|
+
<div
|
|
181
|
+
class="h-full rounded-full bg-indigo-400 transition-all duration-500"
|
|
182
|
+
:style="{ width: `${(s.subtasks.completed / s.subtasks.total) * 100}%` }"
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<!-- model used for this step -->
|
|
188
|
+
<p
|
|
189
|
+
v-if="s.model"
|
|
190
|
+
class="mt-2 flex items-center gap-1 truncate text-[10px] text-slate-500"
|
|
191
|
+
:title="s.model"
|
|
192
|
+
>
|
|
193
|
+
<UIcon name="i-lucide-cpu" class="h-3 w-3 shrink-0" />
|
|
194
|
+
{{ models.labelForRef(s.model) }}
|
|
195
|
+
</p>
|
|
196
|
+
|
|
197
|
+
<!-- output the agent produced -->
|
|
198
|
+
<p
|
|
199
|
+
v-if="s.output"
|
|
200
|
+
class="mt-2 line-clamp-3 rounded-md bg-slate-950/60 px-2 py-1.5 text-[11px] text-slate-300"
|
|
201
|
+
:title="s.output"
|
|
202
|
+
>
|
|
203
|
+
{{ s.output }}
|
|
204
|
+
</p>
|
|
205
|
+
|
|
206
|
+
<!-- decision: unresolved => prompt, resolved => show the choice -->
|
|
207
|
+
<div v-if="s.decision && !s.decision.chosen" class="mt-3">
|
|
208
|
+
<UButton
|
|
209
|
+
color="warning"
|
|
210
|
+
variant="soft"
|
|
211
|
+
size="xs"
|
|
212
|
+
icon="i-lucide-circle-help"
|
|
213
|
+
@click="emit('openDecision', s.decision.id)"
|
|
214
|
+
>
|
|
215
|
+
Resolve: {{ s.decision.question }}
|
|
216
|
+
</UButton>
|
|
217
|
+
</div>
|
|
218
|
+
<p
|
|
219
|
+
v-else-if="s.decision?.chosen"
|
|
220
|
+
class="mt-2 flex items-center gap-1 truncate text-[11px] text-emerald-400"
|
|
221
|
+
:title="s.decision.chosen"
|
|
222
|
+
>
|
|
223
|
+
<UIcon name="i-lucide-check" class="h-3 w-3 shrink-0" />
|
|
224
|
+
{{ s.decision.chosen }}
|
|
225
|
+
</p>
|
|
226
|
+
</div>
|
|
227
|
+
</li>
|
|
228
|
+
</ol>
|
|
229
|
+
</div>
|
|
230
|
+
</template>
|
|
231
|
+
|
|
232
|
+
<style scoped>
|
|
233
|
+
/* Soft indigo halo around the rail node of the actively-working step. */
|
|
234
|
+
@keyframes step-pulse {
|
|
235
|
+
0%,
|
|
236
|
+
100% {
|
|
237
|
+
box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.5);
|
|
238
|
+
}
|
|
239
|
+
50% {
|
|
240
|
+
box-shadow: 0 0 0 6px rgba(99, 102, 241, 0);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
.step-active {
|
|
244
|
+
animation: step-pulse 1.6s ease-in-out infinite;
|
|
245
|
+
}
|
|
246
|
+
</style>
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Requirements-review panel: surfaces the stateless reviewer agent's questions /
|
|
3
|
+
// gaps / clarifications about a block's collected requirements in a form a human
|
|
4
|
+
// can work through — answer or dismiss each item, then incorporate the answers
|
|
5
|
+
// back into the block's requirements. Triggered from the inspector; the block id
|
|
6
|
+
// to review lives on the ui store.
|
|
7
|
+
import type {
|
|
8
|
+
RequirementReview,
|
|
9
|
+
RequirementReviewItem,
|
|
10
|
+
ReviewItemCategory,
|
|
11
|
+
ReviewItemSeverity,
|
|
12
|
+
ReviewItemStatus,
|
|
13
|
+
} from '~/types/requirements'
|
|
14
|
+
|
|
15
|
+
const ui = useUiStore()
|
|
16
|
+
const board = useBoardStore()
|
|
17
|
+
const requirements = useRequirementsStore()
|
|
18
|
+
const toast = useToast()
|
|
19
|
+
|
|
20
|
+
const open = computed({
|
|
21
|
+
get: () => ui.requirementReviewBlockId !== null,
|
|
22
|
+
set: (v: boolean) => {
|
|
23
|
+
if (!v) ui.closeRequirementReview()
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const blockId = computed(() => ui.requirementReviewBlockId)
|
|
28
|
+
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
29
|
+
const review = computed<RequirementReview | null>(() =>
|
|
30
|
+
blockId.value ? requirements.reviewFor(blockId.value) : null,
|
|
31
|
+
)
|
|
32
|
+
const busy = computed(() => (blockId.value ? requirements.isReviewing(blockId.value) : false))
|
|
33
|
+
const incorporating = computed(() =>
|
|
34
|
+
review.value ? requirements.isIncorporating(review.value.id) : false,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
// Draft replies, keyed by item id, so editing one item doesn't disturb others.
|
|
38
|
+
const drafts = ref<Record<string, string>>({})
|
|
39
|
+
|
|
40
|
+
// Load the current review whenever the panel opens for a block.
|
|
41
|
+
watch(blockId, (id) => {
|
|
42
|
+
drafts.value = {}
|
|
43
|
+
if (id) void requirements.load(id)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const sortedItems = computed<RequirementReviewItem[]>(() => {
|
|
47
|
+
if (!review.value) return []
|
|
48
|
+
const rank: Record<ReviewItemSeverity, number> = { high: 0, medium: 1, low: 2 }
|
|
49
|
+
return [...review.value.items].sort((a, b) => rank[a.severity] - rank[b.severity])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const openCount = computed(() => (review.value ? requirements.openCount(review.value) : 0))
|
|
53
|
+
const canIncorporate = computed(() => !!review.value && requirements.allSettled(review.value))
|
|
54
|
+
|
|
55
|
+
const SEVERITY_COLOR: Record<ReviewItemSeverity, string> = {
|
|
56
|
+
high: 'error',
|
|
57
|
+
medium: 'warning',
|
|
58
|
+
low: 'neutral',
|
|
59
|
+
}
|
|
60
|
+
const CATEGORY_ICON: Record<ReviewItemCategory, string> = {
|
|
61
|
+
gap: 'i-lucide-puzzle',
|
|
62
|
+
clarification: 'i-lucide-help-circle',
|
|
63
|
+
assumption: 'i-lucide-lightbulb',
|
|
64
|
+
risk: 'i-lucide-shield-alert',
|
|
65
|
+
question: 'i-lucide-message-circle-question',
|
|
66
|
+
}
|
|
67
|
+
const STATUS_COLOR: Record<ReviewItemStatus, string> = {
|
|
68
|
+
open: 'warning',
|
|
69
|
+
answered: 'info',
|
|
70
|
+
resolved: 'success',
|
|
71
|
+
dismissed: 'neutral',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function notifyError(title: string, e: unknown) {
|
|
75
|
+
toast.add({
|
|
76
|
+
title,
|
|
77
|
+
description: e instanceof Error ? e.message : String(e),
|
|
78
|
+
icon: 'i-lucide-triangle-alert',
|
|
79
|
+
color: 'error',
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function runReview() {
|
|
84
|
+
if (!blockId.value) return
|
|
85
|
+
try {
|
|
86
|
+
const result = await requirements.review(blockId.value)
|
|
87
|
+
toast.add({
|
|
88
|
+
title: result.items.length
|
|
89
|
+
? `${result.items.length} item(s) to review`
|
|
90
|
+
: 'No gaps found — requirements look complete',
|
|
91
|
+
icon: 'i-lucide-sparkles',
|
|
92
|
+
})
|
|
93
|
+
} catch (e) {
|
|
94
|
+
notifyError('Could not run the requirements review', e)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function submitReply(item: RequirementReviewItem) {
|
|
99
|
+
if (!review.value) return
|
|
100
|
+
const text = (drafts.value[item.id] ?? '').trim()
|
|
101
|
+
if (!text) return
|
|
102
|
+
try {
|
|
103
|
+
await requirements.reply(review.value, item.id, text)
|
|
104
|
+
drafts.value = { ...drafts.value, [item.id]: '' }
|
|
105
|
+
toast.add({ title: 'Answer saved', icon: 'i-lucide-check' })
|
|
106
|
+
} catch (e) {
|
|
107
|
+
notifyError('Could not save the answer', e)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function setStatus(item: RequirementReviewItem, status: ReviewItemStatus) {
|
|
112
|
+
if (!review.value) return
|
|
113
|
+
try {
|
|
114
|
+
await requirements.setItemStatus(review.value, item.id, status)
|
|
115
|
+
} catch (e) {
|
|
116
|
+
notifyError('Could not update the item', e)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function incorporate() {
|
|
121
|
+
if (!review.value) return
|
|
122
|
+
try {
|
|
123
|
+
await requirements.incorporate(review.value)
|
|
124
|
+
toast.add({ title: 'Answers incorporated into the requirements', icon: 'i-lucide-check-check' })
|
|
125
|
+
} catch (e) {
|
|
126
|
+
notifyError('Could not incorporate the answers', e)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
<template>
|
|
132
|
+
<UModal
|
|
133
|
+
v-model:open="open"
|
|
134
|
+
title="Requirements review"
|
|
135
|
+
:description="block?.title"
|
|
136
|
+
:ui="{ content: 'max-w-2xl' }"
|
|
137
|
+
>
|
|
138
|
+
<template #body>
|
|
139
|
+
<div class="flex flex-col gap-4">
|
|
140
|
+
<p class="text-sm text-slate-400">
|
|
141
|
+
An AI reviewer inspects this {{ block?.level ?? 'item' }}’s collected requirements — its
|
|
142
|
+
description plus any linked PRDs and tracker issues — and raises gaps, ambiguities and
|
|
143
|
+
questions. Answer or dismiss each, then incorporate the answers back into the
|
|
144
|
+
requirements.
|
|
145
|
+
</p>
|
|
146
|
+
|
|
147
|
+
<!-- run / re-run -->
|
|
148
|
+
<div class="flex items-center gap-2">
|
|
149
|
+
<UButton
|
|
150
|
+
color="primary"
|
|
151
|
+
variant="solid"
|
|
152
|
+
size="sm"
|
|
153
|
+
icon="i-lucide-sparkles"
|
|
154
|
+
:loading="busy"
|
|
155
|
+
@click="runReview"
|
|
156
|
+
>
|
|
157
|
+
{{ review ? 'Re-run review' : 'Run review' }}
|
|
158
|
+
</UButton>
|
|
159
|
+
<span v-if="review" class="text-xs text-slate-500">
|
|
160
|
+
{{ review.items.length }} item(s) · {{ openCount }} open
|
|
161
|
+
<template v-if="review.model"> · {{ review.model }}</template>
|
|
162
|
+
</span>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<!-- empty / first-run state -->
|
|
166
|
+
<div
|
|
167
|
+
v-if="!review && !busy"
|
|
168
|
+
class="rounded-lg border border-dashed border-slate-700 p-6 text-center text-sm text-slate-500"
|
|
169
|
+
>
|
|
170
|
+
No review yet. Run the reviewer to surface open questions about the requirements.
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<!-- working state -->
|
|
174
|
+
<div
|
|
175
|
+
v-else-if="busy && !review"
|
|
176
|
+
class="flex items-center justify-center gap-2 p-6 text-sm text-slate-400"
|
|
177
|
+
>
|
|
178
|
+
<UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
|
|
179
|
+
Reviewing the requirements…
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<!-- no items: requirements look complete -->
|
|
183
|
+
<div
|
|
184
|
+
v-else-if="review && review.items.length === 0"
|
|
185
|
+
class="flex items-center gap-2 rounded-lg border border-emerald-900/60 bg-emerald-950/30 p-4 text-sm text-emerald-300"
|
|
186
|
+
>
|
|
187
|
+
<UIcon name="i-lucide-circle-check" class="h-5 w-5" />
|
|
188
|
+
The reviewer found no gaps — the requirements look complete and unambiguous.
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<!-- items -->
|
|
192
|
+
<div v-else-if="review" class="flex flex-col gap-3">
|
|
193
|
+
<div
|
|
194
|
+
v-for="item in sortedItems"
|
|
195
|
+
:key="item.id"
|
|
196
|
+
class="rounded-lg border border-slate-800 bg-slate-900/60 p-3"
|
|
197
|
+
:class="{ 'opacity-60': item.status === 'dismissed' }"
|
|
198
|
+
>
|
|
199
|
+
<div class="flex items-start gap-2">
|
|
200
|
+
<UIcon
|
|
201
|
+
:name="CATEGORY_ICON[item.category]"
|
|
202
|
+
class="mt-0.5 h-4 w-4 shrink-0 text-slate-400"
|
|
203
|
+
/>
|
|
204
|
+
<div class="min-w-0 flex-1">
|
|
205
|
+
<div class="flex flex-wrap items-center gap-1.5">
|
|
206
|
+
<span class="text-sm font-medium text-white">{{ item.title }}</span>
|
|
207
|
+
<UBadge size="xs" variant="subtle" :color="SEVERITY_COLOR[item.severity] as any">
|
|
208
|
+
{{ item.severity }}
|
|
209
|
+
</UBadge>
|
|
210
|
+
<UBadge size="xs" variant="outline" color="neutral">{{ item.category }}</UBadge>
|
|
211
|
+
<UBadge
|
|
212
|
+
size="xs"
|
|
213
|
+
variant="soft"
|
|
214
|
+
:color="STATUS_COLOR[item.status] as any"
|
|
215
|
+
class="ml-auto"
|
|
216
|
+
>
|
|
217
|
+
{{ item.status }}
|
|
218
|
+
</UBadge>
|
|
219
|
+
</div>
|
|
220
|
+
<p class="mt-1 whitespace-pre-line text-sm text-slate-400">{{ item.detail }}</p>
|
|
221
|
+
|
|
222
|
+
<!-- recorded answer -->
|
|
223
|
+
<div
|
|
224
|
+
v-if="item.reply"
|
|
225
|
+
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"
|
|
226
|
+
>
|
|
227
|
+
<span class="text-[10px] uppercase tracking-wide text-slate-500">Answer</span>
|
|
228
|
+
<p class="whitespace-pre-line">{{ item.reply }}</p>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<!-- reply + actions (hidden once settled) -->
|
|
232
|
+
<template v-if="item.status !== 'resolved' && item.status !== 'dismissed'">
|
|
233
|
+
<UTextarea
|
|
234
|
+
v-model="drafts[item.id]"
|
|
235
|
+
:rows="2"
|
|
236
|
+
autoresize
|
|
237
|
+
size="sm"
|
|
238
|
+
class="mt-2 w-full"
|
|
239
|
+
:placeholder="item.reply ? 'Refine your answer…' : 'Answer this question…'"
|
|
240
|
+
/>
|
|
241
|
+
<div class="mt-2 flex flex-wrap items-center gap-2">
|
|
242
|
+
<UButton
|
|
243
|
+
color="primary"
|
|
244
|
+
variant="soft"
|
|
245
|
+
size="xs"
|
|
246
|
+
icon="i-lucide-corner-down-left"
|
|
247
|
+
:disabled="!(drafts[item.id] ?? '').trim()"
|
|
248
|
+
@click="submitReply(item)"
|
|
249
|
+
>
|
|
250
|
+
Save answer
|
|
251
|
+
</UButton>
|
|
252
|
+
<UButton
|
|
253
|
+
color="success"
|
|
254
|
+
variant="ghost"
|
|
255
|
+
size="xs"
|
|
256
|
+
icon="i-lucide-check"
|
|
257
|
+
@click="setStatus(item, 'resolved')"
|
|
258
|
+
>
|
|
259
|
+
Resolve
|
|
260
|
+
</UButton>
|
|
261
|
+
<UButton
|
|
262
|
+
color="neutral"
|
|
263
|
+
variant="ghost"
|
|
264
|
+
size="xs"
|
|
265
|
+
icon="i-lucide-x"
|
|
266
|
+
@click="setStatus(item, 'dismissed')"
|
|
267
|
+
>
|
|
268
|
+
Dismiss
|
|
269
|
+
</UButton>
|
|
270
|
+
</div>
|
|
271
|
+
</template>
|
|
272
|
+
|
|
273
|
+
<!-- reopen a settled item -->
|
|
274
|
+
<div v-else class="mt-2">
|
|
275
|
+
<UButton
|
|
276
|
+
color="neutral"
|
|
277
|
+
variant="ghost"
|
|
278
|
+
size="xs"
|
|
279
|
+
icon="i-lucide-rotate-ccw"
|
|
280
|
+
@click="setStatus(item, item.reply ? 'answered' : 'open')"
|
|
281
|
+
>
|
|
282
|
+
Reopen
|
|
283
|
+
</UButton>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<!-- incorporate -->
|
|
290
|
+
<div class="mt-1 flex items-center gap-2 border-t border-slate-800 pt-3">
|
|
291
|
+
<UButton
|
|
292
|
+
color="primary"
|
|
293
|
+
size="sm"
|
|
294
|
+
icon="i-lucide-check-check"
|
|
295
|
+
:loading="incorporating"
|
|
296
|
+
:disabled="!canIncorporate"
|
|
297
|
+
@click="incorporate"
|
|
298
|
+
>
|
|
299
|
+
Incorporate answers
|
|
300
|
+
</UButton>
|
|
301
|
+
<span class="text-xs text-slate-500">
|
|
302
|
+
<template v-if="review.status === 'incorporated'">
|
|
303
|
+
Answers folded into the requirements.
|
|
304
|
+
</template>
|
|
305
|
+
<template v-else-if="canIncorporate">
|
|
306
|
+
All items settled — fold the answers into the requirements.
|
|
307
|
+
</template>
|
|
308
|
+
<template v-else> Resolve or dismiss all items to enable. </template>
|
|
309
|
+
</span>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<!-- incorporated result -->
|
|
313
|
+
<div
|
|
314
|
+
v-if="review.incorporatedRequirements"
|
|
315
|
+
class="rounded-lg border border-slate-800 bg-slate-950/40 p-3"
|
|
316
|
+
>
|
|
317
|
+
<div class="mb-1 text-[10px] uppercase tracking-wide text-slate-500">
|
|
318
|
+
Updated requirements
|
|
319
|
+
</div>
|
|
320
|
+
<p class="whitespace-pre-line text-sm text-slate-300">
|
|
321
|
+
{{ review.incorporatedRequirements }}
|
|
322
|
+
</p>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
</template>
|
|
327
|
+
</UModal>
|
|
328
|
+
</template>
|