@umituz/react-native-ai-generation-content 1.18.2 → 1.19.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 +1 -1
- package/src/features/couple-future/index.ts +7 -0
- package/src/features/couple-future/presentation/hooks/useCoupleFutureFlow.ts +219 -0
- package/src/features/wizard/domain/types.ts +81 -0
- package/src/features/wizard/index.ts +26 -0
- package/src/features/wizard/presentation/components/AIGenerationWizard.tsx +136 -0
- package/src/features/wizard/presentation/hooks/useWizard.ts +120 -0
- package/src/features/wizard/presentation/store/useWizardStore.ts +82 -0
- package/src/index.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.1",
|
|
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",
|
|
@@ -16,6 +16,13 @@ export type {
|
|
|
16
16
|
export { COUPLE_FUTURE_DEFAULTS } from "./domain/types";
|
|
17
17
|
export { useCoupleFutureGeneration } from "./presentation/hooks/useCoupleFutureGeneration";
|
|
18
18
|
export type { CoupleFutureConfig as UseCoupleFutureGenerationConfig } from "./presentation/hooks/useCoupleFutureGeneration";
|
|
19
|
+
export { useCoupleFutureFlow } from "./presentation/hooks/useCoupleFutureFlow";
|
|
20
|
+
export type {
|
|
21
|
+
CoupleFutureFlowConfig,
|
|
22
|
+
CoupleFutureFlowState,
|
|
23
|
+
CoupleFutureFlowActions,
|
|
24
|
+
CoupleFutureFlowProps,
|
|
25
|
+
} from "./presentation/hooks/useCoupleFutureFlow";
|
|
19
26
|
export {
|
|
20
27
|
RomanticMoodSelector,
|
|
21
28
|
ArtStyleSelector,
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCoupleFutureFlow Hook
|
|
3
|
+
* Handles couple future wizard flow logic
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
7
|
+
import { InteractionManager } from "react-native";
|
|
8
|
+
import { useCoupleFutureGeneration } from "./useCoupleFutureGeneration";
|
|
9
|
+
import { buildGenerationInputFromConfig } from "../../infrastructure/generationUtils";
|
|
10
|
+
import type { UploadedImage } from "../../../partner-upload/domain/types";
|
|
11
|
+
|
|
12
|
+
export interface CoupleFutureFlowConfig<TStep, TScenarioId> {
|
|
13
|
+
steps: {
|
|
14
|
+
SCENARIO: TStep;
|
|
15
|
+
SCENARIO_PREVIEW: TStep;
|
|
16
|
+
COUPLE_FEATURE_SELECTOR: TStep;
|
|
17
|
+
TEXT_INPUT: TStep;
|
|
18
|
+
PARTNER_A: TStep;
|
|
19
|
+
PARTNER_B: TStep;
|
|
20
|
+
GENERATING: TStep;
|
|
21
|
+
};
|
|
22
|
+
customScenarioId: TScenarioId;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CoupleFutureFlowState<TStep, TScenarioId> {
|
|
26
|
+
step: TStep;
|
|
27
|
+
selectedScenarioId: TScenarioId | null;
|
|
28
|
+
selectedFeature: string | null;
|
|
29
|
+
partnerA: unknown;
|
|
30
|
+
partnerB: unknown;
|
|
31
|
+
partnerAName: string;
|
|
32
|
+
partnerBName: string;
|
|
33
|
+
customPrompt: string | null;
|
|
34
|
+
visualStyle: string | null;
|
|
35
|
+
selection: unknown;
|
|
36
|
+
isProcessing: boolean;
|
|
37
|
+
scenarioConfig: unknown;
|
|
38
|
+
selectedScenarioData: { requiresPhoto?: boolean } | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CoupleFutureFlowActions<TStep, TScenarioId, TResult> {
|
|
42
|
+
setStep: (step: TStep) => void;
|
|
43
|
+
selectScenario: (id: TScenarioId) => void;
|
|
44
|
+
setPartnerA: (image: unknown) => void;
|
|
45
|
+
setPartnerAName: (name: string) => void;
|
|
46
|
+
setPartnerB: (image: unknown) => void;
|
|
47
|
+
setPartnerBName: (name: string) => void;
|
|
48
|
+
setCustomPrompt: (prompt: string) => void;
|
|
49
|
+
setVisualStyle: (style: string) => void;
|
|
50
|
+
startGeneration: () => void;
|
|
51
|
+
generationSuccess: (result: TResult) => void;
|
|
52
|
+
generationError: (error: string) => void;
|
|
53
|
+
requireFeature: (callback: () => void) => void;
|
|
54
|
+
onNavigateToHistory: () => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CoupleFutureFlowProps<TStep, TScenarioId, TResult> {
|
|
58
|
+
userId?: string;
|
|
59
|
+
config: CoupleFutureFlowConfig<TStep, TScenarioId>;
|
|
60
|
+
state: CoupleFutureFlowState<TStep, TScenarioId>;
|
|
61
|
+
actions: CoupleFutureFlowActions<TStep, TScenarioId, TResult>;
|
|
62
|
+
generationConfig: {
|
|
63
|
+
visualStyleModifiers: Record<string, string>;
|
|
64
|
+
defaultPartnerAName: string;
|
|
65
|
+
defaultPartnerBName: string;
|
|
66
|
+
};
|
|
67
|
+
alertMessages: {
|
|
68
|
+
networkError: string;
|
|
69
|
+
policyViolation: string;
|
|
70
|
+
saveFailed: string;
|
|
71
|
+
creditFailed: string;
|
|
72
|
+
unknown: string;
|
|
73
|
+
};
|
|
74
|
+
processResult: (imageUrl: string, input: unknown) => TResult;
|
|
75
|
+
buildCreation: (result: TResult, input: unknown) => unknown;
|
|
76
|
+
onCreditsExhausted: () => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const useCoupleFutureFlow = <TStep, TScenarioId, TResult>(
|
|
80
|
+
props: CoupleFutureFlowProps<TStep, TScenarioId, TResult>,
|
|
81
|
+
) => {
|
|
82
|
+
const { config, state, actions, generationConfig, alertMessages } = props;
|
|
83
|
+
const { processResult, buildCreation, onCreditsExhausted, userId } = props;
|
|
84
|
+
const hasStarted = useRef(false);
|
|
85
|
+
|
|
86
|
+
const { generate, isGenerating, progress } =
|
|
87
|
+
useCoupleFutureGeneration<TResult>({
|
|
88
|
+
userId,
|
|
89
|
+
onCreditsExhausted,
|
|
90
|
+
onSuccess: (result) => {
|
|
91
|
+
actions.generationSuccess(result);
|
|
92
|
+
InteractionManager.runAfterInteractions(() => {
|
|
93
|
+
setTimeout(() => actions.onNavigateToHistory(), 300);
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
onError: actions.generationError,
|
|
97
|
+
processResult: processResult as never,
|
|
98
|
+
buildCreation: buildCreation as never,
|
|
99
|
+
alertMessages,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (state.step !== config.steps.GENERATING) {
|
|
104
|
+
hasStarted.current = false;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!state.isProcessing || hasStarted.current) return;
|
|
108
|
+
hasStarted.current = true;
|
|
109
|
+
|
|
110
|
+
const input = buildGenerationInputFromConfig({
|
|
111
|
+
partnerA: state.partnerA as never,
|
|
112
|
+
partnerB: state.partnerB as never,
|
|
113
|
+
partnerAName: state.partnerAName,
|
|
114
|
+
partnerBName: state.partnerBName,
|
|
115
|
+
scenario: state.scenarioConfig as never,
|
|
116
|
+
customPrompt: state.customPrompt || undefined,
|
|
117
|
+
visualStyle: state.visualStyle || "",
|
|
118
|
+
defaultPartnerAName: generationConfig.defaultPartnerAName,
|
|
119
|
+
defaultPartnerBName: generationConfig.defaultPartnerBName,
|
|
120
|
+
coupleFeatureSelection: state.selection as never,
|
|
121
|
+
visualStyles: generationConfig.visualStyleModifiers,
|
|
122
|
+
customScenarioId: config.customScenarioId as string,
|
|
123
|
+
});
|
|
124
|
+
if (input) generate(input);
|
|
125
|
+
}, [state, config, generationConfig, generate]);
|
|
126
|
+
|
|
127
|
+
const handleScenarioSelect = useCallback(
|
|
128
|
+
(id: string) => {
|
|
129
|
+
actions.selectScenario(id as TScenarioId);
|
|
130
|
+
actions.setStep(config.steps.SCENARIO_PREVIEW);
|
|
131
|
+
},
|
|
132
|
+
[actions, config.steps.SCENARIO_PREVIEW],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const handleScenarioPreviewBack = useCallback(
|
|
136
|
+
() => actions.setStep(config.steps.SCENARIO),
|
|
137
|
+
[actions, config.steps.SCENARIO],
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const handleScenarioPreviewContinue = useCallback(() => {
|
|
141
|
+
if (state.selectedFeature) {
|
|
142
|
+
actions.setStep(config.steps.COUPLE_FEATURE_SELECTOR);
|
|
143
|
+
} else if (
|
|
144
|
+
state.selectedScenarioId === config.customScenarioId ||
|
|
145
|
+
state.selectedScenarioData?.requiresPhoto === false
|
|
146
|
+
) {
|
|
147
|
+
actions.setStep(config.steps.TEXT_INPUT);
|
|
148
|
+
} else {
|
|
149
|
+
actions.setStep(config.steps.PARTNER_A);
|
|
150
|
+
}
|
|
151
|
+
}, [actions, config, state]);
|
|
152
|
+
|
|
153
|
+
const handlePartnerAContinue = useCallback(
|
|
154
|
+
(image: UploadedImage, name: string) => {
|
|
155
|
+
actions.setPartnerA(image);
|
|
156
|
+
actions.setPartnerAName(name);
|
|
157
|
+
actions.setStep(config.steps.PARTNER_B);
|
|
158
|
+
},
|
|
159
|
+
[actions, config.steps.PARTNER_B],
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const handlePartnerABack = useCallback(() => {
|
|
163
|
+
actions.setStep(
|
|
164
|
+
state.selectedScenarioId === config.customScenarioId
|
|
165
|
+
? config.steps.TEXT_INPUT
|
|
166
|
+
: config.steps.SCENARIO_PREVIEW,
|
|
167
|
+
);
|
|
168
|
+
}, [actions, config, state.selectedScenarioId]);
|
|
169
|
+
|
|
170
|
+
const handlePartnerBContinue = useCallback(
|
|
171
|
+
(image: UploadedImage, name: string) => {
|
|
172
|
+
actions.requireFeature(() => {
|
|
173
|
+
actions.setPartnerB(image);
|
|
174
|
+
actions.setPartnerBName(name);
|
|
175
|
+
actions.startGeneration();
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
[actions],
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const handlePartnerBBack = useCallback(
|
|
182
|
+
() => actions.setStep(config.steps.PARTNER_A),
|
|
183
|
+
[actions, config.steps.PARTNER_A],
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const handleMagicPromptContinue = useCallback(
|
|
187
|
+
(prompt: string, style: string) => {
|
|
188
|
+
actions.setCustomPrompt(prompt);
|
|
189
|
+
actions.setVisualStyle(style);
|
|
190
|
+
if (state.selectedScenarioId === config.customScenarioId) {
|
|
191
|
+
actions.setStep(config.steps.PARTNER_A);
|
|
192
|
+
} else {
|
|
193
|
+
actions.startGeneration();
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
[actions, config, state.selectedScenarioId],
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const handleMagicPromptBack = useCallback(
|
|
200
|
+
() => actions.setStep(config.steps.SCENARIO_PREVIEW),
|
|
201
|
+
[actions, config.steps.SCENARIO_PREVIEW],
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
isGenerating,
|
|
206
|
+
progress,
|
|
207
|
+
handlers: {
|
|
208
|
+
handleScenarioSelect,
|
|
209
|
+
handleScenarioPreviewBack,
|
|
210
|
+
handleScenarioPreviewContinue,
|
|
211
|
+
handlePartnerAContinue,
|
|
212
|
+
handlePartnerABack,
|
|
213
|
+
handlePartnerBContinue,
|
|
214
|
+
handlePartnerBBack,
|
|
215
|
+
handleMagicPromptContinue,
|
|
216
|
+
handleMagicPromptBack,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIGenerationWizard Types
|
|
3
|
+
* Generic wizard for all AI generation flows
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ReactNode } from "react";
|
|
7
|
+
import type { UploadedImage } from "../../partner-upload/domain/types";
|
|
8
|
+
|
|
9
|
+
// Step types
|
|
10
|
+
export type WizardStepId = string;
|
|
11
|
+
|
|
12
|
+
export interface WizardStep {
|
|
13
|
+
id: WizardStepId;
|
|
14
|
+
component: React.ComponentType<WizardStepProps>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface WizardStepProps {
|
|
18
|
+
onNext: () => void;
|
|
19
|
+
onBack: () => void;
|
|
20
|
+
onComplete: () => void;
|
|
21
|
+
isProcessing: boolean;
|
|
22
|
+
progress: number;
|
|
23
|
+
error: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Wizard state
|
|
27
|
+
export interface WizardState {
|
|
28
|
+
currentStepIndex: number;
|
|
29
|
+
isProcessing: boolean;
|
|
30
|
+
progress: number;
|
|
31
|
+
error: string | null;
|
|
32
|
+
result: unknown | null;
|
|
33
|
+
images: Record<string, UploadedImage | null>;
|
|
34
|
+
names: Record<string, string>;
|
|
35
|
+
customData: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Wizard config
|
|
39
|
+
export interface AIGenerationWizardConfig {
|
|
40
|
+
steps: WizardStep[];
|
|
41
|
+
initialStep?: number;
|
|
42
|
+
creditCost?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Wizard callbacks
|
|
46
|
+
export interface AIGenerationWizardCallbacks {
|
|
47
|
+
onAuthRequired?: (callback: () => void) => void;
|
|
48
|
+
onCreditsExhausted?: () => void;
|
|
49
|
+
onGenerationStart?: () => void;
|
|
50
|
+
onGenerationSuccess?: (result: unknown) => void;
|
|
51
|
+
onGenerationError?: (error: string) => void;
|
|
52
|
+
onStepChange?: (stepIndex: number, stepId: WizardStepId) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Main props
|
|
56
|
+
export interface AIGenerationWizardProps {
|
|
57
|
+
userId?: string;
|
|
58
|
+
isAuthenticated?: boolean;
|
|
59
|
+
hasCredits?: boolean;
|
|
60
|
+
config: AIGenerationWizardConfig;
|
|
61
|
+
callbacks?: AIGenerationWizardCallbacks;
|
|
62
|
+
renderStep?: (step: WizardStep, props: WizardStepProps) => ReactNode;
|
|
63
|
+
children?: ReactNode;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Store actions
|
|
67
|
+
export interface WizardActions {
|
|
68
|
+
nextStep: () => void;
|
|
69
|
+
prevStep: () => void;
|
|
70
|
+
goToStep: (index: number) => void;
|
|
71
|
+
setImage: (key: string, image: UploadedImage | null) => void;
|
|
72
|
+
setName: (key: string, name: string) => void;
|
|
73
|
+
setCustomData: (key: string, value: unknown) => void;
|
|
74
|
+
setProcessing: (isProcessing: boolean) => void;
|
|
75
|
+
setProgress: (progress: number) => void;
|
|
76
|
+
setError: (error: string | null) => void;
|
|
77
|
+
setResult: (result: unknown) => void;
|
|
78
|
+
reset: () => void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type WizardStore = WizardState & WizardActions;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIGenerationWizard Feature
|
|
3
|
+
* Generic wizard for all AI generation flows
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Component
|
|
7
|
+
export { AIGenerationWizard, default as AIGenerationWizardDefault } from "./presentation/components/AIGenerationWizard";
|
|
8
|
+
|
|
9
|
+
// Hooks
|
|
10
|
+
export { useWizard, resetWizardStore } from "./presentation/hooks/useWizard";
|
|
11
|
+
|
|
12
|
+
// Store
|
|
13
|
+
export { createWizardStore, type WizardStoreType } from "./presentation/store/useWizardStore";
|
|
14
|
+
|
|
15
|
+
// Types (UploadedImage is already exported from partner-upload)
|
|
16
|
+
export type {
|
|
17
|
+
WizardStepId,
|
|
18
|
+
WizardStep,
|
|
19
|
+
WizardStepProps,
|
|
20
|
+
WizardState,
|
|
21
|
+
WizardActions,
|
|
22
|
+
WizardStore,
|
|
23
|
+
AIGenerationWizardConfig,
|
|
24
|
+
AIGenerationWizardCallbacks,
|
|
25
|
+
AIGenerationWizardProps,
|
|
26
|
+
} from "./domain/types";
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIGenerationWizard
|
|
3
|
+
* Generic wizard component for all AI generation flows
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo, useCallback, useEffect, useRef } from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import type {
|
|
9
|
+
AIGenerationWizardProps,
|
|
10
|
+
WizardStepProps,
|
|
11
|
+
WizardStep,
|
|
12
|
+
} from "../../domain/types";
|
|
13
|
+
import { createWizardStore, type WizardStoreType } from "../store/useWizardStore";
|
|
14
|
+
|
|
15
|
+
export const AIGenerationWizard: React.FC<AIGenerationWizardProps> = ({
|
|
16
|
+
userId,
|
|
17
|
+
isAuthenticated = false,
|
|
18
|
+
hasCredits = true,
|
|
19
|
+
config,
|
|
20
|
+
callbacks,
|
|
21
|
+
renderStep,
|
|
22
|
+
children,
|
|
23
|
+
}) => {
|
|
24
|
+
const { steps, initialStep = 0 } = config;
|
|
25
|
+
const storeRef = useRef<WizardStoreType | null>(null);
|
|
26
|
+
|
|
27
|
+
// Create store once
|
|
28
|
+
if (!storeRef.current) {
|
|
29
|
+
storeRef.current = createWizardStore({ totalSteps: steps.length });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const store = storeRef.current;
|
|
33
|
+
const state = store();
|
|
34
|
+
|
|
35
|
+
// Initialize to initialStep if provided
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (initialStep > 0 && state.currentStepIndex === 0) {
|
|
38
|
+
state.goToStep(initialStep);
|
|
39
|
+
}
|
|
40
|
+
}, [initialStep, state]);
|
|
41
|
+
|
|
42
|
+
// Auth check before proceeding
|
|
43
|
+
const handleAuthCheck = useCallback(
|
|
44
|
+
(callback: () => void) => {
|
|
45
|
+
if (!isAuthenticated && callbacks?.onAuthRequired) {
|
|
46
|
+
callbacks.onAuthRequired(callback);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
},
|
|
51
|
+
[isAuthenticated, callbacks],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Credit check before generation
|
|
55
|
+
const handleCreditCheck = useCallback(() => {
|
|
56
|
+
if (!hasCredits && callbacks?.onCreditsExhausted) {
|
|
57
|
+
callbacks.onCreditsExhausted();
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}, [hasCredits, callbacks]);
|
|
62
|
+
|
|
63
|
+
// Step navigation handlers
|
|
64
|
+
const handleNext = useCallback(() => {
|
|
65
|
+
const canProceed = handleAuthCheck(() => {
|
|
66
|
+
state.nextStep();
|
|
67
|
+
});
|
|
68
|
+
if (canProceed) {
|
|
69
|
+
state.nextStep();
|
|
70
|
+
callbacks?.onStepChange?.(
|
|
71
|
+
state.currentStepIndex + 1,
|
|
72
|
+
steps[state.currentStepIndex + 1]?.id ?? "",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}, [handleAuthCheck, state, callbacks, steps]);
|
|
76
|
+
|
|
77
|
+
const handleBack = useCallback(() => {
|
|
78
|
+
state.prevStep();
|
|
79
|
+
callbacks?.onStepChange?.(
|
|
80
|
+
state.currentStepIndex - 1,
|
|
81
|
+
steps[state.currentStepIndex - 1]?.id ?? "",
|
|
82
|
+
);
|
|
83
|
+
}, [state, callbacks, steps]);
|
|
84
|
+
|
|
85
|
+
const handleComplete = useCallback(() => {
|
|
86
|
+
if (!handleCreditCheck()) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
state.setProcessing(true);
|
|
91
|
+
callbacks?.onGenerationStart?.();
|
|
92
|
+
}, [handleCreditCheck, state, callbacks]);
|
|
93
|
+
|
|
94
|
+
// Build step props
|
|
95
|
+
const stepProps: WizardStepProps = useMemo(
|
|
96
|
+
() => ({
|
|
97
|
+
onNext: handleNext,
|
|
98
|
+
onBack: handleBack,
|
|
99
|
+
onComplete: handleComplete,
|
|
100
|
+
isProcessing: state.isProcessing,
|
|
101
|
+
progress: state.progress,
|
|
102
|
+
error: state.error,
|
|
103
|
+
}),
|
|
104
|
+
[handleNext, handleBack, handleComplete, state.isProcessing, state.progress, state.error],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Current step
|
|
108
|
+
const currentStep = steps[state.currentStepIndex];
|
|
109
|
+
|
|
110
|
+
// Render current step
|
|
111
|
+
const renderCurrentStep = useCallback(() => {
|
|
112
|
+
if (!currentStep) return null;
|
|
113
|
+
|
|
114
|
+
if (renderStep) {
|
|
115
|
+
return renderStep(currentStep, stepProps);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const StepComponent = currentStep.component;
|
|
119
|
+
return <StepComponent {...stepProps} />;
|
|
120
|
+
}, [currentStep, stepProps, renderStep]);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<View style={styles.container}>
|
|
124
|
+
{renderCurrentStep()}
|
|
125
|
+
{children}
|
|
126
|
+
</View>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const styles = StyleSheet.create({
|
|
131
|
+
container: {
|
|
132
|
+
flex: 1,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
export default AIGenerationWizard;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useWizard Hook
|
|
3
|
+
* Access wizard store state and actions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useRef, useCallback } from "react";
|
|
7
|
+
import { createWizardStore, type WizardStoreType } from "../store/useWizardStore";
|
|
8
|
+
import type { WizardState, WizardActions } from "../../domain/types";
|
|
9
|
+
import type { UploadedImage } from "../../../partner-upload/domain/types";
|
|
10
|
+
|
|
11
|
+
interface UseWizardConfig {
|
|
12
|
+
totalSteps: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UseWizardReturn extends WizardState, WizardActions {
|
|
16
|
+
/** Check if user can go to next step */
|
|
17
|
+
canGoNext: boolean;
|
|
18
|
+
/** Check if user can go to previous step */
|
|
19
|
+
canGoBack: boolean;
|
|
20
|
+
/** Get image by key */
|
|
21
|
+
getImage: (key: string) => UploadedImage | null;
|
|
22
|
+
/** Get name by key */
|
|
23
|
+
getName: (key: string) => string;
|
|
24
|
+
/** Get custom data by key */
|
|
25
|
+
getData: <T>(key: string) => T | undefined;
|
|
26
|
+
/** Check if all required images are uploaded */
|
|
27
|
+
hasImages: (keys: string[]) => boolean;
|
|
28
|
+
/** Check if all required names are filled */
|
|
29
|
+
hasNames: (keys: string[]) => boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Store singleton per wizard instance
|
|
33
|
+
let wizardStoreInstance: WizardStoreType | null = null;
|
|
34
|
+
|
|
35
|
+
export const useWizard = (config: UseWizardConfig): UseWizardReturn => {
|
|
36
|
+
const storeRef = useRef<WizardStoreType | null>(null);
|
|
37
|
+
|
|
38
|
+
// Create or reuse store
|
|
39
|
+
if (!storeRef.current) {
|
|
40
|
+
if (!wizardStoreInstance) {
|
|
41
|
+
wizardStoreInstance = createWizardStore(config);
|
|
42
|
+
}
|
|
43
|
+
storeRef.current = wizardStoreInstance;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const store = storeRef.current;
|
|
47
|
+
const state = store();
|
|
48
|
+
|
|
49
|
+
// Computed helpers
|
|
50
|
+
const canGoNext = state.currentStepIndex < config.totalSteps - 1;
|
|
51
|
+
const canGoBack = state.currentStepIndex > 0;
|
|
52
|
+
|
|
53
|
+
const getImage = useCallback(
|
|
54
|
+
(key: string) => state.images[key] ?? null,
|
|
55
|
+
[state.images],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const getName = useCallback(
|
|
59
|
+
(key: string) => state.names[key] ?? "",
|
|
60
|
+
[state.names],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const getData = useCallback(
|
|
64
|
+
<T>(key: string) => state.customData[key] as T | undefined,
|
|
65
|
+
[state.customData],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const hasImages = useCallback(
|
|
69
|
+
(keys: string[]) => keys.every((key) => state.images[key] != null),
|
|
70
|
+
[state.images],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const hasNames = useCallback(
|
|
74
|
+
(keys: string[]) =>
|
|
75
|
+
keys.every((key) => state.names[key] && state.names[key].trim().length > 0),
|
|
76
|
+
[state.names],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
// State
|
|
81
|
+
currentStepIndex: state.currentStepIndex,
|
|
82
|
+
isProcessing: state.isProcessing,
|
|
83
|
+
progress: state.progress,
|
|
84
|
+
error: state.error,
|
|
85
|
+
result: state.result,
|
|
86
|
+
images: state.images,
|
|
87
|
+
names: state.names,
|
|
88
|
+
customData: state.customData,
|
|
89
|
+
|
|
90
|
+
// Actions
|
|
91
|
+
nextStep: state.nextStep,
|
|
92
|
+
prevStep: state.prevStep,
|
|
93
|
+
goToStep: state.goToStep,
|
|
94
|
+
setImage: state.setImage,
|
|
95
|
+
setName: state.setName,
|
|
96
|
+
setCustomData: state.setCustomData,
|
|
97
|
+
setProcessing: state.setProcessing,
|
|
98
|
+
setProgress: state.setProgress,
|
|
99
|
+
setError: state.setError,
|
|
100
|
+
setResult: state.setResult,
|
|
101
|
+
reset: state.reset,
|
|
102
|
+
|
|
103
|
+
// Helpers
|
|
104
|
+
canGoNext,
|
|
105
|
+
canGoBack,
|
|
106
|
+
getImage,
|
|
107
|
+
getName,
|
|
108
|
+
getData,
|
|
109
|
+
hasImages,
|
|
110
|
+
hasNames,
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** Reset the wizard store singleton */
|
|
115
|
+
export const resetWizardStore = () => {
|
|
116
|
+
if (wizardStoreInstance) {
|
|
117
|
+
wizardStoreInstance.getState().reset();
|
|
118
|
+
}
|
|
119
|
+
wizardStoreInstance = null;
|
|
120
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useWizardStore
|
|
3
|
+
* Built-in store for AIGenerationWizard using design-system createStore
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createStore } from "@umituz/react-native-design-system";
|
|
7
|
+
import type { WizardState, WizardActions } from "../../domain/types";
|
|
8
|
+
import type { UploadedImage } from "../../../partner-upload/domain/types";
|
|
9
|
+
|
|
10
|
+
const INITIAL_STATE: WizardState = {
|
|
11
|
+
currentStepIndex: 0,
|
|
12
|
+
isProcessing: false,
|
|
13
|
+
progress: 0,
|
|
14
|
+
error: null,
|
|
15
|
+
result: null,
|
|
16
|
+
images: {},
|
|
17
|
+
names: {},
|
|
18
|
+
customData: {},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
interface WizardStoreConfig {
|
|
22
|
+
totalSteps: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const createWizardStore = (config: WizardStoreConfig) =>
|
|
26
|
+
createStore<WizardState, WizardActions>({
|
|
27
|
+
name: "wizard_store",
|
|
28
|
+
initialState: INITIAL_STATE,
|
|
29
|
+
persist: false,
|
|
30
|
+
actions: (set, get) => ({
|
|
31
|
+
nextStep: () => {
|
|
32
|
+
const { currentStepIndex } = get();
|
|
33
|
+
if (currentStepIndex < config.totalSteps - 1) {
|
|
34
|
+
set({ currentStepIndex: currentStepIndex + 1, error: null });
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
prevStep: () => {
|
|
39
|
+
const { currentStepIndex } = get();
|
|
40
|
+
if (currentStepIndex > 0) {
|
|
41
|
+
set({ currentStepIndex: currentStepIndex - 1, error: null });
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
goToStep: (index: number) => {
|
|
46
|
+
if (index >= 0 && index < config.totalSteps) {
|
|
47
|
+
set({ currentStepIndex: index, error: null });
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
setImage: (key: string, image: UploadedImage | null) => {
|
|
52
|
+
const { images } = get();
|
|
53
|
+
set({ images: { ...images, [key]: image } });
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
setName: (key: string, name: string) => {
|
|
57
|
+
const { names } = get();
|
|
58
|
+
set({ names: { ...names, [key]: name } });
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
setCustomData: (key: string, value: unknown) => {
|
|
62
|
+
const { customData } = get();
|
|
63
|
+
set({ customData: { ...customData, [key]: value } });
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
setProcessing: (isProcessing: boolean) => set({ isProcessing }),
|
|
67
|
+
|
|
68
|
+
setProgress: (progress: number) => set({ progress }),
|
|
69
|
+
|
|
70
|
+
setError: (error: string | null) => {
|
|
71
|
+
set({ error, isProcessing: false, progress: 0 });
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
setResult: (result: unknown) => {
|
|
75
|
+
set({ result, isProcessing: false, progress: 100 });
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
reset: () => set(INITIAL_STATE),
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export type WizardStoreType = ReturnType<typeof createWizardStore>;
|
package/src/index.ts
CHANGED