2d8ccb3eb6
Rename all references from storkit to huskies across the codebase: - .storkit/ directory → .huskies/ - Binary name, Cargo package name, Docker image references - Server code, frontend code, config files, scripts - Fix script/test to build frontend before cargo clippy/test so merge worktrees have frontend/dist available for RustEmbed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
355 lines
8.9 KiB
TypeScript
355 lines
8.9 KiB
TypeScript
import { useCallback, useState } from "react";
|
|
import type { WizardStateData, WizardStepInfo } from "../api/client";
|
|
|
|
const API_BASE = "/api";
|
|
|
|
interface SetupWizardProps {
|
|
wizardState: WizardStateData;
|
|
onWizardUpdate: (state: WizardStateData) => void;
|
|
sendMessage: (message: string) => void;
|
|
}
|
|
|
|
/** Style constants for the wizard UI. */
|
|
const STEP_BG_PENDING = "#1a1f2e";
|
|
const STEP_BG_ACTIVE = "#1c2a1c";
|
|
const STEP_BG_DONE = "#1a2a1a";
|
|
const STEP_BORDER_PENDING = "#2a2f3e";
|
|
const STEP_BORDER_ACTIVE = "#2d4a2d";
|
|
const STEP_BORDER_DONE = "#2d4a2d";
|
|
const COLOR_LABEL = "#ccc";
|
|
const COLOR_LABEL_DONE = "#a0d4a0";
|
|
const COLOR_ACCENT = "#a0d4a0";
|
|
|
|
function statusIcon(status: string): string {
|
|
switch (status) {
|
|
case "confirmed":
|
|
return "\u2713";
|
|
case "skipped":
|
|
return "\u2013";
|
|
case "generating":
|
|
return "\u2026";
|
|
case "awaiting_confirmation":
|
|
return "?";
|
|
default:
|
|
return "\u00B7";
|
|
}
|
|
}
|
|
|
|
function stepBackground(status: string, isActive: boolean): string {
|
|
if (status === "confirmed" || status === "skipped") return STEP_BG_DONE;
|
|
if (isActive) return STEP_BG_ACTIVE;
|
|
return STEP_BG_PENDING;
|
|
}
|
|
|
|
function stepBorder(status: string, isActive: boolean): string {
|
|
if (status === "confirmed" || status === "skipped") return STEP_BORDER_DONE;
|
|
if (isActive) return STEP_BORDER_ACTIVE;
|
|
return STEP_BORDER_PENDING;
|
|
}
|
|
|
|
/** Messages sent to the chat to trigger agent generation for each step. */
|
|
const STEP_PROMPTS: Record<string, string> = {
|
|
context:
|
|
"Read the codebase and generate .huskies/specs/00_CONTEXT.md with a project context spec. Include High-Level Goal, Core Features, Domain Definition, and Glossary sections. Then call the wizard API to store the content: PUT /api/wizard/step/context/content",
|
|
stack:
|
|
"Read the tech stack and generate .huskies/specs/tech/STACK.md with a tech stack spec. Include Core Stack, Coding Standards, Quality Gates, and Libraries sections. Then call the wizard API to store the content: PUT /api/wizard/step/stack/content",
|
|
test_script:
|
|
"Read the project structure and create script/test — a bash script that runs the project's actual test suite. Then call the wizard API: PUT /api/wizard/step/test_script/content",
|
|
release_script:
|
|
"Read the project's deployment setup and create script/release tailored to the project. Then call the wizard API: PUT /api/wizard/step/release_script/content",
|
|
test_coverage:
|
|
"If the stack supports coverage reporting, create script/test_coverage. Then call the wizard API: PUT /api/wizard/step/test_coverage/content",
|
|
};
|
|
|
|
async function apiPost(path: string): Promise<WizardStateData | null> {
|
|
try {
|
|
const resp = await fetch(`${API_BASE}${path}`, { method: "POST" });
|
|
if (!resp.ok) return null;
|
|
return (await resp.json()) as WizardStateData;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function StepCard({
|
|
step,
|
|
isActive,
|
|
onGenerate,
|
|
onConfirm,
|
|
onSkip,
|
|
}: {
|
|
step: WizardStepInfo;
|
|
isActive: boolean;
|
|
onGenerate: () => void;
|
|
onConfirm: () => void;
|
|
onSkip: () => void;
|
|
}) {
|
|
const isDone = step.status === "confirmed" || step.status === "skipped";
|
|
|
|
return (
|
|
<div
|
|
data-testid={`wizard-step-${step.step}`}
|
|
style={{
|
|
padding: "16px",
|
|
borderRadius: "8px",
|
|
background: stepBackground(step.status, isActive),
|
|
border: `1px solid ${stepBorder(step.status, isActive)}`,
|
|
opacity: !isActive && !isDone ? 0.5 : 1,
|
|
transition: "all 0.2s ease",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "12px",
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
width: "24px",
|
|
height: "24px",
|
|
borderRadius: "50%",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
fontSize: "14px",
|
|
fontWeight: 600,
|
|
background: isDone ? COLOR_ACCENT : "transparent",
|
|
border: isDone ? "none" : `1px solid ${COLOR_LABEL}`,
|
|
color: isDone ? "#1a1a1a" : COLOR_LABEL,
|
|
}}
|
|
>
|
|
{statusIcon(step.status)}
|
|
</span>
|
|
<span
|
|
style={{
|
|
flex: 1,
|
|
color: isDone ? COLOR_LABEL_DONE : COLOR_LABEL,
|
|
fontWeight: isActive ? 600 : 400,
|
|
}}
|
|
>
|
|
{step.label}
|
|
</span>
|
|
{isActive && step.status === "pending" && (
|
|
<button
|
|
type="button"
|
|
data-testid={`wizard-generate-${step.step}`}
|
|
onClick={onGenerate}
|
|
style={{
|
|
padding: "6px 14px",
|
|
borderRadius: "6px",
|
|
border: "none",
|
|
backgroundColor: COLOR_ACCENT,
|
|
color: "#1a1a1a",
|
|
cursor: "pointer",
|
|
fontSize: "0.85rem",
|
|
fontWeight: 600,
|
|
}}
|
|
>
|
|
Generate
|
|
</button>
|
|
)}
|
|
{isActive && step.status === "generating" && (
|
|
<span style={{ color: "#aaa", fontSize: "0.85rem" }}>
|
|
Generating...
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{step.content && step.status === "awaiting_confirmation" && (
|
|
<div style={{ marginTop: "12px" }}>
|
|
<pre
|
|
data-testid={`wizard-preview-${step.step}`}
|
|
style={{
|
|
background: "#111",
|
|
padding: "12px",
|
|
borderRadius: "6px",
|
|
fontSize: "0.8rem",
|
|
color: "#ddd",
|
|
whiteSpace: "pre-wrap",
|
|
maxHeight: "200px",
|
|
overflow: "auto",
|
|
margin: "0 0 12px 0",
|
|
}}
|
|
>
|
|
{step.content}
|
|
</pre>
|
|
<div style={{ display: "flex", gap: "8px" }}>
|
|
<button
|
|
type="button"
|
|
data-testid={`wizard-confirm-${step.step}`}
|
|
onClick={onConfirm}
|
|
style={{
|
|
padding: "6px 14px",
|
|
borderRadius: "6px",
|
|
border: "none",
|
|
backgroundColor: COLOR_ACCENT,
|
|
color: "#1a1a1a",
|
|
cursor: "pointer",
|
|
fontSize: "0.85rem",
|
|
fontWeight: 600,
|
|
}}
|
|
>
|
|
Confirm
|
|
</button>
|
|
<button
|
|
type="button"
|
|
data-testid={`wizard-revise-${step.step}`}
|
|
onClick={onGenerate}
|
|
style={{
|
|
padding: "6px 14px",
|
|
borderRadius: "6px",
|
|
border: "1px solid #555",
|
|
backgroundColor: "transparent",
|
|
color: "#ccc",
|
|
cursor: "pointer",
|
|
fontSize: "0.85rem",
|
|
}}
|
|
>
|
|
Revise
|
|
</button>
|
|
<button
|
|
type="button"
|
|
data-testid={`wizard-skip-${step.step}`}
|
|
onClick={onSkip}
|
|
style={{
|
|
padding: "6px 14px",
|
|
borderRadius: "6px",
|
|
border: "1px solid #555",
|
|
backgroundColor: "transparent",
|
|
color: "#888",
|
|
cursor: "pointer",
|
|
fontSize: "0.85rem",
|
|
}}
|
|
>
|
|
Skip
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isActive && step.status === "pending" && !step.content && (
|
|
<div style={{ marginTop: "8px", display: "flex", gap: "8px" }}>
|
|
<button
|
|
type="button"
|
|
data-testid={`wizard-skip-${step.step}`}
|
|
onClick={onSkip}
|
|
style={{
|
|
padding: "4px 10px",
|
|
borderRadius: "6px",
|
|
border: "1px solid #444",
|
|
backgroundColor: "transparent",
|
|
color: "#888",
|
|
cursor: "pointer",
|
|
fontSize: "0.8rem",
|
|
}}
|
|
>
|
|
Skip this step
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function SetupWizard({
|
|
wizardState,
|
|
onWizardUpdate,
|
|
sendMessage,
|
|
}: SetupWizardProps) {
|
|
const [, setRefreshKey] = useState(0);
|
|
|
|
const handleGenerate = useCallback(
|
|
(step: WizardStepInfo) => {
|
|
const prompt = STEP_PROMPTS[step.step];
|
|
if (prompt) {
|
|
sendMessage(prompt);
|
|
}
|
|
},
|
|
[sendMessage],
|
|
);
|
|
|
|
const handleConfirm = useCallback(
|
|
async (step: WizardStepInfo) => {
|
|
const result = await apiPost(`/wizard/step/${step.step}/confirm`);
|
|
if (result) {
|
|
onWizardUpdate(result);
|
|
setRefreshKey((k) => k + 1);
|
|
}
|
|
},
|
|
[onWizardUpdate],
|
|
);
|
|
|
|
const handleSkip = useCallback(
|
|
async (step: WizardStepInfo) => {
|
|
const result = await apiPost(`/wizard/step/${step.step}/skip`);
|
|
if (result) {
|
|
onWizardUpdate(result);
|
|
setRefreshKey((k) => k + 1);
|
|
}
|
|
},
|
|
[onWizardUpdate],
|
|
);
|
|
|
|
if (wizardState.completed) {
|
|
return (
|
|
<div
|
|
data-testid="wizard-complete"
|
|
style={{
|
|
padding: "24px",
|
|
borderRadius: "12px",
|
|
background: STEP_BG_DONE,
|
|
border: `1px solid ${STEP_BORDER_DONE}`,
|
|
textAlign: "center",
|
|
}}
|
|
>
|
|
<h3 style={{ margin: "0 0 8px 0", color: COLOR_ACCENT }}>
|
|
Setup Complete
|
|
</h3>
|
|
<p style={{ margin: 0, color: COLOR_LABEL }}>
|
|
Your project is configured. You can start writing stories.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
data-testid="setup-wizard"
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "12px",
|
|
}}
|
|
>
|
|
<div style={{ marginBottom: "8px" }}>
|
|
<h3
|
|
style={{
|
|
margin: "0 0 4px 0",
|
|
color: COLOR_ACCENT,
|
|
fontSize: "1.1rem",
|
|
}}
|
|
>
|
|
Project Setup Wizard
|
|
</h3>
|
|
<p style={{ margin: 0, color: "#999", fontSize: "0.85rem" }}>
|
|
Step {wizardState.current_step_index + 1} of{" "}
|
|
{wizardState.steps.length}
|
|
</p>
|
|
</div>
|
|
|
|
{wizardState.steps.map((step, idx) => (
|
|
<StepCard
|
|
key={step.step}
|
|
step={step}
|
|
isActive={idx === wizardState.current_step_index}
|
|
onGenerate={() => handleGenerate(step)}
|
|
onConfirm={() => handleConfirm(step)}
|
|
onSkip={() => handleSkip(step)}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|