@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.
Files changed (239) hide show
  1. package/cli/agent-loader.js +21 -0
  2. package/cli/agents/agent-selector.md +152 -0
  3. package/cli/agents/architecture-recommender.md +418 -0
  4. package/cli/agents/code-implementer.md +117 -0
  5. package/cli/agents/code-validator.md +80 -0
  6. package/cli/agents/context-reviewer-epic.md +101 -0
  7. package/cli/agents/context-reviewer-story.md +92 -0
  8. package/cli/agents/context-writer-epic.md +145 -0
  9. package/cli/agents/context-writer-story.md +111 -0
  10. package/cli/agents/database-deep-dive.md +470 -0
  11. package/cli/agents/database-recommender.md +634 -0
  12. package/cli/agents/doc-distributor.md +176 -0
  13. package/cli/agents/doc-writer-epic.md +42 -0
  14. package/cli/agents/doc-writer-story.md +43 -0
  15. package/cli/agents/documentation-updater.md +203 -0
  16. package/cli/agents/duplicate-detector.md +110 -0
  17. package/cli/agents/epic-story-decomposer.md +559 -0
  18. package/cli/agents/feature-context-generator.md +91 -0
  19. package/cli/agents/gap-checker-epic.md +52 -0
  20. package/cli/agents/impact-checker-story.md +51 -0
  21. package/cli/agents/migration-guide-generator.md +305 -0
  22. package/cli/agents/mission-scope-generator.md +143 -0
  23. package/cli/agents/mission-scope-validator.md +146 -0
  24. package/cli/agents/project-context-extractor.md +122 -0
  25. package/cli/agents/project-documentation-creator.json +226 -0
  26. package/cli/agents/project-documentation-creator.md +595 -0
  27. package/cli/agents/question-prefiller.md +269 -0
  28. package/cli/agents/refiner-epic.md +39 -0
  29. package/cli/agents/refiner-story.md +42 -0
  30. package/cli/agents/scaffolding-generator.md +99 -0
  31. package/cli/agents/seed-validator.md +71 -0
  32. package/cli/agents/story-doc-enricher.md +133 -0
  33. package/cli/agents/story-scope-reviewer.md +147 -0
  34. package/cli/agents/story-splitter.md +83 -0
  35. package/cli/agents/suggestion-business-analyst.md +88 -0
  36. package/cli/agents/suggestion-deployment-architect.md +263 -0
  37. package/cli/agents/suggestion-product-manager.md +129 -0
  38. package/cli/agents/suggestion-security-specialist.md +156 -0
  39. package/cli/agents/suggestion-technical-architect.md +269 -0
  40. package/cli/agents/suggestion-ux-researcher.md +93 -0
  41. package/cli/agents/task-subtask-decomposer.md +188 -0
  42. package/cli/agents/validator-documentation.json +183 -0
  43. package/cli/agents/validator-documentation.md +455 -0
  44. package/cli/agents/validator-selector.md +211 -0
  45. package/cli/ansi-colors.js +21 -0
  46. package/cli/api-reference-tool.js +368 -0
  47. package/cli/build-docs.js +29 -8
  48. package/cli/ceremony-history.js +369 -0
  49. package/cli/checks/catalog.json +76 -0
  50. package/cli/checks/code/quality.json +26 -0
  51. package/cli/checks/code/testing.json +14 -0
  52. package/cli/checks/code/traceability.json +26 -0
  53. package/cli/checks/cross-refs/epic.json +171 -0
  54. package/cli/checks/cross-refs/story.json +149 -0
  55. package/cli/checks/epic/api.json +114 -0
  56. package/cli/checks/epic/backend.json +126 -0
  57. package/cli/checks/epic/cloud.json +126 -0
  58. package/cli/checks/epic/data.json +102 -0
  59. package/cli/checks/epic/database.json +114 -0
  60. package/cli/checks/epic/developer.json +182 -0
  61. package/cli/checks/epic/devops.json +174 -0
  62. package/cli/checks/epic/frontend.json +162 -0
  63. package/cli/checks/epic/mobile.json +102 -0
  64. package/cli/checks/epic/qa.json +90 -0
  65. package/cli/checks/epic/security.json +184 -0
  66. package/cli/checks/epic/solution-architect.json +192 -0
  67. package/cli/checks/epic/test-architect.json +90 -0
  68. package/cli/checks/epic/ui.json +102 -0
  69. package/cli/checks/epic/ux.json +90 -0
  70. package/cli/checks/fixes/epic-fix-template.md +10 -0
  71. package/cli/checks/fixes/story-fix-template.md +10 -0
  72. package/cli/checks/story/api.json +186 -0
  73. package/cli/checks/story/backend.json +102 -0
  74. package/cli/checks/story/cloud.json +102 -0
  75. package/cli/checks/story/data.json +210 -0
  76. package/cli/checks/story/database.json +102 -0
  77. package/cli/checks/story/developer.json +168 -0
  78. package/cli/checks/story/devops.json +102 -0
  79. package/cli/checks/story/frontend.json +174 -0
  80. package/cli/checks/story/mobile.json +102 -0
  81. package/cli/checks/story/qa.json +210 -0
  82. package/cli/checks/story/security.json +198 -0
  83. package/cli/checks/story/solution-architect.json +230 -0
  84. package/cli/checks/story/test-architect.json +210 -0
  85. package/cli/checks/story/ui.json +102 -0
  86. package/cli/checks/story/ux.json +102 -0
  87. package/cli/coding-order.js +401 -0
  88. package/cli/command-logger.js +49 -12
  89. package/cli/components/static-output.js +63 -0
  90. package/cli/console-output-manager.js +94 -0
  91. package/cli/dependency-checker.js +72 -0
  92. package/cli/docs-sync.js +306 -0
  93. package/cli/epic-story-validator.js +659 -0
  94. package/cli/evaluation-prompts.js +1008 -0
  95. package/cli/execution-context.js +195 -0
  96. package/cli/generate-summary-table.js +340 -0
  97. package/cli/init-model-config.js +704 -0
  98. package/cli/init.js +1737 -278
  99. package/cli/kanban-server-manager.js +227 -0
  100. package/cli/llm-claude.js +150 -1
  101. package/cli/llm-gemini.js +109 -0
  102. package/cli/llm-local.js +493 -0
  103. package/cli/llm-mock.js +233 -0
  104. package/cli/llm-openai.js +454 -0
  105. package/cli/llm-provider.js +379 -3
  106. package/cli/llm-token-limits.js +211 -0
  107. package/cli/llm-verifier.js +662 -0
  108. package/cli/llm-xiaomi.js +143 -0
  109. package/cli/message-constants.js +49 -0
  110. package/cli/message-manager.js +334 -0
  111. package/cli/message-types.js +96 -0
  112. package/cli/messaging-api.js +291 -0
  113. package/cli/micro-check-fixer.js +335 -0
  114. package/cli/micro-check-runner.js +449 -0
  115. package/cli/micro-check-scorer.js +148 -0
  116. package/cli/micro-check-validator.js +538 -0
  117. package/cli/model-pricing.js +192 -0
  118. package/cli/model-query-engine.js +468 -0
  119. package/cli/model-recommendation-analyzer.js +495 -0
  120. package/cli/model-selector.js +270 -0
  121. package/cli/output-buffer.js +107 -0
  122. package/cli/process-manager.js +73 -2
  123. package/cli/prompt-logger.js +57 -0
  124. package/cli/repl-ink.js +4625 -1094
  125. package/cli/repl-old.js +3 -4
  126. package/cli/seed-processor.js +962 -0
  127. package/cli/sprint-planning-processor.js +4162 -0
  128. package/cli/template-processor.js +2149 -105
  129. package/cli/templates/project.md +25 -8
  130. package/cli/templates/vitepress-config.mts.template +5 -4
  131. package/cli/token-tracker.js +547 -0
  132. package/cli/tools/generate-story-validators.js +317 -0
  133. package/cli/tools/generate-validators.js +669 -0
  134. package/cli/update-checker.js +19 -17
  135. package/cli/update-notifier.js +4 -4
  136. package/cli/validation-router.js +667 -0
  137. package/cli/verification-tracker.js +563 -0
  138. package/cli/worktree-runner.js +654 -0
  139. package/kanban/README.md +386 -0
  140. package/kanban/client/README.md +205 -0
  141. package/kanban/client/components.json +20 -0
  142. package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
  143. package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
  144. package/kanban/client/dist/index.html +16 -0
  145. package/kanban/client/dist/vite.svg +1 -0
  146. package/kanban/client/index.html +15 -0
  147. package/kanban/client/package-lock.json +9442 -0
  148. package/kanban/client/package.json +44 -0
  149. package/kanban/client/postcss.config.js +6 -0
  150. package/kanban/client/public/vite.svg +1 -0
  151. package/kanban/client/src/App.jsx +651 -0
  152. package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
  153. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +420 -0
  154. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +629 -0
  155. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +1133 -0
  156. package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
  157. package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
  158. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +686 -0
  159. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +838 -0
  160. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
  161. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +136 -0
  162. package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
  163. package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
  164. package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
  165. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +329 -0
  166. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +249 -0
  167. package/kanban/client/src/components/kanban/CardDetailModal.jsx +646 -0
  168. package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
  169. package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
  170. package/kanban/client/src/components/kanban/GroupingSelector.jsx +63 -0
  171. package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
  172. package/kanban/client/src/components/kanban/KanbanCard.jsx +147 -0
  173. package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
  174. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +784 -0
  175. package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
  176. package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
  177. package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
  178. package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
  179. package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
  180. package/kanban/client/src/components/settings/AgentsTab.jsx +381 -0
  181. package/kanban/client/src/components/settings/ApiKeysTab.jsx +142 -0
  182. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +105 -0
  183. package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
  184. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +95 -0
  185. package/kanban/client/src/components/settings/ModelPricingTab.jsx +269 -0
  186. package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
  187. package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
  188. package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
  189. package/kanban/client/src/components/stats/CostModal.jsx +384 -0
  190. package/kanban/client/src/components/ui/badge.jsx +27 -0
  191. package/kanban/client/src/components/ui/dialog.jsx +121 -0
  192. package/kanban/client/src/components/ui/tabs.jsx +85 -0
  193. package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
  194. package/kanban/client/src/hooks/useGrouping.js +177 -0
  195. package/kanban/client/src/hooks/useWebSocket.js +120 -0
  196. package/kanban/client/src/lib/__tests__/api.test.js +196 -0
  197. package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
  198. package/kanban/client/src/lib/api.js +515 -0
  199. package/kanban/client/src/lib/status-grouping.js +154 -0
  200. package/kanban/client/src/lib/utils.js +11 -0
  201. package/kanban/client/src/main.jsx +10 -0
  202. package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
  203. package/kanban/client/src/store/ceremonyStore.js +172 -0
  204. package/kanban/client/src/store/filterStore.js +201 -0
  205. package/kanban/client/src/store/kanbanStore.js +123 -0
  206. package/kanban/client/src/store/processStore.js +65 -0
  207. package/kanban/client/src/store/sprintPlanningStore.js +33 -0
  208. package/kanban/client/src/styles/globals.css +59 -0
  209. package/kanban/client/tailwind.config.js +77 -0
  210. package/kanban/client/vite.config.js +28 -0
  211. package/kanban/client/vitest.config.js +28 -0
  212. package/kanban/dev-start.sh +47 -0
  213. package/kanban/package.json +12 -0
  214. package/kanban/server/index.js +537 -0
  215. package/kanban/server/routes/ceremony.js +454 -0
  216. package/kanban/server/routes/costs.js +163 -0
  217. package/kanban/server/routes/openai-oauth.js +366 -0
  218. package/kanban/server/routes/processes.js +50 -0
  219. package/kanban/server/routes/settings.js +736 -0
  220. package/kanban/server/routes/websocket.js +281 -0
  221. package/kanban/server/routes/work-items.js +487 -0
  222. package/kanban/server/services/CeremonyService.js +1441 -0
  223. package/kanban/server/services/FileSystemScanner.js +95 -0
  224. package/kanban/server/services/FileWatcher.js +144 -0
  225. package/kanban/server/services/HierarchyBuilder.js +196 -0
  226. package/kanban/server/services/ProcessRegistry.js +122 -0
  227. package/kanban/server/services/TaskRunnerService.js +261 -0
  228. package/kanban/server/services/WorkItemReader.js +123 -0
  229. package/kanban/server/services/WorkItemRefineService.js +510 -0
  230. package/kanban/server/start.js +49 -0
  231. package/kanban/server/utils/kanban-logger.js +132 -0
  232. package/kanban/server/utils/markdown.js +91 -0
  233. package/kanban/server/utils/status-grouping.js +107 -0
  234. package/kanban/server/workers/run-task-worker.js +121 -0
  235. package/kanban/server/workers/seed-worker.js +94 -0
  236. package/kanban/server/workers/sponsor-call-worker.js +92 -0
  237. package/kanban/server/workers/sprint-planning-worker.js +212 -0
  238. package/package.json +19 -7
  239. package/cli/agents/documentation.md +0 -302
