@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,106 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useCeremonyStore } from '../../../store/ceremonyStore';
|
|
3
|
+
import { AskModelPopup } from '../AskModelPopup';
|
|
4
|
+
|
|
5
|
+
export function MissionStep({ onNext, onBack, analyzing, onOpenSettings }) {
|
|
6
|
+
const { mission, setMission, initialScope, setInitialScope } = useCeremonyStore();
|
|
7
|
+
const [showPopup, setShowPopup] = useState(false);
|
|
8
|
+
|
|
9
|
+
const canContinue = mission.trim().length > 0 && initialScope.trim().length > 0;
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="space-y-6">
|
|
13
|
+
<div>
|
|
14
|
+
<h2 className="text-xl font-semibold text-slate-900">Mission & Scope</h2>
|
|
15
|
+
<p className="text-sm text-slate-500 mt-1">
|
|
16
|
+
Describe what your project does and what it will deliver in the first version.
|
|
17
|
+
</p>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div className="space-y-4">
|
|
21
|
+
<div>
|
|
22
|
+
<div className="flex items-center justify-between mb-1">
|
|
23
|
+
<label className="block text-sm font-medium text-slate-700">
|
|
24
|
+
Mission Statement <span className="text-red-500">*</span>
|
|
25
|
+
</label>
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
onClick={() => setShowPopup(true)}
|
|
29
|
+
className="text-xs text-blue-600 hover:text-blue-700 flex items-center gap-1 transition-colors"
|
|
30
|
+
>
|
|
31
|
+
✨ Ask a Model
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
<p className="text-xs text-slate-400 mb-2">
|
|
35
|
+
A concise statement describing the core purpose and value proposition.
|
|
36
|
+
Example: "Enable small businesses to manage inventory through an intuitive mobile-first platform."
|
|
37
|
+
</p>
|
|
38
|
+
<textarea
|
|
39
|
+
value={mission}
|
|
40
|
+
onChange={(e) => setMission(e.target.value)}
|
|
41
|
+
rows={3}
|
|
42
|
+
placeholder="Enter your mission statement..."
|
|
43
|
+
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
|
44
|
+
/>
|
|
45
|
+
{!mission.trim() && (
|
|
46
|
+
<p className="text-xs text-red-500 mt-1">Mission statement is required.</p>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div>
|
|
51
|
+
<label className="block text-sm font-medium text-slate-700 mb-1">
|
|
52
|
+
Initial Scope <span className="text-red-500">*</span>
|
|
53
|
+
</label>
|
|
54
|
+
<p className="text-xs text-slate-400 mb-2">
|
|
55
|
+
Describe key features, main workflows, and core functionality for the first version.
|
|
56
|
+
Example: "Users can create tasks, assign them to team members, track progress, and set deadlines."
|
|
57
|
+
</p>
|
|
58
|
+
<textarea
|
|
59
|
+
value={initialScope}
|
|
60
|
+
onChange={(e) => setInitialScope(e.target.value)}
|
|
61
|
+
rows={4}
|
|
62
|
+
placeholder="Describe the initial scope and features..."
|
|
63
|
+
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div className="flex items-center justify-between pt-2">
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
onClick={onBack}
|
|
72
|
+
disabled={analyzing}
|
|
73
|
+
className="text-sm text-slate-400 hover:text-slate-600 disabled:opacity-40 transition-colors"
|
|
74
|
+
>
|
|
75
|
+
← Back
|
|
76
|
+
</button>
|
|
77
|
+
<button
|
|
78
|
+
onClick={onNext}
|
|
79
|
+
disabled={!canContinue || analyzing}
|
|
80
|
+
className="px-5 py-2 bg-slate-900 text-white text-sm font-medium rounded-lg disabled:opacity-40 hover:bg-slate-700 transition-colors flex items-center gap-2"
|
|
81
|
+
>
|
|
82
|
+
{analyzing ? (
|
|
83
|
+
<>
|
|
84
|
+
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
85
|
+
Analysing…
|
|
86
|
+
</>
|
|
87
|
+
) : (
|
|
88
|
+
'Continue'
|
|
89
|
+
)}
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{showPopup && (
|
|
94
|
+
<AskModelPopup
|
|
95
|
+
onUse={(generatedMission, generatedScope) => {
|
|
96
|
+
setMission(generatedMission);
|
|
97
|
+
setInitialScope(generatedScope);
|
|
98
|
+
setShowPopup(false);
|
|
99
|
+
}}
|
|
100
|
+
onClose={() => setShowPopup(false)}
|
|
101
|
+
onOpenSettings={onOpenSettings}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Sparkles } from 'lucide-react';
|
|
3
|
+
import { useCeremonyStore } from '../../../store/ceremonyStore';
|
|
4
|
+
import { getModels, getSettings, refineField } from '../../../lib/api';
|
|
5
|
+
|
|
6
|
+
const FIELDS = [
|
|
7
|
+
{
|
|
8
|
+
key: 'TARGET_USERS',
|
|
9
|
+
label: 'Target Users',
|
|
10
|
+
description: 'Who will use this application? List different user types and their roles.',
|
|
11
|
+
rows: 3,
|
|
12
|
+
required: true,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
key: 'DEPLOYMENT_TARGET',
|
|
16
|
+
label: 'Deployment Target',
|
|
17
|
+
description: 'Where and how will this application be deployed?',
|
|
18
|
+
rows: 3,
|
|
19
|
+
required: true,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: 'TECHNICAL_CONSIDERATIONS',
|
|
23
|
+
label: 'Technical Considerations',
|
|
24
|
+
description: 'Technology stack, architectural patterns, scalability, and performance requirements.',
|
|
25
|
+
rows: 4,
|
|
26
|
+
required: true,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: 'TECHNICAL_EXCLUSIONS',
|
|
30
|
+
label: 'Technical Exclusions',
|
|
31
|
+
description:
|
|
32
|
+
'Technologies, frameworks, or services to explicitly exclude from recommendations. Leave blank if none.',
|
|
33
|
+
rows: 2,
|
|
34
|
+
required: false,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: 'SECURITY_AND_COMPLIANCE_REQUIREMENTS',
|
|
38
|
+
label: 'Security & Compliance Requirements',
|
|
39
|
+
description:
|
|
40
|
+
'Security, privacy, or regulatory requirements your application must meet. Examples: GDPR compliance, PCI DSS, two-factor authentication.',
|
|
41
|
+
rows: 3,
|
|
42
|
+
required: true,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Maps model provider → apiKeys key returned by getSettings()
|
|
47
|
+
function normalizeProvider(p = '') {
|
|
48
|
+
if (p === 'claude') return 'anthropic';
|
|
49
|
+
return p;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Inline "Ask a Model" panel for a single field.
|
|
54
|
+
*/
|
|
55
|
+
function AskModelPanel({ fieldKey, fieldLabel, currentValue, context, onApply, onClose }) {
|
|
56
|
+
const [models, setModels] = useState([]);
|
|
57
|
+
const [selectedModelId, setSelectedModelId] = useState('');
|
|
58
|
+
const [instruction, setInstruction] = useState('');
|
|
59
|
+
const [loading, setLoading] = useState(false);
|
|
60
|
+
const [result, setResult] = useState(null);
|
|
61
|
+
const [error, setError] = useState('');
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
Promise.all([getModels(), getSettings()])
|
|
65
|
+
.then(([data, settings]) => {
|
|
66
|
+
setModels(data);
|
|
67
|
+
const apiKeys = settings.apiKeys ?? {};
|
|
68
|
+
const ready = data.filter((m) => apiKeys[normalizeProvider(m.provider)]?.isSet);
|
|
69
|
+
const isPro = (id) => /pro|opus|sonnet/i.test(id);
|
|
70
|
+
const best = ready.find((m) => isPro(m.modelId)) || ready[0];
|
|
71
|
+
setSelectedModelId(best ? best.modelId : (data[0]?.modelId || ''));
|
|
72
|
+
})
|
|
73
|
+
.catch(() => setError('Failed to load models.'));
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const selectedModel = models.find((m) => m.modelId === selectedModelId);
|
|
77
|
+
const providers = [...new Set(models.map((m) => m.provider))];
|
|
78
|
+
|
|
79
|
+
async function handleRefine() {
|
|
80
|
+
if (!selectedModel || !instruction.trim()) return;
|
|
81
|
+
setLoading(true);
|
|
82
|
+
setError('');
|
|
83
|
+
setResult(null);
|
|
84
|
+
try {
|
|
85
|
+
const data = await refineField(
|
|
86
|
+
fieldKey, fieldLabel, currentValue,
|
|
87
|
+
instruction.trim(), context,
|
|
88
|
+
selectedModelId, selectedModel.provider,
|
|
89
|
+
);
|
|
90
|
+
setResult(data.value);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
setError(err.message || 'Refinement failed.');
|
|
93
|
+
} finally {
|
|
94
|
+
setLoading(false);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className="mt-2 rounded-lg border border-blue-200 bg-blue-50/50 p-3 space-y-2">
|
|
100
|
+
<div className="flex items-center justify-between">
|
|
101
|
+
<span className="text-xs font-semibold text-blue-700">Ask a Model — {fieldLabel}</span>
|
|
102
|
+
<button type="button" onClick={onClose} className="text-xs text-slate-400 hover:text-slate-600">
|
|
103
|
+
×
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{/* Model selector + instruction */}
|
|
108
|
+
{!result && (
|
|
109
|
+
<>
|
|
110
|
+
<div className="flex gap-2">
|
|
111
|
+
<select
|
|
112
|
+
value={selectedModelId}
|
|
113
|
+
onChange={(e) => setSelectedModelId(e.target.value)}
|
|
114
|
+
disabled={loading || models.length === 0}
|
|
115
|
+
className="flex-1 rounded-md border border-slate-300 px-2 py-1 text-xs text-slate-900 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-60 bg-white"
|
|
116
|
+
>
|
|
117
|
+
{models.length === 0 && <option value="">Loading…</option>}
|
|
118
|
+
{providers.map((p) => (
|
|
119
|
+
<optgroup key={p} label={p.charAt(0).toUpperCase() + p.slice(1)}>
|
|
120
|
+
{models.filter((m) => m.provider === p).map((m) => (
|
|
121
|
+
<option key={m.modelId} value={m.modelId}>
|
|
122
|
+
{m.displayName}{!m.hasApiKey ? ' (no key)' : ''}
|
|
123
|
+
</option>
|
|
124
|
+
))}
|
|
125
|
+
</optgroup>
|
|
126
|
+
))}
|
|
127
|
+
</select>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<textarea
|
|
131
|
+
value={instruction}
|
|
132
|
+
onChange={(e) => setInstruction(e.target.value)}
|
|
133
|
+
rows={2}
|
|
134
|
+
placeholder="What would you like to improve? E.g. Be more specific about enterprise users…"
|
|
135
|
+
disabled={loading}
|
|
136
|
+
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"
|
|
137
|
+
/>
|
|
138
|
+
|
|
139
|
+
{error && <p className="text-xs text-red-600">{error}</p>}
|
|
140
|
+
|
|
141
|
+
<div className="flex justify-end gap-2">
|
|
142
|
+
<button type="button" onClick={onClose}
|
|
143
|
+
className="px-3 py-1 text-xs text-slate-500 hover:text-slate-700">
|
|
144
|
+
Cancel
|
|
145
|
+
</button>
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
onClick={handleRefine}
|
|
149
|
+
disabled={loading || !instruction.trim() || !selectedModelId}
|
|
150
|
+
className="px-3 py-1 text-xs font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-40 flex items-center gap-1.5"
|
|
151
|
+
>
|
|
152
|
+
{loading ? (
|
|
153
|
+
<>
|
|
154
|
+
<span className="w-3 h-3 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
155
|
+
Refining…
|
|
156
|
+
</>
|
|
157
|
+
) : 'Refine'}
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
</>
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
{/* Result */}
|
|
164
|
+
{result && (
|
|
165
|
+
<>
|
|
166
|
+
<div className="text-xs text-slate-700 bg-white rounded-md border border-slate-200 px-2.5 py-2 whitespace-pre-wrap max-h-40 overflow-y-auto">
|
|
167
|
+
{result}
|
|
168
|
+
</div>
|
|
169
|
+
<div className="flex justify-end gap-2">
|
|
170
|
+
<button type="button" onClick={() => { setResult(null); setInstruction(''); }}
|
|
171
|
+
className="px-3 py-1 text-xs text-slate-500 hover:text-slate-700">
|
|
172
|
+
Try Again
|
|
173
|
+
</button>
|
|
174
|
+
<button type="button" onClick={() => { onApply(result); onClose(); }}
|
|
175
|
+
className="px-3 py-1 text-xs font-medium text-white bg-slate-900 rounded-md hover:bg-slate-700">
|
|
176
|
+
Use This
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
179
|
+
</>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function ReviewAnswersStep({ onNext, onBack }) {
|
|
186
|
+
const { requirements, updateRequirement, mission, setMission, initialScope, setInitialScope } = useCeremonyStore();
|
|
187
|
+
|
|
188
|
+
// Track which field has the "Ask Model" panel open (only one at a time)
|
|
189
|
+
const [askModelField, setAskModelField] = useState(null);
|
|
190
|
+
|
|
191
|
+
const context = { mission, scope: initialScope };
|
|
192
|
+
|
|
193
|
+
const canContinue =
|
|
194
|
+
mission.trim().length > 0 &&
|
|
195
|
+
initialScope.trim().length > 0 &&
|
|
196
|
+
FIELDS.filter((f) => f.required).every((f) => requirements[f.key]?.trim());
|
|
197
|
+
|
|
198
|
+
function AskModelButton({ fieldKey }) {
|
|
199
|
+
return (
|
|
200
|
+
<button
|
|
201
|
+
type="button"
|
|
202
|
+
onClick={() => setAskModelField(askModelField === fieldKey ? null : fieldKey)}
|
|
203
|
+
className={`inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-md transition-colors ${
|
|
204
|
+
askModelField === fieldKey
|
|
205
|
+
? 'bg-blue-100 text-blue-700'
|
|
206
|
+
: 'text-slate-400 hover:text-blue-600 hover:bg-blue-50'
|
|
207
|
+
}`}
|
|
208
|
+
title="Ask a model to improve this field"
|
|
209
|
+
>
|
|
210
|
+
<Sparkles className="w-3 h-3" />
|
|
211
|
+
Ask Model
|
|
212
|
+
</button>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div className="space-y-6">
|
|
218
|
+
<div>
|
|
219
|
+
<h2 className="text-xl font-semibold text-slate-900">Review & Edit Requirements</h2>
|
|
220
|
+
<p className="text-sm text-slate-500 mt-1">
|
|
221
|
+
AI has pre-filled these requirements based on your selections. Review and edit as needed.
|
|
222
|
+
</p>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<div className="space-y-4">
|
|
226
|
+
{/* Mission & Scope */}
|
|
227
|
+
<div>
|
|
228
|
+
<div className="flex items-center justify-between mb-0.5">
|
|
229
|
+
<label className="block text-sm font-medium text-slate-700">
|
|
230
|
+
Mission Statement <span className="text-red-500">*</span>
|
|
231
|
+
</label>
|
|
232
|
+
<AskModelButton fieldKey="MISSION_STATEMENT" />
|
|
233
|
+
</div>
|
|
234
|
+
<textarea
|
|
235
|
+
value={mission}
|
|
236
|
+
onChange={(e) => setMission(e.target.value)}
|
|
237
|
+
rows={3}
|
|
238
|
+
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
|
239
|
+
placeholder="Required..."
|
|
240
|
+
/>
|
|
241
|
+
{askModelField === 'MISSION_STATEMENT' && (
|
|
242
|
+
<AskModelPanel
|
|
243
|
+
fieldKey="MISSION_STATEMENT"
|
|
244
|
+
fieldLabel="Mission Statement"
|
|
245
|
+
currentValue={mission}
|
|
246
|
+
context={context}
|
|
247
|
+
onApply={(val) => setMission(val)}
|
|
248
|
+
onClose={() => setAskModelField(null)}
|
|
249
|
+
/>
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
<div>
|
|
253
|
+
<div className="flex items-center justify-between mb-0.5">
|
|
254
|
+
<label className="block text-sm font-medium text-slate-700">
|
|
255
|
+
Initial Scope <span className="text-red-500">*</span>
|
|
256
|
+
</label>
|
|
257
|
+
<AskModelButton fieldKey="INITIAL_SCOPE" />
|
|
258
|
+
</div>
|
|
259
|
+
<textarea
|
|
260
|
+
value={initialScope}
|
|
261
|
+
onChange={(e) => setInitialScope(e.target.value)}
|
|
262
|
+
rows={4}
|
|
263
|
+
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
|
264
|
+
placeholder="Required..."
|
|
265
|
+
/>
|
|
266
|
+
{askModelField === 'INITIAL_SCOPE' && (
|
|
267
|
+
<AskModelPanel
|
|
268
|
+
fieldKey="INITIAL_SCOPE"
|
|
269
|
+
fieldLabel="Initial Scope"
|
|
270
|
+
currentValue={initialScope}
|
|
271
|
+
context={context}
|
|
272
|
+
onApply={(val) => setInitialScope(val)}
|
|
273
|
+
onClose={() => setAskModelField(null)}
|
|
274
|
+
/>
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<hr className="border-slate-200" />
|
|
279
|
+
|
|
280
|
+
{FIELDS.map((field) => (
|
|
281
|
+
<div key={field.key}>
|
|
282
|
+
<div className="flex items-center justify-between mb-0.5">
|
|
283
|
+
<label className="block text-sm font-medium text-slate-700">
|
|
284
|
+
{field.label}
|
|
285
|
+
{field.required && <span className="text-red-500 ml-1">*</span>}
|
|
286
|
+
</label>
|
|
287
|
+
<AskModelButton fieldKey={field.key} />
|
|
288
|
+
</div>
|
|
289
|
+
<p className="text-xs text-slate-400 mb-1">{field.description}</p>
|
|
290
|
+
<textarea
|
|
291
|
+
value={requirements[field.key] || ''}
|
|
292
|
+
onChange={(e) => updateRequirement(field.key, e.target.value)}
|
|
293
|
+
rows={field.rows}
|
|
294
|
+
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
|
295
|
+
placeholder={field.required ? `Required...` : 'Optional...'}
|
|
296
|
+
/>
|
|
297
|
+
{askModelField === field.key && (
|
|
298
|
+
<AskModelPanel
|
|
299
|
+
fieldKey={field.key}
|
|
300
|
+
fieldLabel={field.label}
|
|
301
|
+
currentValue={requirements[field.key] || ''}
|
|
302
|
+
context={context}
|
|
303
|
+
onApply={(val) => updateRequirement(field.key, val)}
|
|
304
|
+
onClose={() => setAskModelField(null)}
|
|
305
|
+
/>
|
|
306
|
+
)}
|
|
307
|
+
</div>
|
|
308
|
+
))}
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<div className="flex items-center justify-between pt-2">
|
|
312
|
+
<button
|
|
313
|
+
type="button"
|
|
314
|
+
onClick={onBack}
|
|
315
|
+
className="text-sm text-slate-400 hover:text-slate-600 transition-colors"
|
|
316
|
+
>
|
|
317
|
+
← Back
|
|
318
|
+
</button>
|
|
319
|
+
<button
|
|
320
|
+
onClick={onNext}
|
|
321
|
+
disabled={!canContinue}
|
|
322
|
+
className="px-5 py-2 bg-green-600 text-white text-sm font-medium rounded-lg disabled:opacity-40 hover:bg-green-700 transition-colors"
|
|
323
|
+
>
|
|
324
|
+
Generate Project Brief
|
|
325
|
+
</button>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { ArrowDownToLine } from 'lucide-react';
|
|
3
|
+
import { useCeremonyStore } from '../../../store/ceremonyStore';
|
|
4
|
+
import { resetCeremony } from '../../../lib/api';
|
|
5
|
+
|
|
6
|
+
function parseStageNumber(message) {
|
|
7
|
+
const m = message?.match(/Stage\s+(\d+(?:\.\d+)?)\/(\d+)/i);
|
|
8
|
+
if (m) return { current: parseFloat(m[1]), total: parseInt(m[2]) };
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildStageGroups(progressLog) {
|
|
13
|
+
const groups = [];
|
|
14
|
+
for (const entry of progressLog) {
|
|
15
|
+
if (entry.type === 'progress') {
|
|
16
|
+
groups.push({ message: entry.message, substeps: [], orphanDetails: [] });
|
|
17
|
+
} else if (entry.type === 'substep' && groups.length > 0) {
|
|
18
|
+
groups[groups.length - 1].substeps.push({ text: entry.substep, details: [] });
|
|
19
|
+
} else if (entry.type === 'detail' && groups.length > 0) {
|
|
20
|
+
const group = groups[groups.length - 1];
|
|
21
|
+
const substeps = group.substeps;
|
|
22
|
+
if (substeps.length > 0) {
|
|
23
|
+
substeps[substeps.length - 1].details.push(entry.detail);
|
|
24
|
+
} else {
|
|
25
|
+
group.orphanDetails.push(entry.detail);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return groups;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function RunningStep({ transitioning, onPause, onResume, onCancel, onBackground }) {
|
|
33
|
+
const { progressLog, ceremonyStatus, ceremonyError, isPaused, setWizardStep, setCeremonyStatus, setCeremonyError } = useCeremonyStore();
|
|
34
|
+
const logBottomRef = useRef(null);
|
|
35
|
+
const [showForceStop, setShowForceStop] = useState(false);
|
|
36
|
+
|
|
37
|
+
// Show "Force Stop" button after 5s of cancelling
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (transitioning === 'cancelling') {
|
|
40
|
+
setShowForceStop(false);
|
|
41
|
+
const timer = setTimeout(() => setShowForceStop(true), 5000);
|
|
42
|
+
return () => clearTimeout(timer);
|
|
43
|
+
}
|
|
44
|
+
setShowForceStop(false);
|
|
45
|
+
}, [transitioning]);
|
|
46
|
+
|
|
47
|
+
const handleForceReset = async () => {
|
|
48
|
+
try { await resetCeremony(); } catch (_) {}
|
|
49
|
+
setCeremonyStatus('idle');
|
|
50
|
+
setCeremonyError(null);
|
|
51
|
+
setWizardStep(1);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
logBottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
56
|
+
}, [progressLog]);
|
|
57
|
+
|
|
58
|
+
// Auto-advance to CompleteStep when ceremony finishes
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (ceremonyStatus === 'complete') {
|
|
61
|
+
setWizardStep(7);
|
|
62
|
+
}
|
|
63
|
+
}, [ceremonyStatus, setWizardStep]);
|
|
64
|
+
|
|
65
|
+
const stageGroups = buildStageGroups(progressLog);
|
|
66
|
+
|
|
67
|
+
let currentStage = null;
|
|
68
|
+
let totalStages = 5;
|
|
69
|
+
for (const g of [...stageGroups].reverse()) {
|
|
70
|
+
const parsed = parseStageNumber(g.message);
|
|
71
|
+
if (parsed) {
|
|
72
|
+
currentStage = parsed.current;
|
|
73
|
+
totalStages = parsed.total;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const progressPct = currentStage ? Math.round((currentStage / totalStages) * 100) : 5;
|
|
79
|
+
const latestProgress = stageGroups[stageGroups.length - 1]?.message || 'Starting ceremony...';
|
|
80
|
+
|
|
81
|
+
if (ceremonyStatus === 'error') {
|
|
82
|
+
const isAlreadyRunning = ceremonyError?.includes('already running');
|
|
83
|
+
return (
|
|
84
|
+
<div className="space-y-4">
|
|
85
|
+
<h2 className="text-xl font-semibold text-slate-900">Generation Failed</h2>
|
|
86
|
+
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
87
|
+
<p className="text-sm font-medium text-red-700 mb-1">Error</p>
|
|
88
|
+
<p className="text-sm text-red-600">{ceremonyError || 'An unknown error occurred.'}</p>
|
|
89
|
+
</div>
|
|
90
|
+
{isAlreadyRunning ? (
|
|
91
|
+
<div className="space-y-3">
|
|
92
|
+
<p className="text-sm text-slate-500">
|
|
93
|
+
A ceremony is already running on the server. You can force-stop it and reset the state — this will discard any in-progress work.
|
|
94
|
+
</p>
|
|
95
|
+
<button
|
|
96
|
+
onClick={handleForceReset}
|
|
97
|
+
className="px-4 py-2 text-sm rounded-lg bg-red-600 text-white hover:bg-red-700 transition-colors"
|
|
98
|
+
>
|
|
99
|
+
Force Stop & Reset
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
) : (
|
|
103
|
+
<p className="text-sm text-slate-500">
|
|
104
|
+
Check that your API key is configured correctly in your project's <code>.env</code> file.
|
|
105
|
+
</p>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="space-y-6">
|
|
113
|
+
<div>
|
|
114
|
+
<h2 className="text-xl font-semibold text-slate-900">Generating Documentation</h2>
|
|
115
|
+
<p className="text-sm text-slate-500 mt-1">
|
|
116
|
+
The AI is generating your project documentation. Duration varies depending on the number of validation iterations — typically a few minutes, but may take longer.
|
|
117
|
+
</p>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Stage progress bar */}
|
|
121
|
+
<div>
|
|
122
|
+
<div className="flex items-center justify-between mb-1">
|
|
123
|
+
<span className="text-xs font-medium text-slate-600">{latestProgress}</span>
|
|
124
|
+
<span className="text-xs text-slate-400">{progressPct}%</span>
|
|
125
|
+
</div>
|
|
126
|
+
<div className="w-full bg-slate-200 rounded-full h-2">
|
|
127
|
+
<div
|
|
128
|
+
className="bg-blue-500 h-2 rounded-full transition-all duration-500"
|
|
129
|
+
style={{ width: `${progressPct}%` }}
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Stage log — hierarchical */}
|
|
135
|
+
<div className="bg-slate-900 rounded-lg p-4 h-56 overflow-y-auto font-mono text-xs">
|
|
136
|
+
{stageGroups.length === 0 ? (
|
|
137
|
+
<p className="text-slate-400 animate-pulse">Initializing...</p>
|
|
138
|
+
) : (
|
|
139
|
+
<div className="space-y-2">
|
|
140
|
+
{stageGroups.map((group, gi) => {
|
|
141
|
+
const isActive = gi === stageGroups.length - 1 && ceremonyStatus === 'running';
|
|
142
|
+
return (
|
|
143
|
+
<div key={gi}>
|
|
144
|
+
<div className="flex items-center gap-1.5">
|
|
145
|
+
{isActive ? (
|
|
146
|
+
<span className="inline-block w-3 h-3 border border-blue-400 border-t-blue-200 rounded-full animate-spin flex-shrink-0" />
|
|
147
|
+
) : (
|
|
148
|
+
<span className="text-green-500 flex-shrink-0">✓</span>
|
|
149
|
+
)}
|
|
150
|
+
<span className={isActive ? 'text-blue-300 font-medium' : 'text-slate-400'}>
|
|
151
|
+
{group.message}
|
|
152
|
+
</span>
|
|
153
|
+
</div>
|
|
154
|
+
{group.orphanDetails?.length > 0 && (
|
|
155
|
+
<div className="ml-5 mt-0.5 space-y-0.5 border-l border-slate-700 pl-2">
|
|
156
|
+
{group.orphanDetails.map((d, di) => (
|
|
157
|
+
<p key={di} className="text-slate-500">{d}</p>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
{group.substeps.length > 0 && (
|
|
162
|
+
<div className="ml-5 mt-0.5 space-y-0.5 border-l border-slate-700 pl-2">
|
|
163
|
+
{group.substeps.map((sub, si) => (
|
|
164
|
+
<div key={si}>
|
|
165
|
+
<p className="text-slate-400">{sub.text}</p>
|
|
166
|
+
{sub.details.length > 0 && (
|
|
167
|
+
<div className="ml-3 mt-0.5 space-y-0.5 border-l border-slate-700 pl-2">
|
|
168
|
+
{sub.details.map((d, di) => (
|
|
169
|
+
<p key={di} className="text-slate-500">{d}</p>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
))}
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
})}
|
|
180
|
+
{transitioning === 'cancelling' && (
|
|
181
|
+
<div className="flex items-center gap-1.5 mt-1">
|
|
182
|
+
<span className="inline-block w-3 h-3 border border-red-400 border-t-red-200 rounded-full animate-spin flex-shrink-0" />
|
|
183
|
+
<span className="text-red-400 font-medium">Cancelling…</span>
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
<div ref={logBottomRef} />
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div className="flex items-center justify-between gap-2 pt-2">
|
|
192
|
+
{/* Left: run in background */}
|
|
193
|
+
{onBackground && !transitioning && (
|
|
194
|
+
<button
|
|
195
|
+
type="button"
|
|
196
|
+
onClick={onBackground}
|
|
197
|
+
className="flex items-center gap-1.5 text-xs font-medium text-slate-500 hover:text-slate-700 bg-slate-50 hover:bg-slate-100 border border-slate-200 rounded-md px-2.5 py-1.5 transition-colors"
|
|
198
|
+
title="Hide this window — ceremony keeps running in the background"
|
|
199
|
+
>
|
|
200
|
+
<ArrowDownToLine className="w-3.5 h-3.5" />
|
|
201
|
+
Run in Background
|
|
202
|
+
</button>
|
|
203
|
+
)}
|
|
204
|
+
{transitioning && <span />}
|
|
205
|
+
|
|
206
|
+
{/* Right: pause / resume / cancel */}
|
|
207
|
+
<div className="flex items-center gap-2">
|
|
208
|
+
{transitioning === 'pausing' ? (
|
|
209
|
+
<span className="flex items-center gap-1.5 text-xs text-slate-400"><span className="inline-block w-3 h-3 border border-slate-400 border-t-slate-200 rounded-full animate-spin" />Pausing…</span>
|
|
210
|
+
) : transitioning === 'cancelling' ? (
|
|
211
|
+
<div className="flex items-center gap-3">
|
|
212
|
+
<span className="flex items-center gap-1.5 text-xs text-red-400"><span className="inline-block w-3 h-3 border border-red-400 border-t-red-200 rounded-full animate-spin" />Cancelling…</span>
|
|
213
|
+
{showForceStop && (
|
|
214
|
+
<button
|
|
215
|
+
onClick={handleForceReset}
|
|
216
|
+
className="px-3 py-1.5 text-xs rounded-lg bg-red-600 text-white hover:bg-red-700 transition-colors"
|
|
217
|
+
>
|
|
218
|
+
Force Stop
|
|
219
|
+
</button>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
) : !isPaused ? (
|
|
223
|
+
<button
|
|
224
|
+
onClick={onPause}
|
|
225
|
+
className="px-4 py-2 text-sm rounded-lg border border-slate-200 text-slate-600 hover:bg-slate-50 transition-colors"
|
|
226
|
+
>
|
|
227
|
+
⏸ Pause
|
|
228
|
+
</button>
|
|
229
|
+
) : (
|
|
230
|
+
<button
|
|
231
|
+
onClick={onResume}
|
|
232
|
+
className="px-4 py-2 text-sm rounded-lg border border-amber-300 bg-amber-50 text-amber-700 hover:bg-amber-100 transition-colors"
|
|
233
|
+
>
|
|
234
|
+
▶ Resume
|
|
235
|
+
</button>
|
|
236
|
+
)}
|
|
237
|
+
{!transitioning && (
|
|
238
|
+
<button
|
|
239
|
+
onClick={onCancel}
|
|
240
|
+
className="px-4 py-2 text-sm rounded-lg border border-red-200 text-red-600 hover:bg-red-50 transition-colors"
|
|
241
|
+
>
|
|
242
|
+
✕ Cancel
|
|
243
|
+
</button>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|