@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.
@@ -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) return null;
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
@@ -219,3 +219,6 @@ export type {
219
219
  FlowUploadedImageData,
220
220
  FlowGenerationStatus,
221
221
  } from "./domain/entities/flow-config.types";
222
+
223
+ // Wizard Domain - Generic, configuration-driven wizard system for ALL features
224
+ export * from "./domains/wizard";
@@ -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
+ };