Skip to content

Commit 0b1b880

Browse files
committed
feat: add free text response options to various wizards and update serialization logic
- Introduced free text response configurations in Nuxt, Python, React, Remix, Svelte, Vue, and security questions. - Enhanced serialization logic to prioritize custom free text responses over preset selections. - Updated wizard state management to accommodate free text responses. - Added tests for free text handling in the wizard to ensure correct functionality and storage.
1 parent 8ba2205 commit 0b1b880

29 files changed

+1659
-70
lines changed

app/new/stack/stack-summary-page.tsx

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,14 @@ import {
1616
} from "@/lib/wizard-config"
1717
import { loadWizardState, persistWizardState } from "@/lib/wizard-storage"
1818
import { buildDefaultSummaryData, buildStepsForStack } from "@/lib/wizard-summary-data"
19-
import type { FileOutputConfig, Responses, WizardQuestion, WizardAnswer, WizardStep } from "@/types/wizard"
19+
import type {
20+
FileOutputConfig,
21+
FreeTextResponses,
22+
Responses,
23+
WizardQuestion,
24+
WizardAnswer,
25+
WizardStep,
26+
} from "@/types/wizard"
2027
import type { GeneratedFileResult } from "@/types/output"
2128
import { WizardEditAnswerDialog } from "@/components/wizard-edit-answer-dialog"
2229

@@ -30,6 +37,7 @@ type StackSummaryPageProps = {
3037
export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) {
3138
const [wizardSteps, setWizardSteps] = useState<WizardStep[] | null>(null)
3239
const [responses, setResponses] = useState<Responses | null>(null)
40+
const [freeTextResponses, setFreeTextResponses] = useState<FreeTextResponses>({})
3341
const [autoFilledMap, setAutoFilledMap] = useState<Record<string, boolean>>({})
3442
const [stackLabel, setStackLabel] = useState<string | null>(null)
3543
const [autoFillNotice, setAutoFillNotice] = useState<string | null>(null)
@@ -53,7 +61,13 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) {
5361
setErrorMessage(null)
5462
try {
5563
if (mode === "default") {
56-
const { steps, responses: defaultResponses, autoFilledMap: defaultsMap, stackLabel: label } =
64+
const {
65+
steps,
66+
responses: defaultResponses,
67+
freeTextResponses: defaultFreeTextResponses,
68+
autoFilledMap: defaultsMap,
69+
stackLabel: label,
70+
} =
5771
await buildDefaultSummaryData(stackId)
5872

5973
if (!isActive) {
@@ -62,6 +76,7 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) {
6276

6377
setWizardSteps(steps)
6478
setResponses(defaultResponses)
79+
setFreeTextResponses(defaultFreeTextResponses)
6580
setAutoFilledMap(defaultsMap)
6681
setStackLabel(label)
6782
setAutoFillNotice("We applied the recommended defaults for you. Tweak any section before generating.")
@@ -70,6 +85,7 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) {
7085
stackId,
7186
stackLabel: label,
7287
responses: defaultResponses,
88+
freeTextResponses: defaultFreeTextResponses,
7389
autoFilledMap: defaultsMap,
7490
updatedAt: Date.now(),
7591
})
@@ -85,6 +101,7 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) {
85101
if (!storedState) {
86102
setWizardSteps(steps)
87103
setResponses(null)
104+
setFreeTextResponses({})
88105
setAutoFilledMap({})
89106
setStackLabel(computedLabel)
90107
setAutoFillNotice(null)
@@ -99,6 +116,7 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) {
99116

100117
setWizardSteps(steps)
101118
setResponses(normalizedResponses)
119+
setFreeTextResponses(storedState.freeTextResponses ?? {})
102120
setAutoFilledMap(storedState.autoFilledMap ?? {})
103121
setStackLabel(storedState.stackLabel ?? computedLabel)
104122
setAutoFillNotice(null)
@@ -133,10 +151,11 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) {
133151
null,
134152
wizardSteps,
135153
responses,
154+
freeTextResponses,
136155
autoFilledMap,
137156
false
138157
)
139-
}, [wizardSteps, responses, autoFilledMap])
158+
}, [wizardSteps, responses, freeTextResponses, autoFilledMap])
140159