@@ -0,0 +1,629 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { AlertTriangle, Settings as SettingsIcon } from 'lucide-react';
3
+ import { getModels, getSettings, generateMission, refineMission } from '../../lib/api';
4
+ import { useCeremonyStore } from '../../store/ceremonyStore';
5
+
6
+ // Maps model provider → apiKeys key returned by getSettings()
7
+ function normalizeProvider(p = '') {
8
+ if (p === 'claude') return 'anthropic';
9
+ return p;
10
+ }
11
+
12
+ const KEY_LABELS = {
13
+ claude: 'Anthropic API Key (ANTHROPIC_API_KEY)',
14
+ anthropic: 'Anthropic API Key (ANTHROPIC_API_KEY)',
15
+ gemini: 'Google Gemini API Key (GEMINI_API_KEY)',
16
+ openai: 'OpenAI API Key (OPENAI_API_KEY)',
17
+ xiaomi: 'Xiaomi MiMo API Key (XIAOMI_API_KEY)',
18
+ };
19
+
20
+ /**
21
+ * IssuesPopup
22
+ * Sub-modal that displays all validation issues with full detail.
23
+ * Supports selecting issues and triggering a targeted refine.
24
+ */
25
+ function IssuesPopup({ issues, onClose, onRefineWithIssues }) {
26
+ const [selectedIssueIndices, setSelectedIssueIndices] = useState(new Set());
27
+
28
+ const severityColor = {
29
+ critical: 'text-red-600',
30
+ major: 'text-orange-600',
31
+ minor: 'text-amber-600',
32
+ };
33
+ const severityBg = {
34
+ critical: 'bg-red-50 border-red-100',
35
+ major: 'bg-orange-50 border-orange-100',
36
+ minor: 'bg-amber-50 border-amber-100',
37
+ };
38
+
39
+ function toggleIssue(i) {
40
+ setSelectedIssueIndices(prev => {
41
+ const next = new Set(prev);
42
+ if (next.has(i)) next.delete(i);
43
+ else next.add(i);
44
+ return next;
45
+ });
46
+ }
47
+
48
+ return (
49
+ <div
50
+ className="fixed inset-0 z-[70] flex items-center justify-center bg-black/40 backdrop-blur-sm p-4"
51
+ onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
52
+ >
53
+ <div className="w-full max-w-lg bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden max-h-[70vh]">
54
+ {/* Header */}
55
+ <div className="flex items-center justify-between px-5 py-3 border-b border-slate-100 flex-shrink-0">
56
+ <div>
57
+ <h3 className="text-sm font-semibold text-slate-900">Validation Issues</h3>
58
+ <p className="text-xs text-slate-400 mt-0.5">{issues.length} issue{issues.length !== 1 ? 's' : ''} found during validation</p>
59
+ </div>
60
+ <button
61
+ type="button"
62
+ onClick={onClose}
63
+ className="text-slate-400 hover:text-slate-600 transition-colors text-xl leading-none ml-4"
64
+ aria-label="Close"
65
+ >
66
+ ×
67
+ </button>
68
+ </div>
69
+
70
+ {/* Issues list */}
71
+ <ul className="flex-1 overflow-y-auto px-4 py-3 space-y-2">
72
+ {issues.map((issue, i) => (
73
+ <li
74
+ key={i}
75
+ onClick={() => toggleIssue(i)}
76
+ className={`rounded-lg border px-3 py-2.5 cursor-pointer flex items-start gap-2.5 ${selectedIssueIndices.has(i) ? 'ring-2 ring-blue-500' : ''} ${severityBg[issue.severity] ?? 'bg-slate-50 border-slate-100'}`}
77
+ >
78
+ <input
79
+ type="checkbox"
80
+ checked={selectedIssueIndices.has(i)}
81
+ onChange={() => toggleIssue(i)}
82
+ onClick={(e) => e.stopPropagation()}
83
+ className="mt-0.5 flex-shrink-0 accent-blue-600"
84
+ />
85
+ <div className="flex-1 min-w-0">
86
+ <div className="flex items-center gap-2 mb-1">
87
+ <span className={`text-xs font-semibold uppercase ${severityColor[issue.severity] ?? 'text-slate-600'}`}>
88
+ {issue.severity}
89
+ </span>
90
+ </div>
91
+ <p className="text-sm text-slate-800 leading-snug">{issue.description}</p>
92
+ {issue.suggestion && (
93
+ <p className="text-xs text-slate-500 mt-1 leading-snug">
94
+ <span className="font-medium">Suggestion:</span> {issue.suggestion}
95
+ </p>
96
+ )}
97
+ </div>
98
+ </li>
99
+ ))}
100
+ </ul>
101
+
102
+ {/* Footer */}
103
+ <div className="px-5 py-3 border-t border-slate-100 flex-shrink-0 flex items-center justify-between gap-2">
104
+ <button
105
+ type="button"
106
+ onClick={() => {
107
+ onRefineWithIssues(issues.filter((_, i) => selectedIssueIndices.has(i)));
108
+ onClose();
109
+ }}
110
+ disabled={selectedIssueIndices.size === 0}
111
+ className="px-4 py-1.5 text-xs text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-40"
112
+ >
113
+ Refine with selected
114
+ </button>
115
+ <button
116
+ type="button"
117
+ onClick={onClose}
118
+ className="px-4 py-1.5 text-xs text-slate-600 border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors"
119
+ >
120
+ Close
121
+ </button>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ );
126
+ }
127
+
128
+ /**
129
+ * RefinePopup
130
+ * Sub-modal for requesting targeted refinements to the generated mission & scope.
131
+ */
132
+ function RefinePopup({ onSubmit, onClose }) {
133
+ const [refinementRequest, setRefinementRequest] = useState('');
134
+
135
+ return (
136
+ <div
137
+ className="fixed inset-0 z-[70] flex items-center justify-center bg-black/40 backdrop-blur-sm p-4"
138
+ onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
139
+ >
140
+ <div className="w-full max-w-md bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden">
141
+ {/* Header */}
142
+ <div className="flex items-center justify-between px-5 py-3 border-b border-slate-100 flex-shrink-0">
143
+ <div>
144
+ <h3 className="text-sm font-semibold text-slate-900">Refine Mission & Scope</h3>
145
+ <p className="text-xs text-slate-400 mt-0.5">Describe what you'd like to change or improve.</p>
146
+ </div>
147
+ <button
148
+ type="button"
149
+ onClick={onClose}
150
+ className="text-slate-400 hover:text-slate-600 transition-colors text-xl leading-none ml-4"
151
+ aria-label="Close"
152
+ >
153
+ ×
154
+ </button>
155
+ </div>
156
+
157
+ {/* Body */}
158
+ <div className="px-5 py-4 flex flex-col gap-3">
159
+ {/* Refinement textarea */}
160
+ <div>
161
+ <label className="block text-xs font-medium text-slate-500 mb-1">
162
+ What would you like to change or improve?
163
+ </label>
164
+ <textarea
165
+ value={refinementRequest}
166
+ onChange={(e) => setRefinementRequest(e.target.value)}
167
+ rows={4}
168
+ placeholder="E.g. Focus more on enterprise teams, make the mission mention mobile specifically…"
169
+ autoFocus
170
+ 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-blue-500 resize-none"
171
+ />
172
+ </div>
173
+ </div>
174
+
175
+ {/* Footer */}
176
+ <div className="px-5 py-3 border-t border-slate-100 flex-shrink-0 flex items-center justify-end gap-2">
177
+ <button
178
+ type="button"
179
+ onClick={onClose}
180
+ className="px-4 py-1.5 text-xs text-slate-600 border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors"
181
+ >
182
+ Cancel
183
+ </button>
184
+ <button
185
+ type="button"
186
+ onClick={() => onSubmit(refinementRequest)}
187
+ disabled={!refinementRequest.trim()}
188
+ className="px-4 py-1.5 text-xs text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-40"
189
+ >
190
+ Refine
191
+ </button>
192
+ </div>
193
+ </div>
194
+ </div>
195
+ );
196
+ }
197
+
198
+ /**
199
+ * AskModelPopup
200
+ * Sub-popup for generating a mission statement and initial scope via an LLM.
201
+ * Two-column layout: left = controls/buttons, right = result output.
202
+ *
203
+ * Props:
204
+ * onUse(mission, scope) — called when the user accepts the generated result
205
+ * onClose() — called when the user cancels / closes
206
+ */
207
+ export function AskModelPopup({ onUse, onClose, onOpenSettings }) {
208
+ const [models, setModels] = useState([]);
209
+ const [selectedModelId, setSelectedModelId] = useState('');
210
+ const [selectedValidatorModelId, setSelectedValidatorModelId] = useState('');
211
+ const [description, setDescription] = useState('');
212
+ const [generating, setGenerating] = useState(false);
213
+ const [result, setResult] = useState(null);
214
+ const [error, setError] = useState('');
215
+ const [showIssuesPopup, setShowIssuesPopup] = useState(false);
216
+ const [showRefinePopup, setShowRefinePopup] = useState(false);
217
+
218
+ const { missionProgressLog, clearMissionProgress } = useCeremonyStore();
219
+ const latestProgress = missionProgressLog[missionProgressLog.length - 1] ?? null;
220
+
221
+ const [apiKeyStatus, setApiKeyStatus] = useState(null); // { anthropic: { isSet }, gemini: { isSet }, openai: { isSet } }
222
+
223
+ useEffect(() => {
224
+ Promise.all([getModels(), getSettings()])
225
+ .then(([data, settings]) => {
226
+ setModels(data);
227
+ setApiKeyStatus(settings.apiKeys ?? {});
228
+ // Auto-select best models: prefer "pro" tier for generator, "flash/lite" for validator
229
+ const ready = data.filter((m) => settings.apiKeys?.[normalizeProvider(m.provider)]?.isSet);
230
+ const isPro = (id) => /pro|opus|sonnet/i.test(id);
231
+ const isLite = (id) => /flash|lite|haiku|mini/i.test(id);
232
+ const generator = ready.find((m) => isPro(m.modelId)) || ready[0];
233
+ const generatorId = generator ? generator.modelId : (data.length > 0 ? data[0].modelId : '');
234
+ setSelectedModelId(generatorId);
235
+ const validator = ready.find((m) => isLite(m.modelId) && m.modelId !== generatorId)
236
+ || ready.find((m) => m.modelId !== generatorId)
237
+ || generator;
238
+ setSelectedValidatorModelId(validator ? validator.modelId : generatorId);
239
+ })
240
+ .catch(() => setError('Failed to load available models.'));
241
+ }, []);
242
+
243
+ function recheckKeys() {
244
+ Promise.all([getSettings(), getModels()])
245
+ .then(([s, updatedModels]) => {
246
+ setApiKeyStatus(s.apiKeys ?? {});
247
+ setModels(updatedModels);
248
+ })
249
+ .catch(() => {});
250
+ }
251
+
252
+ // Auto-recheck when settings are saved from anywhere in the app
253
+ useEffect(() => {
254
+ document.addEventListener('avc:settings-saved', recheckKeys);
255
+ return () => document.removeEventListener('avc:settings-saved', recheckKeys);
256
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
257
+
258
+ const selectedModel = models.find((m) => m.modelId === selectedModelId);
259
+ const selectedValidatorModel = models.find((m) => m.modelId === selectedValidatorModelId);
260
+
261
+ // Derive missing providers from currently selected models using settings key status
262
+ const missingKeyProviders = (() => {
263
+ if (!apiKeyStatus) return [];
264
+ const missing = new Set();
265
+ if (selectedModel && !apiKeyStatus[normalizeProvider(selectedModel.provider)]?.isSet) {
266
+ missing.add(selectedModel.provider);
267
+ }
268
+ if (selectedValidatorModel && !apiKeyStatus[normalizeProvider(selectedValidatorModel.provider)]?.isSet) {
269
+ missing.add(selectedValidatorModel.provider);
270
+ }
271
+ return [...missing];
272
+ })();
273
+
274
+ async function handleGenerate() {
275
+ if (!description.trim() || !selectedModelId || !selectedModel || !selectedValidatorModelId || !selectedValidatorModel) return;
276
+ setGenerating(true);
277
+ setError('');
278
+ setResult(null);
279
+ setShowIssuesPopup(false);
280
+ clearMissionProgress();
281
+ try {
282
+ const data = await generateMission(
283
+ description.trim(),
284
+ selectedModelId,
285
+ selectedModel.provider,
286
+ selectedValidatorModelId,
287
+ selectedValidatorModel.provider,
288
+ );
289
+ setResult(data);
290
+ } catch (err) {
291
+ setError(err.message || 'Generation failed. Please try again.');
292
+ } finally {
293
+ setGenerating(false);
294
+ }
295
+ }
296
+
297
+ async function handleRefine(refinementRequest) {
298
+ const { missionStatement, initialScope } = result;
299
+ setShowRefinePopup(false);
300
+ setResult(null);
301
+ setGenerating(true);
302
+ setError('');
303
+ setShowIssuesPopup(false);
304
+ clearMissionProgress();
305
+ try {
306
+ const data = await refineMission(
307
+ missionStatement, initialScope, refinementRequest,
308
+ selectedModelId, selectedModel.provider,
309
+ selectedValidatorModelId, selectedValidatorModel.provider,
310
+ );
311
+ setResult(data);
312
+ } catch (err) {
313
+ setError(err.message || 'Refinement failed. Please try again.');
314
+ } finally {
315
+ setGenerating(false);
316
+ }
317
+ }
318
+
319
+ function handleRefineFromIssues(selectedIssues) {
320
+ setShowIssuesPopup(false);
321
+ const request = 'Fix the following validation issues:\n' +
322
+ selectedIssues.map(i =>
323
+ `- [${i.severity.toUpperCase()}] ${i.field}: ${i.description} → ${i.suggestion}`
324
+ ).join('\n');
325
+ handleRefine(request);
326
+ }
327
+
328
+ function handleUse() {
329
+ if (!result) return;
330
+ onUse(result.missionStatement, result.initialScope);
331
+ }
332
+
333
+ function handleTryAgain() {
334
+ setResult(null);
335
+ setError('');
336
+ setShowIssuesPopup(false);
337
+ }
338
+
339
+ const providers = [...new Set(models.map((m) => m.provider))];
340
+ const scorePassed = result?.validationScore != null && result.validationScore >= 75;
341
+
342
+ const canGenerate =
343
+ !generating &&
344
+ description.trim().length > 0 &&
345
+ !!selectedModelId &&
346
+ !!selectedValidatorModelId &&
347
+ missingKeyProviders.length === 0;
348
+
349
+ // Shared model dropdown markup
350
+ function ModelSelect({ label, value, onChange }) {
351
+ const chosen = models.find((m) => m.modelId === value);
352
+ return (
353
+ <div>
354
+ <label className="block text-xs font-medium text-slate-500 mb-1">{label}</label>
355
+ <select
356
+ value={value}
357
+ onChange={(e) => onChange(e.target.value)}
358
+ disabled={generating || models.length === 0}
359
+ 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-blue-500 disabled:opacity-60 bg-white"
360
+ >
361
+ {models.length === 0 && <option value="">Loading…</option>}
362
+ {providers.map((p) => (
363
+ <optgroup key={p} label={p.charAt(0).toUpperCase() + p.slice(1)}>
364
+ {models.filter((m) => m.provider === p).map((m) => (
365
+ <option key={m.modelId} value={m.modelId}>
366
+ {m.displayName}{!m.hasApiKey ? ' (no key)' : ''}
367
+ </option>
368
+ ))}
369
+ </optgroup>
370
+ ))}
371
+ </select>
372
+ {chosen && !chosen.hasApiKey && (
373
+ <p className="text-xs text-amber-600 mt-0.5">
374
+ ⚠ No API key — add to <code>.env</code>
375
+ </p>
376
+ )}
377
+ </div>
378
+ );
379
+ }
380
+
381
+ return (
382
+ <>
383
+ <div
384
+ className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
385
+ onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
386
+ >
387
+ {/* Card — fixed height two-column */}
388
+ <div className="w-full max-w-5xl bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden h-[540px]">
389
+
390
+ {/* Header */}
391
+ <div className="flex items-center justify-between px-5 py-3 border-b border-slate-100 flex-shrink-0">
392
+ <div>
393
+ <h3 className="text-sm font-semibold text-slate-900">✨ Ask a Model</h3>
394
+ <p className="text-xs text-slate-400 mt-0.5">Describe your idea — an LLM generates and validates your mission &amp; scope.</p>
395
+ </div>
396
+ <button
397
+ type="button"
398
+ onClick={onClose}
399
+ className="text-slate-400 hover:text-slate-600 transition-colors text-xl leading-none ml-4"
400
+ aria-label="Close"
401
+ >
402
+ ×
403
+ </button>
404
+ </div>
405
+
406
+ {/* Body — two columns */}
407
+ <div className="flex flex-1 min-h-0">
408
+
409
+ {/* ── LEFT column: controls ────────────────────────────────────────── */}
410
+ <div className="w-2/5 min-w-0 border-r border-slate-100 flex flex-col px-4 py-4 gap-3">
411
+
412
+ <ModelSelect
413
+ label="Generator Model"
414
+ value={selectedModelId}
415
+ onChange={setSelectedModelId}
416
+ />
417
+
418
+ <ModelSelect
419
+ label="Validator Model"
420
+ value={selectedValidatorModelId}
421
+ onChange={setSelectedValidatorModelId}
422
+ />
423
+
424
+ {missingKeyProviders.length > 0 && (
425
+ <div className="flex flex-col gap-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
426
+ <div className="flex items-start gap-2">
427
+ <AlertTriangle className="w-3.5 h-3.5 text-amber-500 flex-shrink-0 mt-0.5" />
428
+ <div>
429
+ <p className="text-xs font-semibold text-amber-900">API Key Missing</p>
430
+ <ul className="mt-1 space-y-0.5">
431
+ {missingKeyProviders.map((p) => (
432
+ <li key={p} className="flex items-center gap-1.5 text-xs text-amber-800">
433
+ <span className="w-1 h-1 rounded-full bg-amber-400 flex-shrink-0" />
434
+ {KEY_LABELS[p] || p}
435
+ </li>
436
+ ))}
437
+ </ul>
438
+ </div>
439
+ </div>
440
+ <div className="flex items-center gap-2">
441
+ {onOpenSettings && (
442
+ <button
443
+ type="button"
444
+ onClick={() => onOpenSettings()}
445
+ className="flex items-center gap-1 text-xs font-medium bg-slate-900 text-white px-2.5 py-1 rounded-md hover:bg-slate-700 transition-colors"
446
+ >
447
+ <SettingsIcon className="w-3 h-3" />
448
+ Settings
449
+ </button>
450
+ )}
451
+ <button
452
+ type="button"
453
+ onClick={recheckKeys}
454
+ className="text-xs text-slate-500 hover:text-slate-800 transition-colors"
455
+ >
456
+ Re-check
457
+ </button>
458
+ </div>
459
+ </div>
460
+ )}
461
+
462
+ <div>
463
+ <label className="block text-xs font-medium text-slate-500 mb-1">
464
+ What are you building?
465
+ </label>
466
+ <textarea
467
+ value={description}
468
+ onChange={(e) => setDescription(e.target.value)}
469
+ rows={4}
470
+ placeholder="E.g. A recipe sharing app for home cooks…"
471
+ disabled={generating}
472
+ 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-blue-500 resize-none disabled:opacity-60"
473
+ />
474
+ </div>
475
+
476
+ {/* Error */}
477
+ {error && (
478
+ <p className="text-xs text-red-600 bg-red-50 rounded-md px-2 py-1.5">{error}</p>
479
+ )}
480
+
481
+ {/* Score badge + issues button */}
482
+ {result?.validationScore != null && (
483
+ <div className="flex items-center gap-2 flex-wrap">
484
+ <span
485
+ className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full ${
486
+ scorePassed
487
+ ? 'bg-green-50 text-green-700 border border-green-200'
488
+ : 'bg-amber-50 text-amber-700 border border-amber-200'
489
+ }`}
490
+ >
491
+ {scorePassed ? '✓' : '⚠'} {result.validationScore}/100
492
+ &nbsp;·&nbsp;
493
+ {result.iterations} iter{result.iterations !== 1 ? 's' : ''}
494
+ </span>
495
+ {result.issues?.length > 0 && (
496
+ <button
497
+ type="button"
498
+ onClick={() => setShowIssuesPopup(true)}
499
+ className="text-xs text-slate-400 hover:text-slate-600 underline underline-offset-2 transition-colors"
500
+ >
501
+ Issues ({result.issues.length})
502
+ </button>
503
+ )}
504
+ </div>
505
+ )}
506
+
507
+ {/* Spacer pushes buttons to bottom */}
508
+ <div className="flex-1" />
509
+
510
+ {/* Buttons */}
511
+ <div className="flex flex-col gap-2">
512
+ {result ? (
513
+ <>
514
+ <button
515
+ type="button"
516
+ onClick={handleUse}
517
+ className="w-full px-3 py-2 bg-slate-900 text-white text-xs font-medium rounded-lg hover:bg-slate-700 transition-colors"
518
+ >
519
+ Use This
520
+ </button>
521
+ <button
522
+ type="button"
523
+ onClick={() => setShowRefinePopup(true)}
524
+ className="w-full px-3 py-2 text-xs text-slate-600 border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors"
525
+ >
526
+ Refine
527
+ </button>
528
+ <button
529
+ type="button"
530
+ onClick={handleTryAgain}
531
+ className="w-full px-3 py-2 text-xs text-slate-600 border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors"
532
+ >
533
+ Try Again
534
+ </button>
535
+ </>
536
+ ) : (
537
+ <button
538
+ type="button"
539
+ onClick={handleGenerate}
540
+ disabled={!canGenerate}
541
+ className="w-full px-3 py-2 bg-blue-600 text-white text-xs font-medium rounded-lg disabled:opacity-40 hover:bg-blue-700 transition-colors flex items-center justify-center gap-2"
542
+ >
543
+ {generating ? (
544
+ <>
545
+ <span className="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
546
+ Generating…
547
+ </>
548
+ ) : 'Generate'}
549
+ </button>
550
+ )}
551
+ <button
552
+ type="button"
553
+ onClick={onClose}
554
+ className="w-full px-3 py-1.5 text-xs text-slate-400 hover:text-slate-600 transition-colors"
555
+ >
556
+ Cancel
557
+ </button>
558
+ </div>
559
+ </div>
560
+
561
+ {/* ── RIGHT column: output ─────────────────────────────────────────── */}
562
+ <div className="flex-1 flex flex-col min-w-0">
563
+
564
+ {/* Generating state */}
565
+ {generating && (
566
+ <div className="flex-1 flex flex-col items-center justify-center gap-3 px-8 text-center">
567
+ <span className="w-8 h-8 border-3 border-blue-200 border-t-blue-600 rounded-full animate-spin" style={{ borderWidth: 3 }} />
568
+ <p className="text-sm font-medium text-slate-700">
569
+ {latestProgress ? latestProgress.message : 'Starting…'}
570
+ </p>
571
+ <p className="text-xs text-slate-400">This may take a moment while the models validate and refine the output.</p>
572
+ </div>
573
+ )}
574
+
575
+ {/* Idle / no result yet */}
576
+ {!generating && !result && (
577
+ <div className="flex-1 flex flex-col items-center justify-center gap-2 px-8 text-center text-slate-300">
578
+ <svg className="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
579
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
580
+ d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z" />
581
+ </svg>
582
+ <p className="text-sm font-medium text-slate-400">Generated mission &amp; scope will appear here</p>
583
+ <p className="text-xs text-slate-300">Fill in the form and click Generate</p>
584
+ </div>
585
+ )}
586
+
587
+ {/* Result */}
588
+ {!generating && result && (
589
+ <div className="flex-1 flex flex-col gap-4 px-5 py-4 overflow-y-auto">
590
+ <div>
591
+ <p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">Mission Statement</p>
592
+ <p className="text-sm text-slate-800 bg-slate-50 rounded-lg px-3 py-2.5 leading-relaxed">
593
+ {result.missionStatement}
594
+ </p>
595
+ </div>
596
+ <div className="flex-1 flex flex-col min-h-0">
597
+ <p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">Initial Scope</p>
598
+ <div className="flex-1 text-sm text-slate-800 bg-slate-50 rounded-lg px-3 py-2.5 leading-relaxed overflow-y-auto min-h-0">
599
+ {result.initialScope.split('\n').map((line, i) => (
600
+ <p key={i} className="mb-1 last:mb-0">{line}</p>
601
+ ))}
602
+ </div>
603
+ </div>
604
+ </div>
605
+ )}
606
+ </div>
607
+ </div>
608
+ </div>
609
+ </div>
610
+
611
+ {/* Issues detail popup */}
612
+ {showIssuesPopup && result?.issues?.length > 0 && (
613
+ <IssuesPopup
614
+ issues={result.issues}
615
+ onClose={() => setShowIssuesPopup(false)}
616
+ onRefineWithIssues={handleRefineFromIssues}
617
+ />
618
+ )}
619
+
620
+ {/* Refine popup */}
621
+ {showRefinePopup && result && (
622
+ <RefinePopup
623
+ onSubmit={handleRefine}
624
+ onClose={() => setShowRefinePopup(false)}
625
+ />
626
+ )}
627
+ </>
628
+ );
629
+ }