@umituz/react-native-ai-generation-content 1.23.4 → 1.24.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/package.json +3 -2
- package/src/domain/entities/step-config.types.ts +133 -0
- package/src/domains/scenarios/configs/wizard-configs.ts +187 -0
- package/src/domains/scenarios/index.ts +8 -0
- package/src/domains/wizard/domain/entities/wizard-config.types.ts +214 -0
- package/src/domains/wizard/index.ts +62 -0
- package/src/domains/wizard/infrastructure/builders/dynamic-step-builder.ts +107 -0
- package/src/domains/wizard/infrastructure/renderers/step-renderer.tsx +106 -0
- package/src/domains/wizard/presentation/components/GenericWizardFlow.tsx +189 -0
- package/src/domains/wizard/presentation/hooks/usePhotoUploadState.ts +115 -0
- package/src/domains/wizard/presentation/screens/GenericPhotoUploadScreen.tsx +226 -0
- package/src/domains/wizard/presentation/steps/PhotoUploadStep.tsx +67 -0
- package/src/domains/wizard/presentation/steps/SelectionStep.tsx +244 -0
- package/src/domains/wizard/presentation/steps/TextInputStep.tsx +199 -0
- package/src/features/couple-future/domain/wizard-config.adapter.ts +76 -0
- package/src/features/couple-future/presentation/components/CoupleFutureWizard.tsx +45 -1
- package/src/index.ts +3 -0
- package/src/infrastructure/flow/step-builder.ts +226 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Text Input Step
|
|
3
|
+
* Used by ANY feature that needs text input
|
|
4
|
+
* (text-to-video, prompt-based generation, etc.)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState, useCallback } from "react";
|
|
8
|
+
import { View, StyleSheet, TextInput, TouchableOpacity } from "react-native";
|
|
9
|
+
import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
|
|
10
|
+
import type { TextInputStepConfig } from "../../domain/entities/wizard-config.types";
|
|
11
|
+
|
|
12
|
+
export interface TextInputStepProps {
|
|
13
|
+
readonly config: TextInputStepConfig;
|
|
14
|
+
readonly onContinue: (text: string) => void;
|
|
15
|
+
readonly onBack: () => void;
|
|
16
|
+
readonly t: (key: string) => string;
|
|
17
|
+
readonly translations?: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const TextInputStep: React.FC<TextInputStepProps> = ({
|
|
21
|
+
config,
|
|
22
|
+
onContinue,
|
|
23
|
+
onBack,
|
|
24
|
+
t,
|
|
25
|
+
translations,
|
|
26
|
+
}) => {
|
|
27
|
+
const tokens = useAppDesignTokens();
|
|
28
|
+
const [text, setText] = useState("");
|
|
29
|
+
const [error, setError] = useState<string | null>(null);
|
|
30
|
+
|
|
31
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
32
|
+
console.log("[TextInputStep] Rendering", {
|
|
33
|
+
stepId: config.id,
|
|
34
|
+
textLength: text.length,
|
|
35
|
+
minLength: config.minLength,
|
|
36
|
+
maxLength: config.maxLength,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const handleContinue = useCallback(() => {
|
|
41
|
+
// Validate text length
|
|
42
|
+
if (config.minLength && text.length < config.minLength) {
|
|
43
|
+
setError(t("textInput.errors.tooShort"));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (config.maxLength && text.length > config.maxLength) {
|
|
48
|
+
setError(t("textInput.errors.tooLong"));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (text.trim().length === 0) {
|
|
53
|
+
setError(t("textInput.errors.required"));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
58
|
+
console.log("[TextInputStep] Continue", { stepId: config.id, text });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
onContinue(text);
|
|
62
|
+
}, [text, config, onContinue, t]);
|
|
63
|
+
|
|
64
|
+
const handleTextChange = useCallback((newText: string) => {
|
|
65
|
+
setText(newText);
|
|
66
|
+
setError(null); // Clear error on change
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
|
|
71
|
+
{/* Header */}
|
|
72
|
+
<View style={styles.header}>
|
|
73
|
+
<TouchableOpacity onPress={onBack} style={styles.backButton}>
|
|
74
|
+
<AtomicText type="body">{t("common.back")}</AtomicText>
|
|
75
|
+
</TouchableOpacity>
|
|
76
|
+
</View>
|
|
77
|
+
|
|
78
|
+
{/* Content */}
|
|
79
|
+
<View style={styles.content}>
|
|
80
|
+
{/* Title */}
|
|
81
|
+
{config.titleKey && (
|
|
82
|
+
<AtomicText type="heading2" style={styles.title}>
|
|
83
|
+
{t(config.titleKey)}
|
|
84
|
+
</AtomicText>
|
|
85
|
+
)}
|
|
86
|
+
|
|
87
|
+
{/* Subtitle */}
|
|
88
|
+
{config.subtitleKey && (
|
|
89
|
+
<AtomicText type="body" style={styles.subtitle}>
|
|
90
|
+
{t(config.subtitleKey)}
|
|
91
|
+
</AtomicText>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
{/* Text Input */}
|
|
95
|
+
<TextInput
|
|
96
|
+
style={[
|
|
97
|
+
styles.input,
|
|
98
|
+
config.multiline && styles.multilineInput,
|
|
99
|
+
{
|
|
100
|
+
backgroundColor: tokens.colors.backgroundSecondary,
|
|
101
|
+
borderColor: error ? tokens.colors.error : tokens.colors.border,
|
|
102
|
+
color: tokens.colors.textPrimary,
|
|
103
|
+
},
|
|
104
|
+
]}
|
|
105
|
+
value={text}
|
|
106
|
+
onChangeText={handleTextChange}
|
|
107
|
+
placeholder={config.placeholderKey ? t(config.placeholderKey) : t("textInput.placeholder")}
|
|
108
|
+
placeholderTextColor={tokens.colors.textSecondary}
|
|
109
|
+
multiline={config.multiline}
|
|
110
|
+
numberOfLines={config.multiline ? 5 : 1}
|
|
111
|
+
maxLength={config.maxLength}
|
|
112
|
+
/>
|
|
113
|
+
|
|
114
|
+
{/* Character Count */}
|
|
115
|
+
{config.maxLength && (
|
|
116
|
+
<AtomicText type="caption" style={styles.characterCount}>
|
|
117
|
+
{text.length} / {config.maxLength}
|
|
118
|
+
</AtomicText>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{/* Error Message */}
|
|
122
|
+
{error && (
|
|
123
|
+
<AtomicText type="body" style={[styles.error, { color: tokens.colors.error }]}>
|
|
124
|
+
{error}
|
|
125
|
+
</AtomicText>
|
|
126
|
+
)}
|
|
127
|
+
</View>
|
|
128
|
+
|
|
129
|
+
{/* Continue Button */}
|
|
130
|
+
<View style={styles.footer}>
|
|
131
|
+
<TouchableOpacity
|
|
132
|
+
style={[
|
|
133
|
+
styles.continueButton,
|
|
134
|
+
{
|
|
135
|
+
backgroundColor: text.trim().length > 0 ? tokens.colors.primary : tokens.colors.disabled,
|
|
136
|
+
},
|
|
137
|
+
]}
|
|
138
|
+
onPress={handleContinue}
|
|
139
|
+
disabled={text.trim().length === 0}
|
|
140
|
+
>
|
|
141
|
+
<AtomicText
|
|
142
|
+
type="buttonLarge"
|
|
143
|
+
style={{ color: text.trim().length > 0 ? tokens.colors.textOnPrimary : tokens.colors.textDisabled }}
|
|
144
|
+
>
|
|
145
|
+
{t("common.continue")}
|
|
146
|
+
</AtomicText>
|
|
147
|
+
</TouchableOpacity>
|
|
148
|
+
</View>
|
|
149
|
+
</View>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const styles = StyleSheet.create({
|
|
154
|
+
container: {
|
|
155
|
+
flex: 1,
|
|
156
|
+
},
|
|
157
|
+
header: {
|
|
158
|
+
padding: 16,
|
|
159
|
+
},
|
|
160
|
+
backButton: {
|
|
161
|
+
alignSelf: "flex-start",
|
|
162
|
+
},
|
|
163
|
+
content: {
|
|
164
|
+
flex: 1,
|
|
165
|
+
padding: 16,
|
|
166
|
+
},
|
|
167
|
+
title: {
|
|
168
|
+
marginBottom: 8,
|
|
169
|
+
},
|
|
170
|
+
subtitle: {
|
|
171
|
+
marginBottom: 24,
|
|
172
|
+
},
|
|
173
|
+
input: {
|
|
174
|
+
borderWidth: 1,
|
|
175
|
+
borderRadius: 8,
|
|
176
|
+
padding: 12,
|
|
177
|
+
fontSize: 16,
|
|
178
|
+
marginBottom: 8,
|
|
179
|
+
},
|
|
180
|
+
multilineInput: {
|
|
181
|
+
height: 120,
|
|
182
|
+
textAlignVertical: "top",
|
|
183
|
+
},
|
|
184
|
+
characterCount: {
|
|
185
|
+
textAlign: "right",
|
|
186
|
+
marginBottom: 8,
|
|
187
|
+
},
|
|
188
|
+
error: {
|
|
189
|
+
marginTop: 8,
|
|
190
|
+
},
|
|
191
|
+
footer: {
|
|
192
|
+
padding: 16,
|
|
193
|
+
},
|
|
194
|
+
continueButton: {
|
|
195
|
+
padding: 16,
|
|
196
|
+
borderRadius: 8,
|
|
197
|
+
alignItems: "center",
|
|
198
|
+
},
|
|
199
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wizard Configuration Adapter
|
|
3
|
+
* Adapts old wizard config to new dynamic step system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { buildStepsFromScenario, buildStepsWithNavigation, SCENARIO_CONFIGS } from "../../../infrastructure/flow/step-builder";
|
|
7
|
+
import type { DynamicStepDefinition } from "../../../domain/entities/step-config.types";
|
|
8
|
+
import type { WizardScenarioData } from "../presentation/types";
|
|
9
|
+
import { StepType } from "../../../domain/entities/flow-config.types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build steps for a scenario
|
|
13
|
+
* Returns dynamically generated step definitions based on scenario type
|
|
14
|
+
*/
|
|
15
|
+
export const buildStepsForScenario = (scenario: WizardScenarioData | undefined): DynamicStepDefinition[] => {
|
|
16
|
+
if (!scenario) {
|
|
17
|
+
// No scenario selected - only show scenario selection
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
id: "SCENARIO_SELECTION",
|
|
21
|
+
type: StepType.SCENARIO_SELECTION,
|
|
22
|
+
required: true,
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const steps: DynamicStepDefinition[] = [];
|
|
28
|
+
|
|
29
|
+
// Always start with scenario preview if scenario is selected
|
|
30
|
+
steps.push({
|
|
31
|
+
id: "SCENARIO_PREVIEW",
|
|
32
|
+
type: StepType.SCENARIO_PREVIEW,
|
|
33
|
+
required: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Get scenario-specific config
|
|
37
|
+
const scenarioConfig = SCENARIO_CONFIGS[scenario.category || ""] || SCENARIO_CONFIGS["romantic-kiss"];
|
|
38
|
+
|
|
39
|
+
// Build dynamic steps from config
|
|
40
|
+
const dynamicSteps = buildStepsFromScenario(scenario.id, scenarioConfig);
|
|
41
|
+
steps.push(...dynamicSteps);
|
|
42
|
+
|
|
43
|
+
// Add generating step
|
|
44
|
+
steps.push({
|
|
45
|
+
id: "GENERATING",
|
|
46
|
+
type: StepType.GENERATING,
|
|
47
|
+
required: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Link steps together
|
|
51
|
+
return buildStepsWithNavigation(steps);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get photo upload count for a scenario
|
|
56
|
+
* Used to determine how many partner images are needed
|
|
57
|
+
*/
|
|
58
|
+
export const getPhotoUploadCount = (scenario: WizardScenarioData | undefined): number => {
|
|
59
|
+
if (!scenario) return 0;
|
|
60
|
+
|
|
61
|
+
const scenarioConfig = SCENARIO_CONFIGS[scenario.category || ""] || SCENARIO_CONFIGS["romantic-kiss"];
|
|
62
|
+
return scenarioConfig.photoUploads?.count ?? 0;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get photo upload label for a specific index
|
|
67
|
+
*/
|
|
68
|
+
export const getPhotoUploadLabel = (
|
|
69
|
+
scenario: WizardScenarioData | undefined,
|
|
70
|
+
index: number,
|
|
71
|
+
): string => {
|
|
72
|
+
if (!scenario) return `Photo ${index + 1}`;
|
|
73
|
+
|
|
74
|
+
const scenarioConfig = SCENARIO_CONFIGS[scenario.category || ""] || SCENARIO_CONFIGS["romantic-kiss"];
|
|
75
|
+
return scenarioConfig.photoUploads?.labels?.[index] || `Photo ${index + 1}`;
|
|
76
|
+
};
|
|
@@ -33,6 +33,15 @@ export const CoupleFutureWizard: React.FC<CoupleFutureWizardProps> = ({
|
|
|
33
33
|
}) => {
|
|
34
34
|
const tokens = useAppDesignTokens();
|
|
35
35
|
|
|
36
|
+
// DEBUG: Log every render
|
|
37
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
38
|
+
console.log("[CoupleFutureWizard] RENDER", {
|
|
39
|
+
hasData: !!data,
|
|
40
|
+
hasSelectedScenario: !!data.selectedScenario,
|
|
41
|
+
scenarioId: data.selectedScenario?.id,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
const stepDefinitions = useMemo(
|
|
37
46
|
() => createStepDefinitions(config),
|
|
38
47
|
[config],
|
|
@@ -48,6 +57,16 @@ export const CoupleFutureWizard: React.FC<CoupleFutureWizardProps> = ({
|
|
|
48
57
|
initialStepIndex,
|
|
49
58
|
});
|
|
50
59
|
|
|
60
|
+
// DEBUG: Log flow state
|
|
61
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
62
|
+
console.log("[CoupleFutureWizard] FLOW STATE", {
|
|
63
|
+
currentStepIndex: flow.currentStepIndex,
|
|
64
|
+
currentStepId: flow.currentStep?.id,
|
|
65
|
+
currentStepType: flow.currentStep?.type,
|
|
66
|
+
totalSteps: stepDefinitions.length,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
51
70
|
// Notify parent app when step changes
|
|
52
71
|
// NOTE: Do NOT include callbacks in dependencies - causes infinite loop!
|
|
53
72
|
// Parent app re-renders when onStepChange updates state, which recreates callbacks,
|
|
@@ -107,10 +126,26 @@ export const CoupleFutureWizard: React.FC<CoupleFutureWizardProps> = ({
|
|
|
107
126
|
|
|
108
127
|
const renderCurrentStep = useCallback(() => {
|
|
109
128
|
const step = flow.currentStep;
|
|
110
|
-
if (!step)
|
|
129
|
+
if (!step) {
|
|
130
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
131
|
+
console.log("[CoupleFutureWizard] renderCurrentStep: NO STEP");
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
137
|
+
console.log("[CoupleFutureWizard] renderCurrentStep", {
|
|
138
|
+
stepId: step.id,
|
|
139
|
+
stepType: step.type,
|
|
140
|
+
stepIndex: flow.currentStepIndex,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
111
143
|
|
|
112
144
|
switch (step.type) {
|
|
113
145
|
case StepType.SCENARIO_SELECTION:
|
|
146
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
147
|
+
console.log("[CoupleFutureWizard] Rendering SCENARIO_SELECTION (null)");
|
|
148
|
+
}
|
|
114
149
|
return null; // Rendered by parent via CategoryNavigationContainer
|
|
115
150
|
|
|
116
151
|
case StepType.SCENARIO_PREVIEW:
|
|
@@ -140,6 +175,15 @@ export const CoupleFutureWizard: React.FC<CoupleFutureWizardProps> = ({
|
|
|
140
175
|
: translations.partnerB;
|
|
141
176
|
const partnerConfig = isPartnerA ? config.partnerA : config.partnerB;
|
|
142
177
|
|
|
178
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
179
|
+
console.log("[CoupleFutureWizard] Rendering PartnerStepScreen", {
|
|
180
|
+
isPartnerA,
|
|
181
|
+
stepId: step.id,
|
|
182
|
+
hasConfig: !!partnerConfig,
|
|
183
|
+
hasTranslations: !!partnerTranslations,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
143
187
|
return (
|
|
144
188
|
<PartnerStepScreen
|
|
145
189
|
key={isPartnerA ? "a" : "b"}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic Step Builder
|
|
3
|
+
* Builds step definitions from configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { StepType } from "../../domain/entities/flow-config.types";
|
|
7
|
+
import type {
|
|
8
|
+
ScenarioStepConfig,
|
|
9
|
+
DynamicStepDefinition,
|
|
10
|
+
BuiltStep,
|
|
11
|
+
PhotoUploadStepConfig,
|
|
12
|
+
} from "../../domain/entities/step-config.types";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build steps from scenario configuration
|
|
16
|
+
* Automatically creates photo upload steps, text input, etc. based on config
|
|
17
|
+
*/
|
|
18
|
+
export const buildStepsFromScenario = (
|
|
19
|
+
scenarioId: string,
|
|
20
|
+
config: ScenarioStepConfig,
|
|
21
|
+
): DynamicStepDefinition[] => {
|
|
22
|
+
const steps: DynamicStepDefinition[] = [];
|
|
23
|
+
|
|
24
|
+
// Add photo upload steps (dynamic count)
|
|
25
|
+
if (config.photoUploads && config.photoUploads.count > 0) {
|
|
26
|
+
for (let i = 0; i < config.photoUploads.count; i++) {
|
|
27
|
+
const photoConfig: PhotoUploadStepConfig = {
|
|
28
|
+
id: `PHOTO_UPLOAD_${i}`,
|
|
29
|
+
label: config.photoUploads.labels?.[i] || `Photo ${i + 1}`,
|
|
30
|
+
showFaceDetection: config.photoUploads.showFaceDetection ?? false,
|
|
31
|
+
showNameInput: config.photoUploads.showNameInput ?? false,
|
|
32
|
+
showPhotoTips: true,
|
|
33
|
+
required: true,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
steps.push({
|
|
37
|
+
id: `PHOTO_UPLOAD_${i}`,
|
|
38
|
+
type: StepType.PARTNER_UPLOAD, // Reuse existing type
|
|
39
|
+
config: photoConfig,
|
|
40
|
+
required: true,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Add text input step
|
|
46
|
+
if (config.textInput?.enabled) {
|
|
47
|
+
steps.push({
|
|
48
|
+
id: "TEXT_INPUT",
|
|
49
|
+
type: StepType.TEXT_INPUT,
|
|
50
|
+
config: {
|
|
51
|
+
id: "TEXT_INPUT",
|
|
52
|
+
minLength: config.textInput.minLength ?? 0,
|
|
53
|
+
maxLength: config.textInput.maxLength ?? 500,
|
|
54
|
+
required: config.textInput.required ?? false,
|
|
55
|
+
},
|
|
56
|
+
required: config.textInput.required ?? false,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Add style selection step
|
|
61
|
+
if (config.styleSelection?.enabled) {
|
|
62
|
+
steps.push({
|
|
63
|
+
id: "STYLE_SELECTION",
|
|
64
|
+
type: StepType.FEATURE_SELECTION,
|
|
65
|
+
config: {
|
|
66
|
+
id: "STYLE_SELECTION",
|
|
67
|
+
styles: config.styleSelection.styles ?? [],
|
|
68
|
+
required: config.styleSelection.required ?? false,
|
|
69
|
+
},
|
|
70
|
+
required: config.styleSelection.required ?? false,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Add duration selection step
|
|
75
|
+
if (config.durationSelection?.enabled) {
|
|
76
|
+
steps.push({
|
|
77
|
+
id: "DURATION_SELECTION",
|
|
78
|
+
type: StepType.FEATURE_SELECTION,
|
|
79
|
+
config: {
|
|
80
|
+
id: "DURATION_SELECTION",
|
|
81
|
+
durations: config.durationSelection.durations ?? [5, 10, 15],
|
|
82
|
+
required: config.durationSelection.required ?? false,
|
|
83
|
+
},
|
|
84
|
+
required: config.durationSelection.required ?? false,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return steps;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build steps with conditional navigation
|
|
93
|
+
* Allows data-driven next step decisions
|
|
94
|
+
*/
|
|
95
|
+
export const buildStepsWithNavigation = (
|
|
96
|
+
baseSteps: DynamicStepDefinition[],
|
|
97
|
+
): DynamicStepDefinition[] => {
|
|
98
|
+
return baseSteps.map((step, index) => {
|
|
99
|
+
// Auto-link to next step if not specified
|
|
100
|
+
if (!step.nextStep && index < baseSteps.length - 1) {
|
|
101
|
+
return {
|
|
102
|
+
...step,
|
|
103
|
+
nextStep: baseSteps[index + 1].id,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return step;
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get next step ID based on configuration and state
|
|
112
|
+
*/
|
|
113
|
+
export const resolveNextStep = (
|
|
114
|
+
currentStepId: string,
|
|
115
|
+
steps: readonly DynamicStepDefinition[],
|
|
116
|
+
context: {
|
|
117
|
+
readonly values: Record<string, unknown>;
|
|
118
|
+
readonly completedSteps: readonly string[];
|
|
119
|
+
},
|
|
120
|
+
): string | null => {
|
|
121
|
+
const currentStep = steps.find((s) => s.id === currentStepId);
|
|
122
|
+
if (!currentStep) return null;
|
|
123
|
+
|
|
124
|
+
// Check if should skip based on skipIf condition
|
|
125
|
+
const nextStepCandidates = steps.filter((s) => {
|
|
126
|
+
if (s.skipIf && s.skipIf({ values: context.values })) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// If nextStep is a function, call it
|
|
133
|
+
if (typeof currentStep.nextStep === "function") {
|
|
134
|
+
return currentStep.nextStep({
|
|
135
|
+
values: context.values,
|
|
136
|
+
currentStepId,
|
|
137
|
+
completedSteps: context.completedSteps,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// If nextStep is a string, return it
|
|
142
|
+
if (typeof currentStep.nextStep === "string") {
|
|
143
|
+
return currentStep.nextStep;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Default: next step in array
|
|
147
|
+
const currentIndex = steps.findIndex((s) => s.id === currentStepId);
|
|
148
|
+
if (currentIndex >= 0 && currentIndex < steps.length - 1) {
|
|
149
|
+
return steps[currentIndex + 1].id;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return null;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Example scenario configurations
|
|
157
|
+
*/
|
|
158
|
+
export const SCENARIO_CONFIGS: Record<string, ScenarioStepConfig> = {
|
|
159
|
+
// Couple/Partner scenarios (2 photos)
|
|
160
|
+
"romantic-kiss": {
|
|
161
|
+
photoUploads: {
|
|
162
|
+
count: 2,
|
|
163
|
+
labels: ["First Partner", "Second Partner"],
|
|
164
|
+
showFaceDetection: true,
|
|
165
|
+
showNameInput: false,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
"couple-hug": {
|
|
169
|
+
photoUploads: {
|
|
170
|
+
count: 2,
|
|
171
|
+
labels: ["Partner A", "Partner B"],
|
|
172
|
+
showFaceDetection: true,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
// Single photo scenarios
|
|
177
|
+
"image-to-video": {
|
|
178
|
+
photoUploads: {
|
|
179
|
+
count: 1,
|
|
180
|
+
labels: ["Your Photo"],
|
|
181
|
+
showFaceDetection: false,
|
|
182
|
+
},
|
|
183
|
+
styleSelection: {
|
|
184
|
+
enabled: true,
|
|
185
|
+
required: true,
|
|
186
|
+
},
|
|
187
|
+
durationSelection: {
|
|
188
|
+
enabled: true,
|
|
189
|
+
required: true,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
// Text-only scenarios
|
|
194
|
+
"text-to-video": {
|
|
195
|
+
textInput: {
|
|
196
|
+
enabled: true,
|
|
197
|
+
required: true,
|
|
198
|
+
minLength: 10,
|
|
199
|
+
maxLength: 500,
|
|
200
|
+
},
|
|
201
|
+
styleSelection: {
|
|
202
|
+
enabled: true,
|
|
203
|
+
required: true,
|
|
204
|
+
},
|
|
205
|
+
durationSelection: {
|
|
206
|
+
enabled: true,
|
|
207
|
+
required: true,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
// Complex scenario with optional steps
|
|
212
|
+
"advanced-generation": {
|
|
213
|
+
photoUploads: {
|
|
214
|
+
count: 1,
|
|
215
|
+
labels: ["Base Image"],
|
|
216
|
+
},
|
|
217
|
+
textInput: {
|
|
218
|
+
enabled: true,
|
|
219
|
+
required: false, // Optional
|
|
220
|
+
},
|
|
221
|
+
styleSelection: {
|
|
222
|
+
enabled: true,
|
|
223
|
+
required: true,
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
};
|