@umituz/react-native-ai-generation-content 1.26.10 → 1.26.12
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 +1 -1
- package/src/domains/generation/wizard/index.ts +7 -0
- package/src/domains/generation/wizard/presentation/components/GenericWizardFlow.tsx +293 -0
- package/src/domains/generation/wizard/presentation/components/index.ts +2 -0
- package/src/domains/generation/wizard/presentation/screens/GeneratingScreen.tsx +123 -0
- package/src/domains/generation/wizard/presentation/screens/index.ts +1 -0
- package/src/domains/scenarios/domain/scenario.types.ts +39 -0
- package/src/domains/scenarios/index.ts +23 -0
- package/src/domains/scenarios/presentation/containers/CategoryNavigationContainer.tsx +191 -0
- package/src/domains/scenarios/presentation/containers/index.ts +2 -0
- package/src/domains/scenarios/presentation/screens/HierarchicalScenarioListScreen.tsx +291 -0
- package/src/domains/scenarios/presentation/screens/MainCategoryScreen.tsx +198 -0
- package/src/domains/scenarios/presentation/screens/ScenarioPreviewScreen.tsx +164 -0
- package/src/domains/scenarios/presentation/screens/SubCategoryScreen.tsx +216 -0
- package/src/domains/scenarios/presentation/screens/index.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.12",
|
|
4
4
|
"description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -42,3 +42,10 @@ export type {
|
|
|
42
42
|
WizardScenarioData,
|
|
43
43
|
WizardOutputType,
|
|
44
44
|
} from "./presentation/hooks/useWizardGeneration";
|
|
45
|
+
|
|
46
|
+
// Presentation - Components
|
|
47
|
+
export { GenericWizardFlow } from "./presentation/components";
|
|
48
|
+
export type { GenericWizardFlowProps } from "./presentation/components";
|
|
49
|
+
|
|
50
|
+
// Presentation - Screens
|
|
51
|
+
export { GeneratingScreen } from "./presentation/screens";
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Wizard Flow Component
|
|
3
|
+
* ONE wizard to rule them all!
|
|
4
|
+
*
|
|
5
|
+
* Works for:
|
|
6
|
+
* - Couple features (romantic-kiss, ai-hug, etc.)
|
|
7
|
+
* - Face swap
|
|
8
|
+
* - Image-to-video
|
|
9
|
+
* - Text-to-video
|
|
10
|
+
* - ANY future feature!
|
|
11
|
+
*
|
|
12
|
+
* NO feature-specific code here - everything driven by configuration!
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React, { useMemo, useCallback, useEffect, useRef } from "react";
|
|
16
|
+
import { View, StyleSheet } from "react-native";
|
|
17
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
18
|
+
import { useFlow } from "../../../infrastructure/flow/useFlow";
|
|
19
|
+
import { StepType, type StepDefinition } from "../../../../../domain/entities/flow-config.types";
|
|
20
|
+
import type { WizardFeatureConfig } from "../../domain/entities/wizard-config.types";
|
|
21
|
+
import { buildFlowStepsFromWizard } from "../../infrastructure/builders/dynamic-step-builder";
|
|
22
|
+
import { useWizardGeneration, type WizardScenarioData } from "../hooks/useWizardGeneration";
|
|
23
|
+
import type { AlertMessages } from "../../../../../presentation/hooks/generation/types";
|
|
24
|
+
|
|
25
|
+
export interface GenericWizardFlowProps {
|
|
26
|
+
readonly featureConfig: WizardFeatureConfig;
|
|
27
|
+
readonly scenario?: WizardScenarioData;
|
|
28
|
+
readonly userId?: string;
|
|
29
|
+
readonly alertMessages?: AlertMessages;
|
|
30
|
+
readonly onStepChange?: (stepId: string, stepType: StepType | string) => void;
|
|
31
|
+
readonly onGenerationStart?: (data: Record<string, unknown>, proceedToGenerating: () => void) => void;
|
|
32
|
+
readonly onGenerationComplete?: (result: unknown) => void;
|
|
33
|
+
readonly onGenerationError?: (error: string) => void;
|
|
34
|
+
readonly onCreditsExhausted?: () => void;
|
|
35
|
+
readonly onBack?: () => void;
|
|
36
|
+
readonly t: (key: string) => string;
|
|
37
|
+
readonly translations?: Record<string, string>;
|
|
38
|
+
readonly renderPreview?: (onContinue: () => void) => React.ReactElement | null;
|
|
39
|
+
readonly renderGenerating?: (progress: number) => React.ReactElement | null;
|
|
40
|
+
readonly renderResult?: (result: unknown) => React.ReactElement | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
|
|
44
|
+
featureConfig,
|
|
45
|
+
scenario,
|
|
46
|
+
userId,
|
|
47
|
+
alertMessages,
|
|
48
|
+
onStepChange,
|
|
49
|
+
onGenerationStart,
|
|
50
|
+
onGenerationComplete,
|
|
51
|
+
onGenerationError,
|
|
52
|
+
onCreditsExhausted,
|
|
53
|
+
onBack,
|
|
54
|
+
t,
|
|
55
|
+
translations,
|
|
56
|
+
renderPreview,
|
|
57
|
+
renderGenerating,
|
|
58
|
+
renderResult,
|
|
59
|
+
}) => {
|
|
60
|
+
const tokens = useAppDesignTokens();
|
|
61
|
+
|
|
62
|
+
// Build flow steps from wizard config
|
|
63
|
+
const flowSteps = useMemo<StepDefinition[]>(() => {
|
|
64
|
+
return buildFlowStepsFromWizard(featureConfig, {
|
|
65
|
+
includePreview: !!renderPreview,
|
|
66
|
+
includeGenerating: !!renderGenerating,
|
|
67
|
+
});
|
|
68
|
+
}, [featureConfig, renderPreview, renderGenerating]);
|
|
69
|
+
|
|
70
|
+
// Initialize flow and destructure to prevent infinite loops
|
|
71
|
+
const flow = useFlow({
|
|
72
|
+
steps: flowSteps,
|
|
73
|
+
initialStepIndex: 0,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Destructure flow to get stable references for useCallback dependencies
|
|
77
|
+
const {
|
|
78
|
+
currentStep,
|
|
79
|
+
currentStepIndex,
|
|
80
|
+
customData,
|
|
81
|
+
generationProgress,
|
|
82
|
+
generationResult,
|
|
83
|
+
nextStep,
|
|
84
|
+
previousStep,
|
|
85
|
+
setCustomData,
|
|
86
|
+
updateProgress,
|
|
87
|
+
} = flow;
|
|
88
|
+
|
|
89
|
+
// Handle progress change - memoized to prevent infinite loops
|
|
90
|
+
const handleProgressChange = useCallback(
|
|
91
|
+
(progress: number) => {
|
|
92
|
+
updateProgress(progress);
|
|
93
|
+
},
|
|
94
|
+
[updateProgress],
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Ensure scenario has required fields - use feature config as fallback
|
|
98
|
+
const validatedScenario = useMemo(() => {
|
|
99
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
100
|
+
console.log("[GenericWizardFlow] Validating scenario", {
|
|
101
|
+
hasScenario: !!scenario,
|
|
102
|
+
scenarioId: scenario?.id,
|
|
103
|
+
hasAiPrompt: scenario?.aiPrompt !== undefined,
|
|
104
|
+
hasModel: !!scenario?.model,
|
|
105
|
+
scenarioModel: scenario?.model,
|
|
106
|
+
outputType: scenario?.outputType,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (scenario && scenario.id && scenario.aiPrompt !== undefined) {
|
|
111
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
112
|
+
console.log("[GenericWizardFlow] Scenario validation passed", {
|
|
113
|
+
scenarioId: scenario.id,
|
|
114
|
+
model: scenario.model,
|
|
115
|
+
outputType: scenario.outputType,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return scenario;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Fallback to feature config
|
|
122
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
123
|
+
console.warn("[GenericWizardFlow] Scenario missing required fields, using fallback", {
|
|
124
|
+
hasScenario: !!scenario,
|
|
125
|
+
scenarioId: scenario?.id,
|
|
126
|
+
featureConfigId: featureConfig.id,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
id: featureConfig.id,
|
|
132
|
+
aiPrompt: "",
|
|
133
|
+
outputType: "image" as const, // Default to image for safety
|
|
134
|
+
title: featureConfig.id,
|
|
135
|
+
};
|
|
136
|
+
}, [scenario, featureConfig.id]);
|
|
137
|
+
|
|
138
|
+
// Generation hook - handles AI generation automatically
|
|
139
|
+
// Note: Hook is used for its side effects (automatic generation)
|
|
140
|
+
useWizardGeneration({
|
|
141
|
+
scenario: validatedScenario,
|
|
142
|
+
wizardData: customData,
|
|
143
|
+
userId,
|
|
144
|
+
isGeneratingStep: currentStep?.type === StepType.GENERATING,
|
|
145
|
+
alertMessages,
|
|
146
|
+
onSuccess: onGenerationComplete,
|
|
147
|
+
onError: onGenerationError,
|
|
148
|
+
onProgressChange: handleProgressChange,
|
|
149
|
+
onCreditsExhausted,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Track previous step ID to prevent infinite loops
|
|
153
|
+
const prevStepIdRef = useRef<string | undefined>(undefined);
|
|
154
|
+
|
|
155
|
+
// DEBUG logging
|
|
156
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
157
|
+
console.log("[GenericWizardFlow] Render", {
|
|
158
|
+
featureId: featureConfig.id,
|
|
159
|
+
currentStepId: currentStep?.id,
|
|
160
|
+
currentStepType: currentStep?.type,
|
|
161
|
+
stepIndex: currentStepIndex,
|
|
162
|
+
totalSteps: flowSteps.length,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Notify parent when step changes
|
|
167
|
+
// Only call onStepChange when step ID actually changes (not on every object reference change)
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (currentStep && onStepChange) {
|
|
170
|
+
const currentStepId = currentStep.id;
|
|
171
|
+
// Only notify if step ID changed
|
|
172
|
+
if (prevStepIdRef.current !== currentStepId) {
|
|
173
|
+
prevStepIdRef.current = currentStepId;
|
|
174
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
175
|
+
console.log("[GenericWizardFlow] Step changed", {
|
|
176
|
+
stepId: currentStep.id,
|
|
177
|
+
stepType: currentStep.type,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
onStepChange(currentStep.id, currentStep.type);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}, [currentStep, currentStepIndex, onStepChange]);
|
|
184
|
+
|
|
185
|
+
// Handle step continue
|
|
186
|
+
const handleStepContinue = useCallback(
|
|
187
|
+
(stepData: Record<string, unknown>) => {
|
|
188
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
189
|
+
console.log("[GenericWizardFlow] Step continue", {
|
|
190
|
+
stepId: currentStep?.id,
|
|
191
|
+
data: stepData,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Store step data in custom data
|
|
196
|
+
Object.entries(stepData).forEach(([key, value]) => {
|
|
197
|
+
setCustomData(key, value);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Check if this is the last step before generating
|
|
201
|
+
if (currentStepIndex === flowSteps.length - 2) {
|
|
202
|
+
// Next step is GENERATING
|
|
203
|
+
// Notify parent and provide callback to proceed to generating
|
|
204
|
+
// Parent will call proceedToGenerating() after feature gate passes
|
|
205
|
+
if (onGenerationStart) {
|
|
206
|
+
onGenerationStart(customData, () => {
|
|
207
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
208
|
+
console.log("[GenericWizardFlow] Proceeding to GENERATING step");
|
|
209
|
+
}
|
|
210
|
+
nextStep();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
// DON'T call nextStep() here - parent will call it via proceedToGenerating callback
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Move to next step (for all non-generation steps)
|
|
218
|
+
nextStep();
|
|
219
|
+
},
|
|
220
|
+
[currentStep, currentStepIndex, customData, setCustomData, nextStep, flowSteps.length, onGenerationStart],
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Handle back
|
|
224
|
+
const handleBack = useCallback(() => {
|
|
225
|
+
if (currentStepIndex === 0) {
|
|
226
|
+
onBack?.();
|
|
227
|
+
} else {
|
|
228
|
+
previousStep();
|
|
229
|
+
}
|
|
230
|
+
}, [currentStepIndex, previousStep, onBack]);
|
|
231
|
+
|
|
232
|
+
// Render current step
|
|
233
|
+
const renderCurrentStep = useCallback(() => {
|
|
234
|
+
const step = currentStep;
|
|
235
|
+
if (!step) {
|
|
236
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
237
|
+
console.warn("[GenericWizardFlow] No current step!");
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
243
|
+
console.log("[GenericWizardFlow] Rendering step", {
|
|
244
|
+
stepId: step.id,
|
|
245
|
+
stepType: step.type,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Special steps with custom renderers
|
|
250
|
+
switch (step.type) {
|
|
251
|
+
case StepType.SCENARIO_PREVIEW:
|
|
252
|
+
// Preview continues to next step automatically
|
|
253
|
+
return renderPreview?.(nextStep) || null;
|
|
254
|
+
|
|
255
|
+
case StepType.GENERATING:
|
|
256
|
+
return renderGenerating?.(generationProgress) || null;
|
|
257
|
+
|
|
258
|
+
case StepType.RESULT_PREVIEW:
|
|
259
|
+
return renderResult?.(generationResult) || null;
|
|
260
|
+
|
|
261
|
+
default:
|
|
262
|
+
// Other step types should be handled by custom render props
|
|
263
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
264
|
+
console.warn("[GenericWizardFlow] Unhandled step type", { stepType: step.type });
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}, [
|
|
269
|
+
currentStep,
|
|
270
|
+
generationProgress,
|
|
271
|
+
generationResult,
|
|
272
|
+
nextStep,
|
|
273
|
+
renderPreview,
|
|
274
|
+
renderGenerating,
|
|
275
|
+
renderResult,
|
|
276
|
+
handleStepContinue,
|
|
277
|
+
handleBack,
|
|
278
|
+
t,
|
|
279
|
+
translations,
|
|
280
|
+
]);
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
|
|
284
|
+
{renderCurrentStep()}
|
|
285
|
+
</View>
|
|
286
|
+
);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const styles = StyleSheet.create({
|
|
290
|
+
container: {
|
|
291
|
+
flex: 1,
|
|
292
|
+
},
|
|
293
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Generating Screen
|
|
3
|
+
* Shows progress while AI generates content
|
|
4
|
+
* Used by ALL features - NO feature-specific code!
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { View, StyleSheet, ActivityIndicator } from "react-native";
|
|
9
|
+
import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
|
|
10
|
+
|
|
11
|
+
export interface GeneratingScreenProps {
|
|
12
|
+
readonly progress: number;
|
|
13
|
+
readonly scenario?: {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly title?: string;
|
|
16
|
+
};
|
|
17
|
+
readonly t: (key: string) => string;
|
|
18
|
+
readonly onCancel?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const GeneratingScreen: React.FC<GeneratingScreenProps> = ({
|
|
22
|
+
progress,
|
|
23
|
+
scenario,
|
|
24
|
+
t,
|
|
25
|
+
onCancel: _onCancel,
|
|
26
|
+
}) => {
|
|
27
|
+
const tokens = useAppDesignTokens();
|
|
28
|
+
|
|
29
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
30
|
+
console.log("[GeneratingScreen] Rendering", {
|
|
31
|
+
progress,
|
|
32
|
+
scenarioId: scenario?.id,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
|
|
38
|
+
<View style={styles.content}>
|
|
39
|
+
<ActivityIndicator size="large" color={tokens.colors.primary} />
|
|
40
|
+
|
|
41
|
+
<AtomicText type="headlineMedium" style={styles.title}>
|
|
42
|
+
{t("generator.title")}
|
|
43
|
+
</AtomicText>
|
|
44
|
+
|
|
45
|
+
<AtomicText type="bodyMedium" style={[styles.message, { color: tokens.colors.textSecondary }]}>
|
|
46
|
+
{t("generator.waitMessage")}
|
|
47
|
+
</AtomicText>
|
|
48
|
+
|
|
49
|
+
{/* Progress Bar */}
|
|
50
|
+
<View style={styles.progressContainer}>
|
|
51
|
+
<View style={[styles.progressBar, { backgroundColor: tokens.colors.surfaceVariant }]}>
|
|
52
|
+
<View
|
|
53
|
+
style={[
|
|
54
|
+
styles.progressFill,
|
|
55
|
+
{
|
|
56
|
+
backgroundColor: tokens.colors.primary,
|
|
57
|
+
width: `${Math.min(100, Math.max(0, progress))}%`,
|
|
58
|
+
},
|
|
59
|
+
]}
|
|
60
|
+
/>
|
|
61
|
+
</View>
|
|
62
|
+
<AtomicText type="bodySmall" style={[styles.progressText, { color: tokens.colors.textSecondary }]}>
|
|
63
|
+
{Math.round(progress)}%
|
|
64
|
+
</AtomicText>
|
|
65
|
+
</View>
|
|
66
|
+
|
|
67
|
+
{/* Scenario Info */}
|
|
68
|
+
{scenario && (
|
|
69
|
+
<AtomicText type="bodySmall" style={[styles.hint, { color: tokens.colors.textSecondary }]}>
|
|
70
|
+
{scenario.title || scenario.id}
|
|
71
|
+
</AtomicText>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
{/* Hint */}
|
|
75
|
+
<AtomicText type="bodySmall" style={[styles.hint, { color: tokens.colors.textSecondary }]}>
|
|
76
|
+
{t("generator.hint")}
|
|
77
|
+
</AtomicText>
|
|
78
|
+
</View>
|
|
79
|
+
</View>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const styles = StyleSheet.create({
|
|
84
|
+
container: {
|
|
85
|
+
flex: 1,
|
|
86
|
+
justifyContent: "center",
|
|
87
|
+
alignItems: "center",
|
|
88
|
+
},
|
|
89
|
+
content: {
|
|
90
|
+
width: "80%",
|
|
91
|
+
maxWidth: 400,
|
|
92
|
+
alignItems: "center",
|
|
93
|
+
gap: 16,
|
|
94
|
+
},
|
|
95
|
+
title: {
|
|
96
|
+
textAlign: "center",
|
|
97
|
+
marginTop: 24,
|
|
98
|
+
},
|
|
99
|
+
message: {
|
|
100
|
+
textAlign: "center",
|
|
101
|
+
},
|
|
102
|
+
progressContainer: {
|
|
103
|
+
width: "100%",
|
|
104
|
+
marginTop: 24,
|
|
105
|
+
gap: 8,
|
|
106
|
+
},
|
|
107
|
+
progressBar: {
|
|
108
|
+
height: 8,
|
|
109
|
+
borderRadius: 4,
|
|
110
|
+
overflow: "hidden",
|
|
111
|
+
},
|
|
112
|
+
progressFill: {
|
|
113
|
+
height: "100%",
|
|
114
|
+
borderRadius: 4,
|
|
115
|
+
},
|
|
116
|
+
progressText: {
|
|
117
|
+
textAlign: "center",
|
|
118
|
+
},
|
|
119
|
+
hint: {
|
|
120
|
+
textAlign: "center",
|
|
121
|
+
marginTop: 8,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GeneratingScreen } from "./GeneratingScreen";
|
|
@@ -84,3 +84,42 @@ export interface ScenarioSelection {
|
|
|
84
84
|
readonly mainCategory: MainCategory;
|
|
85
85
|
readonly subCategory: SubCategory;
|
|
86
86
|
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Legacy types for presentation components (backward compatibility)
|
|
90
|
+
* Used by CategoryNavigationContainer and related screens
|
|
91
|
+
*/
|
|
92
|
+
export interface ScenarioMainCategory {
|
|
93
|
+
readonly id: string;
|
|
94
|
+
readonly titleKey: string;
|
|
95
|
+
readonly descriptionKey?: string;
|
|
96
|
+
readonly icon?: string;
|
|
97
|
+
readonly emoji?: string;
|
|
98
|
+
readonly order: number;
|
|
99
|
+
readonly subCategoryIds: readonly string[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ScenarioSubCategory {
|
|
103
|
+
readonly id: string;
|
|
104
|
+
readonly titleKey: string;
|
|
105
|
+
readonly descriptionKey?: string;
|
|
106
|
+
readonly icon?: string;
|
|
107
|
+
readonly emoji?: string;
|
|
108
|
+
readonly mainCategoryId: string;
|
|
109
|
+
readonly scenarioCategories: readonly string[];
|
|
110
|
+
readonly order: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface ScenarioData {
|
|
114
|
+
readonly id: string;
|
|
115
|
+
readonly category?: string;
|
|
116
|
+
readonly title: string;
|
|
117
|
+
readonly description: string;
|
|
118
|
+
readonly icon: string;
|
|
119
|
+
readonly imageUrl?: string;
|
|
120
|
+
readonly previewImageUrl?: string;
|
|
121
|
+
readonly aiPrompt: string;
|
|
122
|
+
readonly storyTemplate: string;
|
|
123
|
+
readonly requiresPhoto?: boolean;
|
|
124
|
+
readonly hidden?: boolean;
|
|
125
|
+
}
|
|
@@ -25,3 +25,26 @@ export {
|
|
|
25
25
|
} from "./configs/wizard-configs";
|
|
26
26
|
|
|
27
27
|
export type { WizardConfigOptions } from "./configs/wizard-configs";
|
|
28
|
+
|
|
29
|
+
// Presentation - Containers
|
|
30
|
+
export { CategoryNavigationContainer } from "./presentation/containers";
|
|
31
|
+
export type { CategoryNavigationContainerProps } from "./presentation/containers";
|
|
32
|
+
|
|
33
|
+
// Presentation - Screens
|
|
34
|
+
export {
|
|
35
|
+
ScenarioPreviewScreen,
|
|
36
|
+
MainCategoryScreen,
|
|
37
|
+
SubCategoryScreen,
|
|
38
|
+
HierarchicalScenarioListScreen,
|
|
39
|
+
} from "./presentation/screens";
|
|
40
|
+
export type {
|
|
41
|
+
MainCategoryScreenProps,
|
|
42
|
+
SubCategoryScreenProps,
|
|
43
|
+
} from "./presentation/screens";
|
|
44
|
+
|
|
45
|
+
// Legacy types (backward compatibility)
|
|
46
|
+
export type {
|
|
47
|
+
ScenarioMainCategory,
|
|
48
|
+
ScenarioSubCategory,
|
|
49
|
+
ScenarioData,
|
|
50
|
+
} from "./domain/scenario.types";
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CategoryNavigationContainer
|
|
3
|
+
* Orchestrates 3-step hierarchical scenario selection flow:
|
|
4
|
+
* Main Category → Sub Category → Scenario List
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState, useCallback, useEffect } from "react";
|
|
8
|
+
import type {
|
|
9
|
+
ScenarioData,
|
|
10
|
+
ScenarioMainCategory,
|
|
11
|
+
ScenarioSubCategory,
|
|
12
|
+
} from "../../domain/scenario.types";
|
|
13
|
+
import { MainCategoryScreen } from "../screens/MainCategoryScreen";
|
|
14
|
+
import { SubCategoryScreen } from "../screens/SubCategoryScreen";
|
|
15
|
+
import { HierarchicalScenarioListScreen } from "../screens/HierarchicalScenarioListScreen";
|
|
16
|
+
|
|
17
|
+
type NavigationStep = "main_category" | "sub_category" | "scenario_list";
|
|
18
|
+
|
|
19
|
+
export interface CategoryNavigationContainerProps {
|
|
20
|
+
readonly mainCategories: readonly ScenarioMainCategory[];
|
|
21
|
+
readonly subCategories: readonly ScenarioSubCategory[];
|
|
22
|
+
readonly scenarios: readonly ScenarioData[];
|
|
23
|
+
readonly onSelectScenario: (scenarioId: string) => void;
|
|
24
|
+
readonly onBack?: () => void;
|
|
25
|
+
readonly onSelectMainCategory?: (categoryId: string) => void;
|
|
26
|
+
readonly onSelectSubCategory?: (subCategoryId: string) => void;
|
|
27
|
+
readonly t: (key: string) => string;
|
|
28
|
+
readonly headerTitle?: string;
|
|
29
|
+
readonly headerDescription?: string;
|
|
30
|
+
readonly numColumns?: number;
|
|
31
|
+
readonly isLoading?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const CategoryNavigationContainer: React.FC<
|
|
35
|
+
CategoryNavigationContainerProps
|
|
36
|
+
> = ({
|
|
37
|
+
mainCategories,
|
|
38
|
+
subCategories,
|
|
39
|
+
scenarios,
|
|
40
|
+
onSelectScenario,
|
|
41
|
+
onBack,
|
|
42
|
+
onSelectMainCategory,
|
|
43
|
+
onSelectSubCategory,
|
|
44
|
+
t,
|
|
45
|
+
headerTitle,
|
|
46
|
+
headerDescription,
|
|
47
|
+
numColumns = 2,
|
|
48
|
+
isLoading = false,
|
|
49
|
+
}) => {
|
|
50
|
+
const [currentStep, setCurrentStep] = useState<NavigationStep>("main_category");
|
|
51
|
+
const [selectedMainCategoryId, setSelectedMainCategoryId] = useState<string | null>(null);
|
|
52
|
+
const [selectedSubCategoryId, setSelectedSubCategoryId] = useState<string | null>(null);
|
|
53
|
+
|
|
54
|
+
// Debug: Initial mount
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
57
|
+
console.log("[CategoryNavigationContainer] Mounted", {
|
|
58
|
+
mainCategoriesCount: mainCategories.length,
|
|
59
|
+
subCategoriesCount: subCategories.length,
|
|
60
|
+
scenariosCount: scenarios.length,
|
|
61
|
+
currentStep,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
// Debug: Step changes
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
69
|
+
console.log("[CategoryNavigationContainer] Step changed", {
|
|
70
|
+
currentStep,
|
|
71
|
+
selectedMainCategoryId,
|
|
72
|
+
selectedSubCategoryId,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}, [currentStep, selectedMainCategoryId, selectedSubCategoryId]);
|
|
76
|
+
|
|
77
|
+
const handleSelectMainCategory = useCallback((categoryId: string) => {
|
|
78
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
79
|
+
console.log("[CategoryNavigationContainer] Main category selected", {
|
|
80
|
+
categoryId,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
setSelectedMainCategoryId(categoryId);
|
|
84
|
+
setCurrentStep("sub_category");
|
|
85
|
+
if (onSelectMainCategory) {
|
|
86
|
+
onSelectMainCategory(categoryId);
|
|
87
|
+
}
|
|
88
|
+
}, [onSelectMainCategory]);
|
|
89
|
+
|
|
90
|
+
const handleSelectSubCategory = useCallback((subCategoryId: string) => {
|
|
91
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
92
|
+
console.log("[CategoryNavigationContainer] Sub category selected", {
|
|
93
|
+
subCategoryId,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
setSelectedSubCategoryId(subCategoryId);
|
|
97
|
+
setCurrentStep("scenario_list");
|
|
98
|
+
if (onSelectSubCategory) {
|
|
99
|
+
onSelectSubCategory(subCategoryId);
|
|
100
|
+
}
|
|
101
|
+
}, [onSelectSubCategory]);
|
|
102
|
+
|
|
103
|
+
const handleBackFromSubCategory = useCallback(() => {
|
|
104
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
105
|
+
console.log("[CategoryNavigationContainer] Back from sub category");
|
|
106
|
+
}
|
|
107
|
+
setSelectedMainCategoryId(null);
|
|
108
|
+
setCurrentStep("main_category");
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
const handleBackFromScenarioList = useCallback(() => {
|
|
112
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
113
|
+
console.log("[CategoryNavigationContainer] Back from scenario list");
|
|
114
|
+
}
|
|
115
|
+
setSelectedSubCategoryId(null);
|
|
116
|
+
setCurrentStep("sub_category");
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
const handleBackFromMainCategory = useCallback(() => {
|
|
120
|
+
if (onBack) {
|
|
121
|
+
onBack();
|
|
122
|
+
}
|
|
123
|
+
}, [onBack]);
|
|
124
|
+
|
|
125
|
+
if (currentStep === "main_category") {
|
|
126
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
127
|
+
console.log("[CategoryNavigationContainer] Rendering MainCategoryScreen", {
|
|
128
|
+
mainCategoriesCount: mainCategories.length,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return (
|
|
132
|
+
<MainCategoryScreen
|
|
133
|
+
mainCategories={mainCategories}
|
|
134
|
+
onSelectCategory={handleSelectMainCategory}
|
|
135
|
+
onBack={onBack ? handleBackFromMainCategory : undefined}
|
|
136
|
+
t={t}
|
|
137
|
+
headerTitle={headerTitle}
|
|
138
|
+
headerDescription={headerDescription}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (currentStep === "sub_category" && selectedMainCategoryId) {
|
|
144
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
145
|
+
console.log("[CategoryNavigationContainer] Rendering SubCategoryScreen", {
|
|
146
|
+
selectedMainCategoryId,
|
|
147
|
+
subCategoriesCount: subCategories.length,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return (
|
|
151
|
+
<SubCategoryScreen
|
|
152
|
+
mainCategoryId={selectedMainCategoryId}
|
|
153
|
+
subCategories={subCategories}
|
|
154
|
+
onSelectSubCategory={handleSelectSubCategory}
|
|
155
|
+
onBack={handleBackFromSubCategory}
|
|
156
|
+
t={t}
|
|
157
|
+
/>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (currentStep === "scenario_list" && selectedSubCategoryId) {
|
|
162
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
163
|
+
console.log("[CategoryNavigationContainer] Rendering HierarchicalScenarioListScreen", {
|
|
164
|
+
selectedSubCategoryId,
|
|
165
|
+
scenariosCount: scenarios.length,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return (
|
|
169
|
+
<HierarchicalScenarioListScreen
|
|
170
|
+
subCategoryId={selectedSubCategoryId}
|
|
171
|
+
subCategories={subCategories}
|
|
172
|
+
scenarios={scenarios}
|
|
173
|
+
onSelectScenario={onSelectScenario}
|
|
174
|
+
onBack={handleBackFromScenarioList}
|
|
175
|
+
t={t}
|
|
176
|
+
numColumns={numColumns}
|
|
177
|
+
isLoading={isLoading}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
183
|
+
console.log("[CategoryNavigationContainer] Rendering NULL - no matching condition", {
|
|
184
|
+
currentStep,
|
|
185
|
+
selectedMainCategoryId,
|
|
186
|
+
selectedSubCategoryId,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return null;
|
|
191
|
+
};
|