@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,175 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Block } from '~/types/domain'
|
|
3
|
+
import { AGENT_BY_KIND } from '~/utils/catalog'
|
|
4
|
+
import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{ block: Block }>()
|
|
7
|
+
|
|
8
|
+
const execution = useExecutionStore()
|
|
9
|
+
const agentRuns = useAgentRunsStore()
|
|
10
|
+
const ui = useUiStore()
|
|
11
|
+
const models = useModelsStore()
|
|
12
|
+
|
|
13
|
+
const instance = computed(() => execution.getInstance(props.block.executionId))
|
|
14
|
+
|
|
15
|
+
// A failed pipeline run surfaces the shared failure banner + retry — the
|
|
16
|
+
// execution failure surface that the old `pr_ready` flip used to hide.
|
|
17
|
+
const failedRun = computed(() => {
|
|
18
|
+
const run = agentRuns.byBlock[props.block.id]
|
|
19
|
+
return run && run.status === 'failed' ? run : null
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const pr = computed(() => props.block.pullRequest)
|
|
23
|
+
/** A PR is merged once the block is `done`; otherwise it is open awaiting merge. */
|
|
24
|
+
const prMerged = computed(() => props.block.status === 'done')
|
|
25
|
+
const prLabel = computed(() => {
|
|
26
|
+
const number = pr.value?.number
|
|
27
|
+
return number ? `PR #${number}` : 'Pull request'
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const stepLabel: Record<string, string> = {
|
|
31
|
+
pending: 'Pending',
|
|
32
|
+
working: 'Working',
|
|
33
|
+
waiting_decision: 'Needs decision',
|
|
34
|
+
done: 'Done',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function openDecisionFor(decisionId: string) {
|
|
38
|
+
if (instance.value) ui.openDecision(instance.value.id, decisionId)
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<div class="space-y-4">
|
|
44
|
+
<!-- running pipeline -->
|
|
45
|
+
<div v-if="instance">
|
|
46
|
+
<div class="mb-1 flex items-center justify-between">
|
|
47
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
48
|
+
{{ instance.pipelineName }}
|
|
49
|
+
</span>
|
|
50
|
+
<UButton
|
|
51
|
+
icon="i-lucide-square"
|
|
52
|
+
color="error"
|
|
53
|
+
variant="ghost"
|
|
54
|
+
size="xs"
|
|
55
|
+
@click="execution.cancel(block.id)"
|
|
56
|
+
>
|
|
57
|
+
Stop
|
|
58
|
+
</UButton>
|
|
59
|
+
</div>
|
|
60
|
+
<ul class="space-y-1">
|
|
61
|
+
<li
|
|
62
|
+
v-for="(s, i) in instance.steps"
|
|
63
|
+
:key="i"
|
|
64
|
+
class="rounded-md px-2 py-1"
|
|
65
|
+
:class="i === instance.currentStep ? 'bg-slate-800/70' : ''"
|
|
66
|
+
>
|
|
67
|
+
<div class="flex items-center gap-2">
|
|
68
|
+
<UIcon
|
|
69
|
+
:name="AGENT_BY_KIND[s.agentKind].icon"
|
|
70
|
+
class="h-4 w-4"
|
|
71
|
+
:style="{ color: AGENT_BY_KIND[s.agentKind].color }"
|
|
72
|
+
/>
|
|
73
|
+
<span class="text-xs text-slate-200">{{ AGENT_BY_KIND[s.agentKind].label }}</span>
|
|
74
|
+
<span
|
|
75
|
+
v-if="s.subtasks && s.subtasks.total > 0"
|
|
76
|
+
class="ml-auto font-mono text-[10px] tabular-nums text-slate-300"
|
|
77
|
+
:title="
|
|
78
|
+
s.subtasks.inProgress > 0
|
|
79
|
+
? `${s.subtasks.completed} of ${s.subtasks.total} subtasks done, ${s.subtasks.inProgress} in progress`
|
|
80
|
+
: `${s.subtasks.completed} of ${s.subtasks.total} subtasks done`
|
|
81
|
+
"
|
|
82
|
+
>
|
|
83
|
+
{{ s.subtasks.completed }}/{{ s.subtasks.total }}
|
|
84
|
+
</span>
|
|
85
|
+
<span class="text-[10px] text-slate-400" :class="{ 'ml-auto': !s.subtasks }">
|
|
86
|
+
{{ stepLabel[s.state] }}
|
|
87
|
+
</span>
|
|
88
|
+
<UButton
|
|
89
|
+
v-if="s.decision && !s.decision.chosen"
|
|
90
|
+
color="warning"
|
|
91
|
+
variant="soft"
|
|
92
|
+
size="xs"
|
|
93
|
+
icon="i-lucide-circle-help"
|
|
94
|
+
@click="openDecisionFor(s.decision.id)"
|
|
95
|
+
>
|
|
96
|
+
Resolve
|
|
97
|
+
</UButton>
|
|
98
|
+
</div>
|
|
99
|
+
<div
|
|
100
|
+
v-if="s.subtasks && s.subtasks.total > 0"
|
|
101
|
+
class="mt-1 ml-6 h-1 overflow-hidden rounded-full bg-slate-700/60"
|
|
102
|
+
>
|
|
103
|
+
<div
|
|
104
|
+
class="h-full rounded-full bg-indigo-400 transition-all duration-500"
|
|
105
|
+
:style="{ width: `${(s.subtasks.completed / s.subtasks.total) * 100}%` }"
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
<div
|
|
109
|
+
v-if="s.model"
|
|
110
|
+
class="mt-0.5 flex items-center gap-1 pl-6 text-[10px] text-slate-500"
|
|
111
|
+
:title="s.model"
|
|
112
|
+
>
|
|
113
|
+
<UIcon name="i-lucide-cpu" class="h-3 w-3" />
|
|
114
|
+
{{ models.labelForRef(s.model) }}
|
|
115
|
+
</div>
|
|
116
|
+
<!-- Prompt-fragment standards the library selected for this step. -->
|
|
117
|
+
<div
|
|
118
|
+
v-if="s.selectedFragmentIds && s.selectedFragmentIds.length"
|
|
119
|
+
class="mt-0.5 flex flex-wrap items-center gap-1 pl-6 text-[10px] text-slate-500"
|
|
120
|
+
:title="`Best-practice fragments folded into this step: ${s.selectedFragmentIds.join(', ')}`"
|
|
121
|
+
>
|
|
122
|
+
<UIcon name="i-lucide-book-marked" class="h-3 w-3 shrink-0" />
|
|
123
|
+
<span>{{ s.selectedFragmentIds.length }} standard(s) applied</span>
|
|
124
|
+
</div>
|
|
125
|
+
</li>
|
|
126
|
+
</ul>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<!-- failed run: shared failure banner + retry -->
|
|
130
|
+
<AgentFailureCard v-if="failedRun" :run="failedRun" />
|
|
131
|
+
|
|
132
|
+
<!-- Open PR: link straight to it on GitHub -->
|
|
133
|
+
<div v-if="pr" class="space-y-2">
|
|
134
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
135
|
+
Pull request
|
|
136
|
+
</span>
|
|
137
|
+
<UButton
|
|
138
|
+
:to="pr.url"
|
|
139
|
+
target="_blank"
|
|
140
|
+
rel="noopener"
|
|
141
|
+
external
|
|
142
|
+
color="neutral"
|
|
143
|
+
variant="soft"
|
|
144
|
+
size="sm"
|
|
145
|
+
icon="i-lucide-git-pull-request"
|
|
146
|
+
trailing-icon="i-lucide-external-link"
|
|
147
|
+
block
|
|
148
|
+
>
|
|
149
|
+
<span class="flex w-full items-center gap-2">
|
|
150
|
+
{{ prLabel }}
|
|
151
|
+
<UBadge :color="prMerged ? 'success' : 'info'" variant="subtle" size="sm" class="ml-auto">
|
|
152
|
+
{{ prMerged ? 'Merged' : 'Open' }}
|
|
153
|
+
</UBadge>
|
|
154
|
+
</span>
|
|
155
|
+
</UButton>
|
|
156
|
+
<p v-if="pr.branch" class="flex items-center gap-1 truncate text-[10px] text-slate-500">
|
|
157
|
+
<UIcon name="i-lucide-git-branch" class="h-3 w-3 shrink-0" />
|
|
158
|
+
<span class="truncate" :title="pr.branch">{{ pr.branch }}</span>
|
|
159
|
+
</p>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<!-- PR ready: merge -->
|
|
163
|
+
<UButton
|
|
164
|
+
v-if="block.status === 'pr_ready'"
|
|
165
|
+
color="success"
|
|
166
|
+
variant="solid"
|
|
167
|
+
size="sm"
|
|
168
|
+
icon="i-lucide-git-merge"
|
|
169
|
+
block
|
|
170
|
+
@click="execution.mergePr(block.id)"
|
|
171
|
+
>
|
|
172
|
+
Merge PR
|
|
173
|
+
</UButton>
|
|
174
|
+
</div>
|
|
175
|
+
</template>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Block } from '~/types/domain'
|
|
3
|
+
import { DEFAULT_CONFIDENCE_THRESHOLD } from '~/utils/catalog'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{ block: Block }>()
|
|
6
|
+
|
|
7
|
+
const board = useBoardStore()
|
|
8
|
+
const models = useModelsStore()
|
|
9
|
+
|
|
10
|
+
// ---- model selection -------------------------------------------------------
|
|
11
|
+
// The model picked for this block (resolved against the deployment's effective
|
|
12
|
+
// catalog); when none is selected the backend runs it with the default model.
|
|
13
|
+
const selectedModel = computed(() => models.getModel(props.block.modelId))
|
|
14
|
+
|
|
15
|
+
// Picker menu: a "Default" reset plus each catalog model. Each label shows the
|
|
16
|
+
// active flavour (Cloudflare vs the direct provider) so it's clear what will run.
|
|
17
|
+
const modelMenu = computed(() => [
|
|
18
|
+
[
|
|
19
|
+
{
|
|
20
|
+
label: 'Default (Qwen)',
|
|
21
|
+
icon: 'i-lucide-rotate-ccw',
|
|
22
|
+
onSelect: () => setModel(''),
|
|
23
|
+
},
|
|
24
|
+
...models.models.map((m) => ({
|
|
25
|
+
label: `${m.label} · ${m.providerLabel}`,
|
|
26
|
+
icon: m.flavor === 'direct' ? 'i-lucide-zap' : 'i-lucide-cloud',
|
|
27
|
+
onSelect: () => setModel(m.id),
|
|
28
|
+
})),
|
|
29
|
+
],
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
function setModel(id: string) {
|
|
33
|
+
board.updateBlock(props.block.id, { modelId: id })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---- confidence threshold (percent <-> 0..1) -------------------------------
|
|
37
|
+
const thresholdPct = computed({
|
|
38
|
+
get: () => Math.round((props.block.confidenceThreshold ?? DEFAULT_CONFIDENCE_THRESHOLD) * 100),
|
|
39
|
+
set: (v: number) =>
|
|
40
|
+
board.updateBlock(props.block.id, {
|
|
41
|
+
confidenceThreshold: Math.min(100, Math.max(0, v)) / 100,
|
|
42
|
+
}),
|
|
43
|
+
})
|
|
44
|
+
const confidencePct = computed(() =>
|
|
45
|
+
props.block.confidence != null ? Math.round(props.block.confidence * 100) : null,
|
|
46
|
+
)
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<div class="space-y-4">
|
|
51
|
+
<!-- model selection -->
|
|
52
|
+
<div>
|
|
53
|
+
<div class="mb-1 flex items-center justify-between">
|
|
54
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
55
|
+
Model
|
|
56
|
+
</span>
|
|
57
|
+
<UDropdownMenu :items="modelMenu">
|
|
58
|
+
<UButton
|
|
59
|
+
size="xs"
|
|
60
|
+
variant="ghost"
|
|
61
|
+
color="neutral"
|
|
62
|
+
icon="i-lucide-cpu"
|
|
63
|
+
trailing-icon="i-lucide-chevron-down"
|
|
64
|
+
/>
|
|
65
|
+
</UDropdownMenu>
|
|
66
|
+
</div>
|
|
67
|
+
<div v-if="selectedModel" class="flex items-center gap-1">
|
|
68
|
+
<UBadge
|
|
69
|
+
color="primary"
|
|
70
|
+
variant="subtle"
|
|
71
|
+
size="sm"
|
|
72
|
+
class="cursor-pointer"
|
|
73
|
+
:title="selectedModel.description"
|
|
74
|
+
@click="setModel('')"
|
|
75
|
+
>
|
|
76
|
+
{{ selectedModel.label }}<UIcon name="i-lucide-x" class="ml-0.5 h-3 w-3" />
|
|
77
|
+
</UBadge>
|
|
78
|
+
<UBadge
|
|
79
|
+
:color="selectedModel.flavor === 'direct' ? 'success' : 'neutral'"
|
|
80
|
+
variant="subtle"
|
|
81
|
+
size="sm"
|
|
82
|
+
:title="
|
|
83
|
+
selectedModel.flavor === 'direct'
|
|
84
|
+
? `Direct via ${selectedModel.providerLabel}`
|
|
85
|
+
: 'Cloudflare Workers AI'
|
|
86
|
+
"
|
|
87
|
+
>
|
|
88
|
+
{{ selectedModel.providerLabel }}
|
|
89
|
+
</UBadge>
|
|
90
|
+
</div>
|
|
91
|
+
<div v-else class="text-[11px] text-slate-500">
|
|
92
|
+
Default — runs the Qwen model ({{
|
|
93
|
+
models.getModel('qwen')?.providerLabel ?? 'Cloudflare'
|
|
94
|
+
}}).
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<!-- confidence threshold -->
|
|
99
|
+
<div>
|
|
100
|
+
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
101
|
+
Auto-merge threshold
|
|
102
|
+
</div>
|
|
103
|
+
<div class="flex items-center gap-2">
|
|
104
|
+
<UInput
|
|
105
|
+
v-model.number="thresholdPct"
|
|
106
|
+
type="number"
|
|
107
|
+
min="0"
|
|
108
|
+
max="100"
|
|
109
|
+
size="sm"
|
|
110
|
+
class="w-20"
|
|
111
|
+
/>
|
|
112
|
+
<span class="text-[11px] text-slate-400">% confidence</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div v-if="confidencePct != null" class="mt-1 text-[11px]">
|
|
115
|
+
Last run scored
|
|
116
|
+
<span
|
|
117
|
+
:class="
|
|
118
|
+
block.confidence! >= (block.confidenceThreshold ?? 0.8)
|
|
119
|
+
? 'text-emerald-400'
|
|
120
|
+
: 'text-amber-400'
|
|
121
|
+
"
|
|
122
|
+
>
|
|
123
|
+
{{ confidencePct }}%
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</template>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Block } from '~/types/domain'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{ block: Block }>()
|
|
5
|
+
|
|
6
|
+
const board = useBoardStore()
|
|
7
|
+
const fragments = useFragmentsStore()
|
|
8
|
+
|
|
9
|
+
// ---- features implemented --------------------------------------------------
|
|
10
|
+
const newFeature = ref('')
|
|
11
|
+
function addFeature() {
|
|
12
|
+
const v = newFeature.value.trim()
|
|
13
|
+
if (!v) return
|
|
14
|
+
const list = props.block.features ? [...props.block.features] : []
|
|
15
|
+
if (!list.includes(v)) list.push(v)
|
|
16
|
+
board.updateBlock(props.block.id, { features: list })
|
|
17
|
+
newFeature.value = ''
|
|
18
|
+
}
|
|
19
|
+
function removeFeature(f: string) {
|
|
20
|
+
if (!props.block.features) return
|
|
21
|
+
board.updateBlock(props.block.id, { features: props.block.features.filter((x) => x !== f) })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---- best-practice prompt fragments ----------------------------------------
|
|
25
|
+
// Selected fragments (resolved against the catalog; unknown ids are dropped).
|
|
26
|
+
const selectedFragments = computed(() =>
|
|
27
|
+
(props.block.fragmentIds ?? [])
|
|
28
|
+
.map((id) => fragments.getFragment(id))
|
|
29
|
+
.filter((f): f is NonNullable<typeof f> => !!f),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// Picker menu: fragments suitable for this block's type, not already selected,
|
|
33
|
+
// grouped by category so the dropdown reads like the catalog.
|
|
34
|
+
const fragmentMenu = computed(() => {
|
|
35
|
+
const selected = new Set(props.block.fragmentIds ?? [])
|
|
36
|
+
const groups = new Map<string, { label: string; onSelect: () => void }[]>()
|
|
37
|
+
for (const f of fragments.forBlockType(props.block.type)) {
|
|
38
|
+
if (selected.has(f.id)) continue
|
|
39
|
+
const items = groups.get(f.category) ?? []
|
|
40
|
+
items.push({ label: f.title, onSelect: () => addFragment(f.id) })
|
|
41
|
+
groups.set(f.category, items)
|
|
42
|
+
}
|
|
43
|
+
return [...groups.values()]
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function addFragment(id: string) {
|
|
47
|
+
const list = props.block.fragmentIds ? [...props.block.fragmentIds] : []
|
|
48
|
+
if (!list.includes(id)) list.push(id)
|
|
49
|
+
board.updateBlock(props.block.id, { fragmentIds: list })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function removeFragment(id: string) {
|
|
53
|
+
if (!props.block.fragmentIds) return
|
|
54
|
+
board.updateBlock(props.block.id, {
|
|
55
|
+
fragmentIds: props.block.fragmentIds.filter((x) => x !== id),
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<template>
|
|
61
|
+
<div class="space-y-4">
|
|
62
|
+
<!-- module assignment -->
|
|
63
|
+
<div>
|
|
64
|
+
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
65
|
+
Module
|
|
66
|
+
</div>
|
|
67
|
+
<UInput
|
|
68
|
+
v-model="block.moduleName"
|
|
69
|
+
size="sm"
|
|
70
|
+
class="w-full"
|
|
71
|
+
placeholder="e.g. Sessions (created on implement if new)"
|
|
72
|
+
icon="i-lucide-package"
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<!-- features implemented -->
|
|
77
|
+
<div>
|
|
78
|
+
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
79
|
+
Features implemented
|
|
80
|
+
</div>
|
|
81
|
+
<div v-if="block.features?.length" class="mb-1 flex flex-wrap gap-1">
|
|
82
|
+
<UBadge
|
|
83
|
+
v-for="f in block.features"
|
|
84
|
+
:key="f"
|
|
85
|
+
color="success"
|
|
86
|
+
variant="subtle"
|
|
87
|
+
size="sm"
|
|
88
|
+
class="cursor-pointer"
|
|
89
|
+
@click="removeFeature(f)"
|
|
90
|
+
>
|
|
91
|
+
{{ f }}<UIcon name="i-lucide-x" class="ml-0.5 h-3 w-3" />
|
|
92
|
+
</UBadge>
|
|
93
|
+
</div>
|
|
94
|
+
<UInput
|
|
95
|
+
v-model="newFeature"
|
|
96
|
+
size="sm"
|
|
97
|
+
class="w-full"
|
|
98
|
+
placeholder="Add a feature, press Enter"
|
|
99
|
+
icon="i-lucide-puzzle"
|
|
100
|
+
@keydown.enter="addFeature"
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<!-- best practices (prompt fragments) -->
|
|
105
|
+
<div>
|
|
106
|
+
<div class="mb-1 flex items-center justify-between">
|
|
107
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
108
|
+
Best practices
|
|
109
|
+
</span>
|
|
110
|
+
<UDropdownMenu v-if="fragmentMenu.length" :items="fragmentMenu">
|
|
111
|
+
<UButton
|
|
112
|
+
size="xs"
|
|
113
|
+
variant="ghost"
|
|
114
|
+
color="neutral"
|
|
115
|
+
icon="i-lucide-plus"
|
|
116
|
+
trailing-icon="i-lucide-chevron-down"
|
|
117
|
+
/>
|
|
118
|
+
</UDropdownMenu>
|
|
119
|
+
</div>
|
|
120
|
+
<div v-if="selectedFragments.length" class="mb-1 flex flex-wrap gap-1">
|
|
121
|
+
<UBadge
|
|
122
|
+
v-for="f in selectedFragments"
|
|
123
|
+
:key="f.id"
|
|
124
|
+
color="primary"
|
|
125
|
+
variant="subtle"
|
|
126
|
+
size="sm"
|
|
127
|
+
class="cursor-pointer"
|
|
128
|
+
:title="f.summary"
|
|
129
|
+
@click="removeFragment(f.id)"
|
|
130
|
+
>
|
|
131
|
+
{{ f.title }}<UIcon name="i-lucide-x" class="ml-0.5 h-3 w-3" />
|
|
132
|
+
</UBadge>
|
|
133
|
+
</div>
|
|
134
|
+
<div v-else class="text-[11px] text-slate-500">
|
|
135
|
+
None — agents follow their default guidance.
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</template>
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type { AgentKind } from '~/types/domain'
|
|
4
|
+
import { AGENT_BY_KIND } from '~/utils/catalog'
|
|
5
|
+
import AgentPalette from '~/components/palettes/AgentPalette.vue'
|
|
6
|
+
|
|
7
|
+
const pipelines = usePipelinesStore()
|
|
8
|
+
const agents = useAgentsStore()
|
|
9
|
+
const ui = useUiStore()
|
|
10
|
+
|
|
11
|
+
const open = computed({
|
|
12
|
+
get: () => ui.builderOpen,
|
|
13
|
+
set: (v: boolean) => (ui.builderOpen = v),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
function add(kind: AgentKind) {
|
|
17
|
+
pipelines.addToDraft(kind)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const toast = useToast()
|
|
21
|
+
|
|
22
|
+
// ---- "Add agent" mini-form -------------------------------------------------
|
|
23
|
+
const addAgentOpen = ref(false)
|
|
24
|
+
const newAgentName = ref('')
|
|
25
|
+
const newAgentDesc = ref('')
|
|
26
|
+
|
|
27
|
+
function openAddAgent() {
|
|
28
|
+
newAgentName.value = ''
|
|
29
|
+
newAgentDesc.value = ''
|
|
30
|
+
addAgentOpen.value = true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createAgent() {
|
|
34
|
+
const agent = agents.addAgent({
|
|
35
|
+
label: newAgentName.value,
|
|
36
|
+
description: newAgentDesc.value,
|
|
37
|
+
})
|
|
38
|
+
toast.add({ title: `Added agent “${agent.label}”`, color: 'success', icon: 'i-lucide-check' })
|
|
39
|
+
addAgentOpen.value = false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function placeholder(what: string) {
|
|
43
|
+
toast.add({ title: 'Placeholder', description: what, icon: 'i-lucide-construction' })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function save() {
|
|
47
|
+
try {
|
|
48
|
+
const saved = await pipelines.saveDraft()
|
|
49
|
+
if (saved) {
|
|
50
|
+
toast.add({ title: `Saved “${saved.name}”`, color: 'success', icon: 'i-lucide-check' })
|
|
51
|
+
ui.builderOpen = false
|
|
52
|
+
} else {
|
|
53
|
+
toast.add({ title: 'Add at least one agent first', color: 'warning' })
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
toast.add({ title: 'Could not save pipeline', color: 'error' })
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<template>
|
|
62
|
+
<USlideover v-model:open="open" title="Pipeline builder" side="left">
|
|
63
|
+
<template #body>
|
|
64
|
+
<div class="grid h-full grid-cols-2 gap-4">
|
|
65
|
+
<!-- agent palette -->
|
|
66
|
+
<div class="overflow-y-auto pr-1">
|
|
67
|
+
<div class="mb-2 flex items-center justify-between gap-2">
|
|
68
|
+
<h3 class="text-xs font-semibold uppercase tracking-wide text-slate-400">
|
|
69
|
+
Agent palette
|
|
70
|
+
</h3>
|
|
71
|
+
<UButton
|
|
72
|
+
color="primary"
|
|
73
|
+
variant="soft"
|
|
74
|
+
size="xs"
|
|
75
|
+
icon="i-lucide-plus"
|
|
76
|
+
@click="openAddAgent"
|
|
77
|
+
>
|
|
78
|
+
Add agent
|
|
79
|
+
</UButton>
|
|
80
|
+
</div>
|
|
81
|
+
<AgentPalette @add="add" />
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<!-- draft chain -->
|
|
85
|
+
<div class="flex flex-col">
|
|
86
|
+
<h3 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400">
|
|
87
|
+
Pipeline
|
|
88
|
+
</h3>
|
|
89
|
+
<UInput
|
|
90
|
+
v-model="pipelines.draftName"
|
|
91
|
+
placeholder="Pipeline name"
|
|
92
|
+
size="sm"
|
|
93
|
+
class="mb-3"
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
<div
|
|
97
|
+
v-if="pipelines.draft.length === 0"
|
|
98
|
+
class="flex flex-1 items-center justify-center rounded-lg border border-dashed border-slate-700 p-4 text-center text-xs text-slate-500"
|
|
99
|
+
>
|
|
100
|
+
Click agents on the left to assemble a linear pipeline.
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<ol v-else class="flex-1 space-y-2 overflow-y-auto">
|
|
104
|
+
<li
|
|
105
|
+
v-for="(kind, i) in pipelines.draft"
|
|
106
|
+
:key="i"
|
|
107
|
+
class="flex items-center gap-2 rounded-lg border border-slate-700 bg-slate-800/60 p-2"
|
|
108
|
+
>
|
|
109
|
+
<span class="w-4 text-center text-[10px] text-slate-500">{{ i + 1 }}</span>
|
|
110
|
+
<UIcon
|
|
111
|
+
:name="AGENT_BY_KIND[kind].icon"
|
|
112
|
+
class="h-4 w-4"
|
|
113
|
+
:style="{ color: AGENT_BY_KIND[kind].color }"
|
|
114
|
+
/>
|
|
115
|
+
<span class="text-xs text-slate-100">{{ AGENT_BY_KIND[kind].label }}</span>
|
|
116
|
+
<div class="ml-auto flex items-center">
|
|
117
|
+
<UButton
|
|
118
|
+
icon="i-lucide-chevron-up"
|
|
119
|
+
color="neutral"
|
|
120
|
+
variant="ghost"
|
|
121
|
+
size="xs"
|
|
122
|
+
:disabled="i === 0"
|
|
123
|
+
@click="pipelines.moveInDraft(i, i - 1)"
|
|
124
|
+
/>
|
|
125
|
+
<UButton
|
|
126
|
+
icon="i-lucide-chevron-down"
|
|
127
|
+
color="neutral"
|
|
128
|
+
variant="ghost"
|
|
129
|
+
size="xs"
|
|
130
|
+
:disabled="i === pipelines.draft.length - 1"
|
|
131
|
+
@click="pipelines.moveInDraft(i, i + 1)"
|
|
132
|
+
/>
|
|
133
|
+
<UButton
|
|
134
|
+
icon="i-lucide-x"
|
|
135
|
+
color="error"
|
|
136
|
+
variant="ghost"
|
|
137
|
+
size="xs"
|
|
138
|
+
@click="pipelines.removeFromDraft(i)"
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
</li>
|
|
142
|
+
</ol>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</template>
|
|
146
|
+
|
|
147
|
+
<template #footer>
|
|
148
|
+
<div class="flex w-full items-center justify-between">
|
|
149
|
+
<UButton color="neutral" variant="ghost" size="sm" @click="pipelines.clearDraft()">
|
|
150
|
+
Clear
|
|
151
|
+
</UButton>
|
|
152
|
+
<UButton
|
|
153
|
+
color="primary"
|
|
154
|
+
icon="i-lucide-save"
|
|
155
|
+
size="sm"
|
|
156
|
+
:disabled="pipelines.draft.length === 0"
|
|
157
|
+
@click="save"
|
|
158
|
+
>
|
|
159
|
+
Save pipeline
|
|
160
|
+
</UButton>
|
|
161
|
+
</div>
|
|
162
|
+
</template>
|
|
163
|
+
</USlideover>
|
|
164
|
+
|
|
165
|
+
<!-- Add-agent form -->
|
|
166
|
+
<UModal v-model:open="addAgentOpen" title="Add agent">
|
|
167
|
+
<template #body>
|
|
168
|
+
<div class="space-y-3">
|
|
169
|
+
<div>
|
|
170
|
+
<label
|
|
171
|
+
class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-slate-400"
|
|
172
|
+
>
|
|
173
|
+
Name
|
|
174
|
+
</label>
|
|
175
|
+
<UInput
|
|
176
|
+
v-model="newAgentName"
|
|
177
|
+
placeholder="e.g. Security Auditor"
|
|
178
|
+
size="sm"
|
|
179
|
+
class="w-full"
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
<div>
|
|
183
|
+
<label
|
|
184
|
+
class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-slate-400"
|
|
185
|
+
>
|
|
186
|
+
Description
|
|
187
|
+
</label>
|
|
188
|
+
<UTextarea
|
|
189
|
+
v-model="newAgentDesc"
|
|
190
|
+
:rows="2"
|
|
191
|
+
autoresize
|
|
192
|
+
size="sm"
|
|
193
|
+
class="w-full"
|
|
194
|
+
placeholder="What does this agent do?"
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
<UButton
|
|
198
|
+
color="neutral"
|
|
199
|
+
variant="soft"
|
|
200
|
+
size="xs"
|
|
201
|
+
icon="i-lucide-file-text"
|
|
202
|
+
block
|
|
203
|
+
@click="placeholder('Link context document')"
|
|
204
|
+
>
|
|
205
|
+
Link context document
|
|
206
|
+
</UButton>
|
|
207
|
+
</div>
|
|
208
|
+
</template>
|
|
209
|
+
|
|
210
|
+
<template #footer>
|
|
211
|
+
<div class="flex w-full items-center justify-end gap-2">
|
|
212
|
+
<UButton color="neutral" variant="ghost" size="sm" @click="addAgentOpen = false">
|
|
213
|
+
Cancel
|
|
214
|
+
</UButton>
|
|
215
|
+
<UButton
|
|
216
|
+
color="primary"
|
|
217
|
+
icon="i-lucide-plus"
|
|
218
|
+
size="sm"
|
|
219
|
+
:disabled="!newAgentName.trim()"
|
|
220
|
+
@click="createAgent"
|
|
221
|
+
>
|
|
222
|
+
Create agent
|
|
223
|
+
</UButton>
|
|
224
|
+
</div>
|
|
225
|
+
</template>
|
|
226
|
+
</UModal>
|
|
227
|
+
</template>
|