storkit: merge 429_story_interactive_project_setup_wizard_for_new_storkit_projects
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
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 .storkit/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 .storkit/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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user