2026-03-28 13:26:29 +00:00
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 :
2026-04-03 16:12:52 +01:00
"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" ,
2026-03-28 13:26:29 +00:00
stack :
2026-04-03 16:12:52 +01:00
"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" ,
2026-03-28 13:26:29 +00:00
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 >
) ;
}