141160
const handleGenerate = useCallback(
142161
async (fileOption: FileOutputConfig) => {
@@ -148,7 +167,7 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) {
148167
setGeneratedFile(null)
149168

150169
try {
151-
const payload = serializeWizardResponses(wizardSteps, responses, fileOption.id)
170+
const payload = serializeWizardResponses(wizardSteps, responses, freeTextResponses, fileOption.id)
152171

153172
const result = await generateInstructions({
154173
stackSegment: stackId,
@@ -167,7 +186,7 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) {
167186
setIsGeneratingMap((prev) => ({ ...prev, [fileOption.id]: false }))
168187
}
169188
},
170-
[wizardSteps, responses, stackId]
189+
[wizardSteps, responses, freeTextResponses, stackId]
171190
)
172191

173192
const summaryHeader = stackLabel ??
@@ -187,9 +206,56 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) {
187206
setEditingQuestionId(questionId)
188207
}
189208

190-
const handleCloseEdit = () => {
209+
const handleCloseEdit = useCallback(() => {
191210
setEditingQuestionId(null)
192-
}
211+
}, [])
212+
213+
const applyFreeTextUpdate = useCallback(
214+
(question: WizardQuestion, submittedValue: string) => {
215+
if (!responses) {
216+
return
217+
}
218+
219+
const trimmed = submittedValue.trim()
220+
const nextFreeText: FreeTextResponses = (() => {
221+
if (trimmed.length === 0) {
222+
if (!(question.id in freeTextResponses)) {
223+
return { ...freeTextResponses }
224+
}
225+
226+
const next = { ...freeTextResponses }
227+
delete next[question.id]
228+
return next
229+
}
230+
231+
return {
232+
...freeTextResponses,
233+
[question.id]: trimmed,
234+
}
235+
})()
236+
237+
const nextAutoFilledMap = { ...autoFilledMap }
238+
delete nextAutoFilledMap[question.id]
239+
240+
setFreeTextResponses(nextFreeText)
241+
setAutoFilledMap(nextAutoFilledMap)
242+
setAutoFillNotice(null)
243+
244+
if (stackId) {
245+
persistWizardState({
246+
stackId,
247+
stackLabel: stackLabel ?? summaryHeader,
248+
responses,
249+
freeTextResponses: nextFreeText,
250+
autoFilledMap: nextAutoFilledMap,
251+
updatedAt: Date.now(),
252+
})
253+
}
254+
255+
handleCloseEdit()
256+
},
257+
[responses, freeTextResponses, autoFilledMap, stackId, stackLabel, summaryHeader, handleCloseEdit]
258+
)
193259

194260
const applyAnswerUpdate = useCallback(
195261
(question: WizardQuestion, answer: WizardAnswer) => {
@@ -226,6 +292,7 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) {
226292
stackId,
227293
stackLabel: stackLabel ?? summaryHeader,
228294
responses: currentResponses,
295+
freeTextResponses,
229296
autoFilledMap: currentAutoMap,
230297
updatedAt: Date.now(),
231298
})
@@ -235,7 +302,7 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) {
235302
handleCloseEdit()
236303
}
237304
},
238-
[responses, autoFilledMap, stackId, stackLabel, summaryHeader]
305+
[responses, freeTextResponses, autoFilledMap, stackId, stackLabel, summaryHeader, handleCloseEdit]
239306
)
240307

241308
if (isLoading) {
@@ -386,11 +453,16 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) {
386453
return null
387454
}
388455
const currentValue = responses ? responses[editingQuestion.id] : undefined
456+
const currentFreeText = typeof freeTextResponses[editingQuestion.id] === "string"
457+
? freeTextResponses[editingQuestion.id]
458+
: ""
389459
return (
390460
<WizardEditAnswerDialog
391461
question={editingQuestion}
392462
value={currentValue}
393463
onAnswerSelect={(answer) => applyAnswerUpdate(editingQuestion, answer)}
464+
freeTextValue={currentFreeText}
465+
onFreeTextSave={(nextValue) => applyFreeTextUpdate(editingQuestion, nextValue)}
394466
onClose={handleCloseEdit}
395467
/>
396468
)

0 commit comments

Comments
 (0)