@agile-vibe-coding/avc 0.1.1 → 0.3.1
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/cli/agent-loader.js +21 -0
- package/cli/agents/agent-selector.md +152 -0
- package/cli/agents/architecture-recommender.md +418 -0
- package/cli/agents/code-implementer.md +117 -0
- package/cli/agents/code-validator.md +80 -0
- package/cli/agents/context-reviewer-epic.md +101 -0
- package/cli/agents/context-reviewer-story.md +92 -0
- package/cli/agents/context-writer-epic.md +145 -0
- package/cli/agents/context-writer-story.md +111 -0
- package/cli/agents/database-deep-dive.md +470 -0
- package/cli/agents/database-recommender.md +634 -0
- package/cli/agents/doc-distributor.md +176 -0
- package/cli/agents/doc-writer-epic.md +42 -0
- package/cli/agents/doc-writer-story.md +43 -0
- package/cli/agents/documentation-updater.md +203 -0
- package/cli/agents/duplicate-detector.md +110 -0
- package/cli/agents/epic-story-decomposer.md +559 -0
- package/cli/agents/feature-context-generator.md +91 -0
- package/cli/agents/gap-checker-epic.md +52 -0
- package/cli/agents/impact-checker-story.md +51 -0
- package/cli/agents/migration-guide-generator.md +305 -0
- package/cli/agents/mission-scope-generator.md +143 -0
- package/cli/agents/mission-scope-validator.md +146 -0
- package/cli/agents/project-context-extractor.md +122 -0
- package/cli/agents/project-documentation-creator.json +226 -0
- package/cli/agents/project-documentation-creator.md +595 -0
- package/cli/agents/question-prefiller.md +269 -0
- package/cli/agents/refiner-epic.md +39 -0
- package/cli/agents/refiner-story.md +42 -0
- package/cli/agents/scaffolding-generator.md +99 -0
- package/cli/agents/seed-validator.md +71 -0
- package/cli/agents/story-doc-enricher.md +133 -0
- package/cli/agents/story-scope-reviewer.md +147 -0
- package/cli/agents/story-splitter.md +83 -0
- package/cli/agents/suggestion-business-analyst.md +88 -0
- package/cli/agents/suggestion-deployment-architect.md +263 -0
- package/cli/agents/suggestion-product-manager.md +129 -0
- package/cli/agents/suggestion-security-specialist.md +156 -0
- package/cli/agents/suggestion-technical-architect.md +269 -0
- package/cli/agents/suggestion-ux-researcher.md +93 -0
- package/cli/agents/task-subtask-decomposer.md +188 -0
- package/cli/agents/validator-documentation.json +183 -0
- package/cli/agents/validator-documentation.md +455 -0
- package/cli/agents/validator-selector.md +211 -0
- package/cli/ansi-colors.js +21 -0
- package/cli/api-reference-tool.js +368 -0
- package/cli/build-docs.js +29 -8
- package/cli/ceremony-history.js +369 -0
- package/cli/checks/catalog.json +76 -0
- package/cli/checks/code/quality.json +26 -0
- package/cli/checks/code/testing.json +14 -0
- package/cli/checks/code/traceability.json +26 -0
- package/cli/checks/cross-refs/epic.json +171 -0
- package/cli/checks/cross-refs/story.json +149 -0
- package/cli/checks/epic/api.json +114 -0
- package/cli/checks/epic/backend.json +126 -0
- package/cli/checks/epic/cloud.json +126 -0
- package/cli/checks/epic/data.json +102 -0
- package/cli/checks/epic/database.json +114 -0
- package/cli/checks/epic/developer.json +182 -0
- package/cli/checks/epic/devops.json +174 -0
- package/cli/checks/epic/frontend.json +162 -0
- package/cli/checks/epic/mobile.json +102 -0
- package/cli/checks/epic/qa.json +90 -0
- package/cli/checks/epic/security.json +184 -0
- package/cli/checks/epic/solution-architect.json +192 -0
- package/cli/checks/epic/test-architect.json +90 -0
- package/cli/checks/epic/ui.json +102 -0
- package/cli/checks/epic/ux.json +90 -0
- package/cli/checks/fixes/epic-fix-template.md +10 -0
- package/cli/checks/fixes/story-fix-template.md +10 -0
- package/cli/checks/story/api.json +186 -0
- package/cli/checks/story/backend.json +102 -0
- package/cli/checks/story/cloud.json +102 -0
- package/cli/checks/story/data.json +210 -0
- package/cli/checks/story/database.json +102 -0
- package/cli/checks/story/developer.json +168 -0
- package/cli/checks/story/devops.json +102 -0
- package/cli/checks/story/frontend.json +174 -0
- package/cli/checks/story/mobile.json +102 -0
- package/cli/checks/story/qa.json +210 -0
- package/cli/checks/story/security.json +198 -0
- package/cli/checks/story/solution-architect.json +230 -0
- package/cli/checks/story/test-architect.json +210 -0
- package/cli/checks/story/ui.json +102 -0
- package/cli/checks/story/ux.json +102 -0
- package/cli/coding-order.js +401 -0
- package/cli/command-logger.js +49 -12
- package/cli/components/static-output.js +63 -0
- package/cli/console-output-manager.js +94 -0
- package/cli/dependency-checker.js +72 -0
- package/cli/docs-sync.js +306 -0
- package/cli/epic-story-validator.js +659 -0
- package/cli/evaluation-prompts.js +1008 -0
- package/cli/execution-context.js +195 -0
- package/cli/generate-summary-table.js +340 -0
- package/cli/init-model-config.js +704 -0
- package/cli/init.js +1737 -278
- package/cli/kanban-server-manager.js +227 -0
- package/cli/llm-claude.js +150 -1
- package/cli/llm-gemini.js +109 -0
- package/cli/llm-local.js +493 -0
- package/cli/llm-mock.js +233 -0
- package/cli/llm-openai.js +454 -0
- package/cli/llm-provider.js +379 -3
- package/cli/llm-token-limits.js +211 -0
- package/cli/llm-verifier.js +662 -0
- package/cli/llm-xiaomi.js +143 -0
- package/cli/message-constants.js +49 -0
- package/cli/message-manager.js +334 -0
- package/cli/message-types.js +96 -0
- package/cli/messaging-api.js +291 -0
- package/cli/micro-check-fixer.js +335 -0
- package/cli/micro-check-runner.js +449 -0
- package/cli/micro-check-scorer.js +148 -0
- package/cli/micro-check-validator.js +538 -0
- package/cli/model-pricing.js +192 -0
- package/cli/model-query-engine.js +468 -0
- package/cli/model-recommendation-analyzer.js +495 -0
- package/cli/model-selector.js +270 -0
- package/cli/output-buffer.js +107 -0
- package/cli/process-manager.js +73 -2
- package/cli/prompt-logger.js +57 -0
- package/cli/repl-ink.js +4625 -1094
- package/cli/repl-old.js +3 -4
- package/cli/seed-processor.js +962 -0
- package/cli/sprint-planning-processor.js +4162 -0
- package/cli/template-processor.js +2149 -105
- package/cli/templates/project.md +25 -8
- package/cli/templates/vitepress-config.mts.template +5 -4
- package/cli/token-tracker.js +547 -0
- package/cli/tools/generate-story-validators.js +317 -0
- package/cli/tools/generate-validators.js +669 -0
- package/cli/update-checker.js +19 -17
- package/cli/update-notifier.js +4 -4
- package/cli/validation-router.js +667 -0
- package/cli/verification-tracker.js +563 -0
- package/cli/worktree-runner.js +654 -0
- package/kanban/README.md +386 -0
- package/kanban/client/README.md +205 -0
- package/kanban/client/components.json +20 -0
- package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
- package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
- package/kanban/client/dist/index.html +16 -0
- package/kanban/client/dist/vite.svg +1 -0
- package/kanban/client/index.html +15 -0
- package/kanban/client/package-lock.json +9442 -0
- package/kanban/client/package.json +44 -0
- package/kanban/client/postcss.config.js +6 -0
- package/kanban/client/public/vite.svg +1 -0
- package/kanban/client/src/App.jsx +651 -0
- package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
- package/kanban/client/src/components/ceremony/AskArchPopup.jsx +420 -0
- package/kanban/client/src/components/ceremony/AskModelPopup.jsx +629 -0
- package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +1133 -0
- package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
- package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
- package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +686 -0
- package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +838 -0
- package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
- package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +136 -0
- package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
- package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
- package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
- package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +329 -0
- package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +249 -0
- package/kanban/client/src/components/kanban/CardDetailModal.jsx +646 -0
- package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
- package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
- package/kanban/client/src/components/kanban/GroupingSelector.jsx +63 -0
- package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
- package/kanban/client/src/components/kanban/KanbanCard.jsx +147 -0
- package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
- package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +784 -0
- package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
- package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
- package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
- package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
- package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
- package/kanban/client/src/components/settings/AgentsTab.jsx +381 -0
- package/kanban/client/src/components/settings/ApiKeysTab.jsx +142 -0
- package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +105 -0
- package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
- package/kanban/client/src/components/settings/CostThresholdsTab.jsx +95 -0
- package/kanban/client/src/components/settings/ModelPricingTab.jsx +269 -0
- package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
- package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
- package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
- package/kanban/client/src/components/stats/CostModal.jsx +384 -0
- package/kanban/client/src/components/ui/badge.jsx +27 -0
- package/kanban/client/src/components/ui/dialog.jsx +121 -0
- package/kanban/client/src/components/ui/tabs.jsx +85 -0
- package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
- package/kanban/client/src/hooks/useGrouping.js +177 -0
- package/kanban/client/src/hooks/useWebSocket.js +120 -0
- package/kanban/client/src/lib/__tests__/api.test.js +196 -0
- package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
- package/kanban/client/src/lib/api.js +515 -0
- package/kanban/client/src/lib/status-grouping.js +154 -0
- package/kanban/client/src/lib/utils.js +11 -0
- package/kanban/client/src/main.jsx +10 -0
- package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
- package/kanban/client/src/store/ceremonyStore.js +172 -0
- package/kanban/client/src/store/filterStore.js +201 -0
- package/kanban/client/src/store/kanbanStore.js +123 -0
- package/kanban/client/src/store/processStore.js +65 -0
- package/kanban/client/src/store/sprintPlanningStore.js +33 -0
- package/kanban/client/src/styles/globals.css +59 -0
- package/kanban/client/tailwind.config.js +77 -0
- package/kanban/client/vite.config.js +28 -0
- package/kanban/client/vitest.config.js +28 -0
- package/kanban/dev-start.sh +47 -0
- package/kanban/package.json +12 -0
- package/kanban/server/index.js +537 -0
- package/kanban/server/routes/ceremony.js +454 -0
- package/kanban/server/routes/costs.js +163 -0
- package/kanban/server/routes/openai-oauth.js +366 -0
- package/kanban/server/routes/processes.js +50 -0
- package/kanban/server/routes/settings.js +736 -0
- package/kanban/server/routes/websocket.js +281 -0
- package/kanban/server/routes/work-items.js +487 -0
- package/kanban/server/services/CeremonyService.js +1441 -0
- package/kanban/server/services/FileSystemScanner.js +95 -0
- package/kanban/server/services/FileWatcher.js +144 -0
- package/kanban/server/services/HierarchyBuilder.js +196 -0
- package/kanban/server/services/ProcessRegistry.js +122 -0
- package/kanban/server/services/TaskRunnerService.js +261 -0
- package/kanban/server/services/WorkItemReader.js +123 -0
- package/kanban/server/services/WorkItemRefineService.js +510 -0
- package/kanban/server/start.js +49 -0
- package/kanban/server/utils/kanban-logger.js +132 -0
- package/kanban/server/utils/markdown.js +91 -0
- package/kanban/server/utils/status-grouping.js +107 -0
- package/kanban/server/workers/run-task-worker.js +121 -0
- package/kanban/server/workers/seed-worker.js +94 -0
- package/kanban/server/workers/sponsor-call-worker.js +92 -0
- package/kanban/server/workers/sprint-planning-worker.js +212 -0
- package/package.json +19 -7
- package/cli/agents/documentation.md +0 -302
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { Wand2, Check, X, ChevronDown, ChevronRight, AlertTriangle } from 'lucide-react';
|
|
3
|
+
import { getModels, getSettings, refineWorkItem, applyWorkItemChanges } from '../../lib/api';
|
|
4
|
+
|
|
5
|
+
function normalizeProvider(p = '') {
|
|
6
|
+
if (p === 'claude') return 'anthropic';
|
|
7
|
+
return p;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function ModelSelect({ label, value, onChange, models, disabled }) {
|
|
11
|
+
const providers = [...new Set(models.map((m) => m.provider))];
|
|
12
|
+
const chosen = models.find((m) => m.modelId === value);
|
|
13
|
+
return (
|
|
14
|
+
<div>
|
|
15
|
+
<label className="block text-xs font-medium text-slate-500 mb-1">{label}</label>
|
|
16
|
+
<select
|
|
17
|
+
value={value}
|
|
18
|
+
onChange={(e) => onChange(e.target.value)}
|
|
19
|
+
disabled={disabled || models.length === 0}
|
|
20
|
+
className="w-full rounded-md border border-slate-300 px-2 py-1.5 text-xs text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-500 disabled:opacity-60 bg-white"
|
|
21
|
+
>
|
|
22
|
+
{models.length === 0 && <option value="">Loading…</option>}
|
|
23
|
+
{providers.map((p) => (
|
|
24
|
+
<optgroup key={p} label={p.charAt(0).toUpperCase() + p.slice(1)}>
|
|
25
|
+
{models.filter((m) => m.provider === p).map((m) => (
|
|
26
|
+
<option key={m.modelId} value={m.modelId}>
|
|
27
|
+
{m.displayName}{!m.hasApiKey ? ' (no key)' : ''}
|
|
28
|
+
</option>
|
|
29
|
+
))}
|
|
30
|
+
</optgroup>
|
|
31
|
+
))}
|
|
32
|
+
</select>
|
|
33
|
+
{chosen && !chosen.hasApiKey && (
|
|
34
|
+
<p className="text-xs text-amber-600 mt-0.5">
|
|
35
|
+
⚠ No API key — add to <code>.env</code>
|
|
36
|
+
</p>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function FieldDiff({ label, before, after }) {
|
|
43
|
+
const beforeStr = Array.isArray(before) ? before.join('\n') : (before ?? '');
|
|
44
|
+
const afterStr = Array.isArray(after) ? after.join('\n') : (after ?? '');
|
|
45
|
+
if (!beforeStr && !afterStr) return null;
|
|
46
|
+
if (beforeStr === afterStr) return null;
|
|
47
|
+
return (
|
|
48
|
+
<div className="mb-3">
|
|
49
|
+
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">{label}</p>
|
|
50
|
+
{beforeStr && (
|
|
51
|
+
<div className="mb-1 px-2.5 py-2 bg-red-50 border border-red-100 rounded text-xs text-red-700 leading-relaxed line-through">
|
|
52
|
+
{beforeStr}
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
{afterStr && (
|
|
56
|
+
<div className="px-2.5 py-2 bg-green-50 border border-green-100 rounded text-xs text-green-700 leading-relaxed">
|
|
57
|
+
{afterStr}
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function StoryUpdateCard({ impact, checked, onToggle }) {
|
|
65
|
+
const [expanded, setExpanded] = useState(false);
|
|
66
|
+
return (
|
|
67
|
+
<div className={`border rounded-lg overflow-hidden transition-colors ${
|
|
68
|
+
checked ? 'border-violet-300 bg-violet-50' : 'border-slate-200 bg-white'
|
|
69
|
+
}`}>
|
|
70
|
+
<div className="flex items-center gap-2.5 px-3 py-2.5">
|
|
71
|
+
<input
|
|
72
|
+
type="checkbox"
|
|
73
|
+
checked={checked}
|
|
74
|
+
onChange={onToggle}
|
|
75
|
+
className="flex-shrink-0 accent-violet-600"
|
|
76
|
+
/>
|
|
77
|
+
<div className="flex-1 min-w-0">
|
|
78
|
+
<p className="text-xs font-medium text-slate-800 truncate">
|
|
79
|
+
{impact.proposedStory?.name ?? impact.storyId}
|
|
80
|
+
</p>
|
|
81
|
+
<p className="text-xs text-slate-400 truncate">{impact.storyId}</p>
|
|
82
|
+
</div>
|
|
83
|
+
{impact.changesNeeded && (
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={() => setExpanded((v) => !v)}
|
|
87
|
+
className="flex-shrink-0 text-slate-400 hover:text-slate-600"
|
|
88
|
+
>
|
|
89
|
+
{expanded
|
|
90
|
+
? <ChevronDown className="w-3.5 h-3.5" />
|
|
91
|
+
: <ChevronRight className="w-3.5 h-3.5" />}
|
|
92
|
+
</button>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
{expanded && impact.changesNeeded && (
|
|
96
|
+
<div className="px-3 pb-3 pt-1 border-t border-slate-100">
|
|
97
|
+
<p className="text-xs text-slate-600 leading-relaxed">{impact.changesNeeded}</p>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function NewStoryCard({ story, checked, onToggle }) {
|
|
105
|
+
const [expanded, setExpanded] = useState(false);
|
|
106
|
+
const acs = story?.acceptance ?? story?.acceptanceCriteria ?? [];
|
|
107
|
+
return (
|
|
108
|
+
<div className={`border rounded-lg overflow-hidden transition-colors ${
|
|
109
|
+
checked ? 'border-emerald-300 bg-emerald-50' : 'border-slate-200 bg-white'
|
|
110
|
+
}`}>
|
|
111
|
+
<div className="flex items-center gap-2.5 px-3 py-2.5">
|
|
112
|
+
<input
|
|
113
|
+
type="checkbox"
|
|
114
|
+
checked={checked}
|
|
115
|
+
onChange={onToggle}
|
|
116
|
+
className="flex-shrink-0 accent-emerald-600"
|
|
117
|
+
/>
|
|
118
|
+
<div className="flex-1 min-w-0">
|
|
119
|
+
<div className="flex items-center gap-1.5 mb-0.5">
|
|
120
|
+
<span className="flex-shrink-0 text-xs font-bold px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-700">
|
|
121
|
+
NEW
|
|
122
|
+
</span>
|
|
123
|
+
<p className="text-xs font-medium text-slate-800 truncate">{story?.name}</p>
|
|
124
|
+
</div>
|
|
125
|
+
{story?.description && (
|
|
126
|
+
<p className="text-xs text-slate-500 truncate">{story.description}</p>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
onClick={() => setExpanded((v) => !v)}
|
|
132
|
+
className="flex-shrink-0 text-slate-400 hover:text-slate-600"
|
|
133
|
+
>
|
|
134
|
+
{expanded
|
|
135
|
+
? <ChevronDown className="w-3.5 h-3.5" />
|
|
136
|
+
: <ChevronRight className="w-3.5 h-3.5" />}
|
|
137
|
+
</button>
|
|
138
|
+
</div>
|
|
139
|
+
{expanded && (
|
|
140
|
+
<div className="px-3 pb-3 pt-1 border-t border-slate-100 space-y-2">
|
|
141
|
+
{story?.description && (
|
|
142
|
+
<div>
|
|
143
|
+
<p className="text-xs font-medium text-slate-500 mb-0.5">Description</p>
|
|
144
|
+
<p className="text-xs text-slate-700 leading-relaxed">{story.description}</p>
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
{acs.length > 0 && (
|
|
148
|
+
<div>
|
|
149
|
+
<p className="text-xs font-medium text-slate-500 mb-0.5">Acceptance Criteria</p>
|
|
150
|
+
<ul className="space-y-0.5">
|
|
151
|
+
{acs.map((ac, i) => (
|
|
152
|
+
<li key={i} className="flex items-start gap-1 text-xs text-slate-700">
|
|
153
|
+
<span className="text-slate-400 mt-0.5 flex-shrink-0">•</span>
|
|
154
|
+
<span>{ac}</span>
|
|
155
|
+
</li>
|
|
156
|
+
))}
|
|
157
|
+
</ul>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const SEVERITY_COLOR = {
|
|
167
|
+
critical: 'text-red-600',
|
|
168
|
+
major: 'text-orange-600',
|
|
169
|
+
minor: 'text-amber-600',
|
|
170
|
+
};
|
|
171
|
+
const SEVERITY_BG = {
|
|
172
|
+
critical: 'bg-red-50 border-red-100',
|
|
173
|
+
major: 'bg-orange-50 border-orange-100',
|
|
174
|
+
minor: 'bg-amber-50 border-amber-100',
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
function scoreBadgeClass(score) {
|
|
178
|
+
if (score >= 95) return 'bg-green-100 text-green-700';
|
|
179
|
+
if (score >= 80) return 'bg-amber-100 text-amber-700';
|
|
180
|
+
return 'bg-red-100 text-red-700';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* RefineWorkItemPopup
|
|
185
|
+
* Three-phase popup:
|
|
186
|
+
* configure → running (while LLM runs) → results (accept / discard)
|
|
187
|
+
*
|
|
188
|
+
* Props:
|
|
189
|
+
* item - full work item from CardDetailModal (includes metadata.validationResult)
|
|
190
|
+
* refineProgress - { itemId, jobId, message } from WS, or null
|
|
191
|
+
* refineResult - { itemId, jobId, result } from WS, or null
|
|
192
|
+
* refineError - { itemId, jobId, error } from WS, or null
|
|
193
|
+
* onClose() - close popup without accepting
|
|
194
|
+
* onAccepted() - called after successful apply (triggers detail reload)
|
|
195
|
+
*/
|
|
196
|
+
export function RefineWorkItemPopup({
|
|
197
|
+
item,
|
|
198
|
+
refineProgress,
|
|
199
|
+
refineResult,
|
|
200
|
+
refineError,
|
|
201
|
+
onClose,
|
|
202
|
+
onAccepted,
|
|
203
|
+
}) {
|
|
204
|
+
// ── Configure state ─────────────────────────────────────────────────────────
|
|
205
|
+
const vr = item?.metadata?.validationResult;
|
|
206
|
+
const allIssues = [
|
|
207
|
+
...(vr?.criticalIssues || []).map((i) => ({ ...i, severity: 'critical' })),
|
|
208
|
+
...(vr?.majorIssues || []).map((i) => ({ ...i, severity: 'major' })),
|
|
209
|
+
...(vr?.minorIssues || []).map((i) => ({ ...i, severity: 'minor' })),
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
// Pre-select all critical issues
|
|
213
|
+
const [selectedIssueIndices, setSelectedIssueIndices] = useState(
|
|
214
|
+
() => new Set(
|
|
215
|
+
allIssues
|
|
216
|
+
.map((issue, i) => (issue.severity === 'critical' ? i : null))
|
|
217
|
+
.filter((i) => i !== null)
|
|
218
|
+
)
|
|
219
|
+
);
|
|
220
|
+
const [refinementRequest, setRefinementRequest] = useState('');
|
|
221
|
+
const [models, setModels] = useState([]);
|
|
222
|
+
const [selectedModelId, setSelectedModelId] = useState('');
|
|
223
|
+
const [selectedValidatorModelId, setSelectedValidatorModelId] = useState('');
|
|
224
|
+
const [configError, setConfigError] = useState('');
|
|
225
|
+
|
|
226
|
+
// Local "refine started, waiting for first WS progress" state
|
|
227
|
+
const [refining, setRefining] = useState(false);
|
|
228
|
+
|
|
229
|
+
// ── Running state ───────────────────────────────────────────────────────────
|
|
230
|
+
const [progressLog, setProgressLog] = useState([]);
|
|
231
|
+
const progressEndRef = useRef(null);
|
|
232
|
+
|
|
233
|
+
// ── Results state ───────────────────────────────────────────────────────────
|
|
234
|
+
const [storyCheckboxes, setStoryCheckboxes] = useState(null); // { [storyImpactIndex]: boolean }
|
|
235
|
+
const [accepting, setAccepting] = useState(false);
|
|
236
|
+
const [acceptError, setAcceptError] = useState('');
|
|
237
|
+
|
|
238
|
+
// ── Phase derivation ────────────────────────────────────────────────────────
|
|
239
|
+
const phase = refineResult
|
|
240
|
+
? 'results'
|
|
241
|
+
: refining || refineProgress
|
|
242
|
+
? 'running'
|
|
243
|
+
: 'configure';
|
|
244
|
+
|
|
245
|
+
// ── Effects ─────────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
// Load models on mount
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
Promise.all([getModels(), getSettings()])
|
|
250
|
+
.then(([data, settings]) => {
|
|
251
|
+
setModels(data);
|
|
252
|
+
const ready = data.filter((m) => settings.apiKeys?.[normalizeProvider(m.provider)]?.isSet);
|
|
253
|
+
const isPro = (id) => /pro|opus|sonnet/i.test(id);
|
|
254
|
+
const isLite = (id) => /flash|lite|haiku|mini/i.test(id);
|
|
255
|
+
const generator = ready.find((m) => isPro(m.modelId)) || ready[0];
|
|
256
|
+
const genId = generator ? generator.modelId : (data.length > 0 ? data[0].modelId : '');
|
|
257
|
+
setSelectedModelId(genId);
|
|
258
|
+
const validator = ready.find((m) => isLite(m.modelId) && m.modelId !== genId)
|
|
259
|
+
|| ready.find((m) => m.modelId !== genId)
|
|
260
|
+
|| generator;
|
|
261
|
+
setSelectedValidatorModelId(validator ? validator.modelId : genId);
|
|
262
|
+
})
|
|
263
|
+
.catch(() => setConfigError('Failed to load models.'));
|
|
264
|
+
}, []);
|
|
265
|
+
|
|
266
|
+
// Accumulate progress messages
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
if (refineProgress?.message) {
|
|
269
|
+
setProgressLog((prev) => [...prev, refineProgress.message]);
|
|
270
|
+
}
|
|
271
|
+
}, [refineProgress]);
|
|
272
|
+
|
|
273
|
+
// Auto-scroll progress log
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
progressEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
276
|
+
}, [progressLog]);
|
|
277
|
+
|
|
278
|
+
// When result arrives, reset refining and initialise story checkboxes
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
if (!refineResult) return;
|
|
281
|
+
setRefining(false);
|
|
282
|
+
const impacts = refineResult.result?.storyImpacts || [];
|
|
283
|
+
const init = {};
|
|
284
|
+
impacts.forEach((impact, i) => {
|
|
285
|
+
// Pre-check: all impacted updates and all new stories
|
|
286
|
+
init[i] = impact.type === 'new' || impact.impacted === true;
|
|
287
|
+
});
|
|
288
|
+
setStoryCheckboxes(init);
|
|
289
|
+
}, [refineResult]);
|
|
290
|
+
|
|
291
|
+
// When error arrives, go back to configure with the error message shown
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
if (!refineError) return;
|
|
294
|
+
setRefining(false);
|
|
295
|
+
setConfigError(
|
|
296
|
+
typeof refineError.error === 'string'
|
|
297
|
+
? refineError.error
|
|
298
|
+
: 'Refinement failed — please try again.'
|
|
299
|
+
);
|
|
300
|
+
}, [refineError]);
|
|
301
|
+
|
|
302
|
+
// ── Handlers ─────────────────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
function toggleIssue(i) {
|
|
305
|
+
setSelectedIssueIndices((prev) => {
|
|
306
|
+
const next = new Set(prev);
|
|
307
|
+
if (next.has(i)) next.delete(i);
|
|
308
|
+
else next.add(i);
|
|
309
|
+
return next;
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function handleRefine() {
|
|
314
|
+
const selectedModel = models.find((m) => m.modelId === selectedModelId);
|
|
315
|
+
const selectedValidatorModel = models.find(
|
|
316
|
+
(m) => m.modelId === selectedValidatorModelId
|
|
317
|
+
);
|
|
318
|
+
if (!selectedModel || !selectedValidatorModel) return;
|
|
319
|
+
|
|
320
|
+
const selectedIssues = allIssues.filter((_, i) => selectedIssueIndices.has(i));
|
|
321
|
+
|
|
322
|
+
setRefining(true);
|
|
323
|
+
setConfigError('');
|
|
324
|
+
setProgressLog([]);
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
await refineWorkItem(item.id, {
|
|
328
|
+
refinementRequest,
|
|
329
|
+
selectedIssues,
|
|
330
|
+
modelId: selectedModelId,
|
|
331
|
+
provider: selectedModel.provider,
|
|
332
|
+
validatorModelId: selectedValidatorModelId,
|
|
333
|
+
validatorProvider: selectedValidatorModel.provider,
|
|
334
|
+
});
|
|
335
|
+
// Job started — WebSocket broadcasts refine:progress / refine:complete / refine:error
|
|
336
|
+
} catch (err) {
|
|
337
|
+
setRefining(false);
|
|
338
|
+
setConfigError(err.message || 'Failed to start refinement.');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function handleAccept() {
|
|
343
|
+
if (!refineResult) return;
|
|
344
|
+
const { proposedItem, storyImpacts = [] } = refineResult.result;
|
|
345
|
+
|
|
346
|
+
const acceptedStoryChanges = storyImpacts
|
|
347
|
+
.filter((_, i) => storyCheckboxes?.[i])
|
|
348
|
+
.map((impact) => ({
|
|
349
|
+
type: impact.type,
|
|
350
|
+
storyId: impact.storyId ?? null,
|
|
351
|
+
proposedStory: impact.proposedStory,
|
|
352
|
+
}));
|
|
353
|
+
|
|
354
|
+
setAccepting(true);
|
|
355
|
+
setAcceptError('');
|
|
356
|
+
try {
|
|
357
|
+
await applyWorkItemChanges(item.id, proposedItem, acceptedStoryChanges);
|
|
358
|
+
onAccepted?.();
|
|
359
|
+
} catch (err) {
|
|
360
|
+
setAcceptError(err.message || 'Failed to apply changes.');
|
|
361
|
+
setAccepting(false);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ── Derived values ──────────────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
const selectedModel = models.find((m) => m.modelId === selectedModelId);
|
|
368
|
+
const selectedValidatorModel = models.find(
|
|
369
|
+
(m) => m.modelId === selectedValidatorModelId
|
|
370
|
+
);
|
|
371
|
+
const canRefine =
|
|
372
|
+
!refining &&
|
|
373
|
+
!!selectedModelId &&
|
|
374
|
+
!!selectedValidatorModelId &&
|
|
375
|
+
!!selectedModel &&
|
|
376
|
+
!!selectedValidatorModel;
|
|
377
|
+
const selectedIssueCount = selectedIssueIndices.size;
|
|
378
|
+
|
|
379
|
+
// Results phase derived values
|
|
380
|
+
const resultData = refineResult?.result;
|
|
381
|
+
const storyImpacts = resultData?.storyImpacts || [];
|
|
382
|
+
const updateImpacts = storyImpacts
|
|
383
|
+
.map((impact, i) => ({ impact, i }))
|
|
384
|
+
.filter(({ impact }) => impact.type === 'update' && impact.impacted);
|
|
385
|
+
const newImpacts = storyImpacts
|
|
386
|
+
.map((impact, i) => ({ impact, i }))
|
|
387
|
+
.filter(({ impact }) => impact.type === 'new');
|
|
388
|
+
|
|
389
|
+
const checkedStoryCount = Object.values(storyCheckboxes || {}).filter(Boolean).length;
|
|
390
|
+
const isEpic = item.type === 'epic';
|
|
391
|
+
|
|
392
|
+
// ── Render ──────────────────────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
|
+
<div
|
|
396
|
+
className="fixed inset-0 z-[80] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
|
|
397
|
+
onClick={(e) => {
|
|
398
|
+
if (e.target === e.currentTarget && phase !== 'running') onClose();
|
|
399
|
+
}}
|
|
400
|
+
>
|
|
401
|
+
<div
|
|
402
|
+
className={`bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden w-full transition-all duration-200 ${
|
|
403
|
+
phase === 'results' ? 'max-w-2xl' : 'max-w-xl'
|
|
404
|
+
}`}
|
|
405
|
+
style={{ maxHeight: '88vh' }}
|
|
406
|
+
>
|
|
407
|
+
{/* ── Header ─────────────────────────────────────────────────────────── */}
|
|
408
|
+
<div className="flex items-center justify-between px-5 py-3 border-b border-slate-100 flex-shrink-0">
|
|
409
|
+
<div>
|
|
410
|
+
<h3 className="text-sm font-semibold text-slate-900 flex items-center gap-1.5">
|
|
411
|
+
<Wand2 className="w-3.5 h-3.5 text-violet-600" />
|
|
412
|
+
Refine with AI
|
|
413
|
+
{phase !== 'configure' && (
|
|
414
|
+
<span
|
|
415
|
+
className={`ml-1 text-xs font-medium px-1.5 py-0.5 rounded ${
|
|
416
|
+
phase === 'running'
|
|
417
|
+
? 'bg-blue-100 text-blue-700'
|
|
418
|
+
: 'bg-violet-100 text-violet-700'
|
|
419
|
+
}`}
|
|
420
|
+
>
|
|
421
|
+
{phase === 'running' ? 'Running…' : 'Results'}
|
|
422
|
+
</span>
|
|
423
|
+
)}
|
|
424
|
+
</h3>
|
|
425
|
+
<p className="text-xs text-slate-400 mt-0.5 truncate max-w-xs">{item.name}</p>
|
|
426
|
+
</div>
|
|
427
|
+
{phase !== 'running' && (
|
|
428
|
+
<button
|
|
429
|
+
type="button"
|
|
430
|
+
onClick={onClose}
|
|
431
|
+
className="text-slate-400 hover:text-slate-600 transition-colors ml-4 flex-shrink-0"
|
|
432
|
+
aria-label="Close"
|
|
433
|
+
>
|
|
434
|
+
<X className="w-4 h-4" />
|
|
435
|
+
</button>
|
|
436
|
+
)}
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
{/* ── CONFIGURE PHASE ────────────────────────────────────────────────── */}
|
|
440
|
+
{phase === 'configure' && (
|
|
441
|
+
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-4 min-h-0">
|
|
442
|
+
{/* Issues checklist */}
|
|
443
|
+
{allIssues.length > 0 ? (
|
|
444
|
+
<div>
|
|
445
|
+
<div className="flex items-center justify-between mb-2">
|
|
446
|
+
<p className="text-xs font-semibold text-slate-600 uppercase tracking-wide">
|
|
447
|
+
Validation Issues ({allIssues.length})
|
|
448
|
+
</p>
|
|
449
|
+
<div className="flex items-center gap-2 text-xs">
|
|
450
|
+
<button
|
|
451
|
+
type="button"
|
|
452
|
+
onClick={() =>
|
|
453
|
+
setSelectedIssueIndices(
|
|
454
|
+
new Set(allIssues.map((_, i) => i))
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
className="text-violet-600 hover:text-violet-800 transition-colors"
|
|
458
|
+
>
|
|
459
|
+
All
|
|
460
|
+
</button>
|
|
461
|
+
<span className="text-slate-300">|</span>
|
|
462
|
+
<button
|
|
463
|
+
type="button"
|
|
464
|
+
onClick={() => setSelectedIssueIndices(new Set())}
|
|
465
|
+
className="text-slate-400 hover:text-slate-600 transition-colors"
|
|
466
|
+
>
|
|
467
|
+
None
|
|
468
|
+
</button>
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
<ul className="space-y-1.5 max-h-52 overflow-y-auto pr-0.5">
|
|
472
|
+
{allIssues.map((issue, i) => (
|
|
473
|
+
<li
|
|
474
|
+
key={i}
|
|
475
|
+
onClick={() => toggleIssue(i)}
|
|
476
|
+
className={`rounded-lg border px-3 py-2 cursor-pointer flex items-start gap-2.5 transition-shadow ${
|
|
477
|
+
selectedIssueIndices.has(i) ? 'ring-2 ring-violet-400' : ''
|
|
478
|
+
} ${SEVERITY_BG[issue.severity] ?? 'bg-slate-50 border-slate-100'}`}
|
|
479
|
+
>
|
|
480
|
+
<input
|
|
481
|
+
type="checkbox"
|
|
482
|
+
checked={selectedIssueIndices.has(i)}
|
|
483
|
+
onChange={() => toggleIssue(i)}
|
|
484
|
+
onClick={(e) => e.stopPropagation()}
|
|
485
|
+
className="mt-0.5 flex-shrink-0 accent-violet-600"
|
|
486
|
+
/>
|
|
487
|
+
<div className="flex-1 min-w-0">
|
|
488
|
+
<span
|
|
489
|
+
className={`text-xs font-semibold uppercase ${
|
|
490
|
+
SEVERITY_COLOR[issue.severity] ?? 'text-slate-500'
|
|
491
|
+
}`}
|
|
492
|
+
>
|
|
493
|
+
{issue.severity}
|
|
494
|
+
</span>
|
|
495
|
+
<p className="text-xs text-slate-800 mt-0.5 leading-snug">
|
|
496
|
+
{issue.description}
|
|
497
|
+
</p>
|
|
498
|
+
{issue.suggestion && (
|
|
499
|
+
<p className="text-xs text-slate-500 mt-0.5 leading-snug">
|
|
500
|
+
<span className="font-medium">Suggestion:</span>{' '}
|
|
501
|
+
{issue.suggestion}
|
|
502
|
+
</p>
|
|
503
|
+
)}
|
|
504
|
+
</div>
|
|
505
|
+
</li>
|
|
506
|
+
))}
|
|
507
|
+
</ul>
|
|
508
|
+
</div>
|
|
509
|
+
) : (
|
|
510
|
+
<div className="flex items-center gap-2 px-3 py-2.5 bg-green-50 border border-green-100 rounded-lg">
|
|
511
|
+
<Check className="w-3.5 h-3.5 text-green-600 flex-shrink-0" />
|
|
512
|
+
<p className="text-xs text-green-700">
|
|
513
|
+
No validation issues found — you can still refine with a custom request.
|
|
514
|
+
</p>
|
|
515
|
+
</div>
|
|
516
|
+
)}
|
|
517
|
+
|
|
518
|
+
{/* Free-text request */}
|
|
519
|
+
<div>
|
|
520
|
+
<label className="block text-xs font-medium text-slate-500 mb-1">
|
|
521
|
+
Refinement request{' '}
|
|
522
|
+
<span className="text-slate-400">(optional)</span>
|
|
523
|
+
</label>
|
|
524
|
+
<textarea
|
|
525
|
+
value={refinementRequest}
|
|
526
|
+
onChange={(e) => setRefinementRequest(e.target.value)}
|
|
527
|
+
rows={3}
|
|
528
|
+
placeholder={
|
|
529
|
+
isEpic
|
|
530
|
+
? 'E.g. Sharpen the scope, add examples, expand the features list…'
|
|
531
|
+
: 'E.g. Make acceptance criteria more testable, add edge cases…'
|
|
532
|
+
}
|
|
533
|
+
className="w-full rounded-md border border-slate-300 px-2 py-1.5 text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500 resize-none"
|
|
534
|
+
/>
|
|
535
|
+
</div>
|
|
536
|
+
|
|
537
|
+
{/* Model selectors */}
|
|
538
|
+
<div className="grid grid-cols-2 gap-3">
|
|
539
|
+
<ModelSelect
|
|
540
|
+
label="Generator Model"
|
|
541
|
+
value={selectedModelId}
|
|
542
|
+
onChange={setSelectedModelId}
|
|
543
|
+
models={models}
|
|
544
|
+
disabled={false}
|
|
545
|
+
/>
|
|
546
|
+
<ModelSelect
|
|
547
|
+
label="Validator Model"
|
|
548
|
+
value={selectedValidatorModelId}
|
|
549
|
+
onChange={setSelectedValidatorModelId}
|
|
550
|
+
models={models}
|
|
551
|
+
disabled={false}
|
|
552
|
+
/>
|
|
553
|
+
</div>
|
|
554
|
+
|
|
555
|
+
{configError && (
|
|
556
|
+
<div className="flex items-start gap-2 px-3 py-2 bg-red-50 border border-red-100 rounded-lg">
|
|
557
|
+
<AlertTriangle className="w-3.5 h-3.5 text-red-500 flex-shrink-0 mt-0.5" />
|
|
558
|
+
<p className="text-xs text-red-700">{configError}</p>
|
|
559
|
+
</div>
|
|
560
|
+
)}
|
|
561
|
+
</div>
|
|
562
|
+
)}
|
|
563
|
+
|
|
564
|
+
{/* ── RUNNING PHASE ──────────────────────────────────────────────────── */}
|
|
565
|
+
{phase === 'running' && (
|
|
566
|
+
<div className="flex-1 flex flex-col overflow-hidden px-5 py-4 min-h-0">
|
|
567
|
+
<div className="flex flex-col items-center gap-3 mb-4 flex-shrink-0">
|
|
568
|
+
<span
|
|
569
|
+
className="w-8 h-8 border-2 border-violet-200 border-t-violet-600 rounded-full animate-spin"
|
|
570
|
+
/>
|
|
571
|
+
<p className="text-sm font-medium text-slate-700">
|
|
572
|
+
Refining {item.type}…
|
|
573
|
+
</p>
|
|
574
|
+
</div>
|
|
575
|
+
<div className="flex-1 overflow-y-auto bg-slate-50 rounded-lg px-3 py-2.5 space-y-1 min-h-0">
|
|
576
|
+
{progressLog.length === 0 ? (
|
|
577
|
+
<p className="text-xs text-slate-400 italic">Starting…</p>
|
|
578
|
+
) : (
|
|
579
|
+
progressLog.map((msg, i) => (
|
|
580
|
+
<p key={i} className="text-xs text-slate-600 leading-snug">
|
|
581
|
+
{msg}
|
|
582
|
+
</p>
|
|
583
|
+
))
|
|
584
|
+
)}
|
|
585
|
+
<div ref={progressEndRef} />
|
|
586
|
+
</div>
|
|
587
|
+
</div>
|
|
588
|
+
)}
|
|
589
|
+
|
|
590
|
+
{/* ── RESULTS PHASE ──────────────────────────────────────────────────── */}
|
|
591
|
+
{phase === 'results' && resultData && (
|
|
592
|
+
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-5 min-h-0">
|
|
593
|
+
{/* Score comparison */}
|
|
594
|
+
{(() => {
|
|
595
|
+
const oldScore = item?.metadata?.validationResult?.averageScore;
|
|
596
|
+
const newScore =
|
|
597
|
+
resultData.validationResult?.averageScore ??
|
|
598
|
+
resultData.proposedItem?.metadata?.validationResult?.averageScore;
|
|
599
|
+
if (newScore == null) return null;
|
|
600
|
+
return (
|
|
601
|
+
<div className="flex items-center gap-3">
|
|
602
|
+
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
|
603
|
+
Validation Score
|
|
604
|
+
</p>
|
|
605
|
+
<div className="flex items-center gap-2">
|
|
606
|
+
{oldScore != null && (
|
|
607
|
+
<>
|
|
608
|
+
<span
|
|
609
|
+
className={`text-xs font-bold px-2 py-0.5 rounded-full ${scoreBadgeClass(oldScore)}`}
|
|
610
|
+
>
|
|
611
|
+
{oldScore}/100
|
|
612
|
+
</span>
|
|
613
|
+
<span className="text-slate-400 text-xs">→</span>
|
|
614
|
+
</>
|
|
615
|
+
)}
|
|
616
|
+
<span
|
|
617
|
+
className={`text-xs font-bold px-2 py-0.5 rounded-full ${scoreBadgeClass(newScore)}`}
|
|
618
|
+
>
|
|
619
|
+
{newScore}/100
|
|
620
|
+
</span>
|
|
621
|
+
</div>
|
|
622
|
+
</div>
|
|
623
|
+
);
|
|
624
|
+
})()}
|
|
625
|
+
|
|
626
|
+
{/* Field diffs */}
|
|
627
|
+
<div>
|
|
628
|
+
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">
|
|
629
|
+
Proposed Changes
|
|
630
|
+
</p>
|
|
631
|
+
<FieldDiff
|
|
632
|
+
label="Description"
|
|
633
|
+
before={resultData.originalItem?.description}
|
|
634
|
+
after={resultData.proposedItem?.description}
|
|
635
|
+
/>
|
|
636
|
+
{isEpic && (
|
|
637
|
+
<FieldDiff
|
|
638
|
+
label="Features"
|
|
639
|
+
before={resultData.originalItem?.features}
|
|
640
|
+
after={resultData.proposedItem?.features}
|
|
641
|
+
/>
|
|
642
|
+
)}
|
|
643
|
+
{!isEpic && (
|
|
644
|
+
<FieldDiff
|
|
645
|
+
label="Acceptance Criteria"
|
|
646
|
+
before={resultData.originalItem?.acceptance ?? resultData.originalItem?.acceptanceCriteria}
|
|
647
|
+
after={resultData.proposedItem?.acceptance ?? resultData.proposedItem?.acceptanceCriteria}
|
|
648
|
+
/>
|
|
649
|
+
)}
|
|
650
|
+
{resultData.originalItem?.description ===
|
|
651
|
+
resultData.proposedItem?.description &&
|
|
652
|
+
!resultData.originalItem?.features &&
|
|
653
|
+
!resultData.originalItem?.acceptanceCriteria && (
|
|
654
|
+
<p className="text-xs text-slate-400 italic">
|
|
655
|
+
No textual changes detected — check dependencies or metadata.
|
|
656
|
+
</p>
|
|
657
|
+
)}
|
|
658
|
+
</div>
|
|
659
|
+
|
|
660
|
+
{/* Existing stories to update */}
|
|
661
|
+
{updateImpacts.length > 0 && (
|
|
662
|
+
<div>
|
|
663
|
+
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">
|
|
664
|
+
Existing Stories to Update ({updateImpacts.length})
|
|
665
|
+
</p>
|
|
666
|
+
<div className="space-y-2">
|
|
667
|
+
{updateImpacts.map(({ impact, i }) => (
|
|
668
|
+
<StoryUpdateCard
|
|
669
|
+
key={i}
|
|
670
|
+
impact={impact}
|
|
671
|
+
checked={storyCheckboxes?.[i] ?? true}
|
|
672
|
+
onToggle={() =>
|
|
673
|
+
setStoryCheckboxes((prev) => ({
|
|
674
|
+
...prev,
|
|
675
|
+
[i]: !prev?.[i],
|
|
676
|
+
}))
|
|
677
|
+
}
|
|
678
|
+
/>
|
|
679
|
+
))}
|
|
680
|
+
</div>
|
|
681
|
+
</div>
|
|
682
|
+
)}
|
|
683
|
+
|
|
684
|
+
{/* New stories to add */}
|
|
685
|
+
{newImpacts.length > 0 && (
|
|
686
|
+
<div>
|
|
687
|
+
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">
|
|
688
|
+
New Stories to Add ({newImpacts.length})
|
|
689
|
+
</p>
|
|
690
|
+
<div className="space-y-2">
|
|
691
|
+
{newImpacts.map(({ impact, i }) => (
|
|
692
|
+
<NewStoryCard
|
|
693
|
+
key={i}
|
|
694
|
+
story={impact.proposedStory}
|
|
695
|
+
checked={storyCheckboxes?.[i] ?? true}
|
|
696
|
+
onToggle={() =>
|
|
697
|
+
setStoryCheckboxes((prev) => ({
|
|
698
|
+
...prev,
|
|
699
|
+
[i]: !prev?.[i],
|
|
700
|
+
}))
|
|
701
|
+
}
|
|
702
|
+
/>
|
|
703
|
+
))}
|
|
704
|
+
</div>
|
|
705
|
+
</div>
|
|
706
|
+
)}
|
|
707
|
+
|
|
708
|
+
{acceptError && (
|
|
709
|
+
<div className="flex items-start gap-2 px-3 py-2 bg-red-50 border border-red-100 rounded-lg">
|
|
710
|
+
<AlertTriangle className="w-3.5 h-3.5 text-red-500 flex-shrink-0 mt-0.5" />
|
|
711
|
+
<p className="text-xs text-red-700">{acceptError}</p>
|
|
712
|
+
</div>
|
|
713
|
+
)}
|
|
714
|
+
</div>
|
|
715
|
+
)}
|
|
716
|
+
|
|
717
|
+
{/* ── Footer ─────────────────────────────────────────────────────────── */}
|
|
718
|
+
<div className="px-5 py-3 border-t border-slate-100 flex-shrink-0 flex items-center justify-between gap-2">
|
|
719
|
+
{phase === 'configure' && (
|
|
720
|
+
<>
|
|
721
|
+
<button
|
|
722
|
+
type="button"
|
|
723
|
+
onClick={onClose}
|
|
724
|
+
className="px-4 py-1.5 text-xs text-slate-600 border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors"
|
|
725
|
+
>
|
|
726
|
+
Cancel
|
|
727
|
+
</button>
|
|
728
|
+
<button
|
|
729
|
+
type="button"
|
|
730
|
+
onClick={handleRefine}
|
|
731
|
+
disabled={!canRefine}
|
|
732
|
+
className="flex items-center gap-1.5 px-4 py-1.5 text-xs font-medium text-white bg-violet-600 rounded-lg hover:bg-violet-700 transition-colors disabled:opacity-40"
|
|
733
|
+
>
|
|
734
|
+
<Wand2 className="w-3.5 h-3.5" />
|
|
735
|
+
{selectedIssueCount > 0
|
|
736
|
+
? `Refine (${selectedIssueCount} issue${selectedIssueCount !== 1 ? 's' : ''})`
|
|
737
|
+
: 'Refine'}
|
|
738
|
+
</button>
|
|
739
|
+
</>
|
|
740
|
+
)}
|
|
741
|
+
|
|
742
|
+
{phase === 'running' && (
|
|
743
|
+
<p className="w-full text-center text-xs text-slate-400 italic">
|
|
744
|
+
Waiting for LLM response — please keep this window open.
|
|
745
|
+
</p>
|
|
746
|
+
)}
|
|
747
|
+
|
|
748
|
+
{phase === 'results' && (
|
|
749
|
+
<>
|
|
750
|
+
<button
|
|
751
|
+
type="button"
|
|
752
|
+
onClick={onClose}
|
|
753
|
+
disabled={accepting}
|
|
754
|
+
className="px-4 py-1.5 text-xs text-slate-600 border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors disabled:opacity-40"
|
|
755
|
+
>
|
|
756
|
+
Discard
|
|
757
|
+
</button>
|
|
758
|
+
<button
|
|
759
|
+
type="button"
|
|
760
|
+
onClick={handleAccept}
|
|
761
|
+
disabled={accepting}
|
|
762
|
+
className="flex items-center gap-1.5 px-4 py-1.5 text-xs font-medium text-white bg-violet-600 rounded-lg hover:bg-violet-700 transition-colors disabled:opacity-40"
|
|
763
|
+
>
|
|
764
|
+
{accepting ? (
|
|
765
|
+
<>
|
|
766
|
+
<span className="w-3 h-3 border border-white/40 border-t-white rounded-full animate-spin" />
|
|
767
|
+
Applying…
|
|
768
|
+
</>
|
|
769
|
+
) : (
|
|
770
|
+
<>
|
|
771
|
+
<Check className="w-3.5 h-3.5" />
|
|
772
|
+
{checkedStoryCount > 0
|
|
773
|
+
? `Accept + ${checkedStoryCount} story change${checkedStoryCount !== 1 ? 's' : ''}`
|
|
774
|
+
: 'Accept Changes'}
|
|
775
|
+
</>
|
|
776
|
+
)}
|
|
777
|
+
</button>
|
|
778
|
+
</>
|
|
779
|
+
)}
|
|
780
|
+
</div>
|
|
781
|
+
</div>
|
|
782
|
+
</div>
|
|
783
|
+
);
|
|
784
|
+
}
|