@umituz/react-native-ai-generation-content 1.61.62 → 1.61.63
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/presentation/hooks/generationExecutor.ts +65 -0
- package/src/domains/generation/wizard/presentation/hooks/generationStateMachine.ts +35 -0
- package/src/domains/generation/wizard/presentation/hooks/typeGuards.ts +13 -0
- package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +34 -71
- package/src/domains/generation/wizard/presentation/hooks/useWizardFlowHandlers.ts +6 -84
- package/src/domains/generation/wizard/presentation/hooks/useWizardGeneration.ts +19 -131
- package/src/domains/generation/wizard/presentation/hooks/videoQueuePoller.ts +59 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.61.
|
|
3
|
+
"version": "1.61.63",
|
|
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",
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { buildWizardInput } from "../../infrastructure/strategies";
|
|
2
|
+
import type { WizardScenarioData } from "./wizard-generation.types";
|
|
3
|
+
import type { Scenario } from "../../../../scenarios/domain/scenario.types";
|
|
4
|
+
import type { GenerationAction } from "./wizard-state-machine";
|
|
5
|
+
|
|
6
|
+
declare const __DEV__: boolean;
|
|
7
|
+
|
|
8
|
+
interface ExecuteGenerationParams {
|
|
9
|
+
wizardData: WizardScenarioData;
|
|
10
|
+
scenario: Scenario;
|
|
11
|
+
isVideoMode: boolean;
|
|
12
|
+
isMountedRef: React.MutableRefObject<boolean>;
|
|
13
|
+
dispatch: React.Dispatch<GenerationAction>;
|
|
14
|
+
onError?: (error: string) => void;
|
|
15
|
+
videoGenerationFn: (input: any, prompt: string) => Promise<void>;
|
|
16
|
+
photoGenerationFn: (input: any, prompt: string) => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const executeWizardGeneration = async (params: ExecuteGenerationParams): Promise<void> => {
|
|
20
|
+
const {
|
|
21
|
+
wizardData,
|
|
22
|
+
scenario,
|
|
23
|
+
isVideoMode,
|
|
24
|
+
isMountedRef,
|
|
25
|
+
dispatch,
|
|
26
|
+
onError,
|
|
27
|
+
videoGenerationFn,
|
|
28
|
+
photoGenerationFn,
|
|
29
|
+
} = params;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const input = await buildWizardInput(wizardData, scenario);
|
|
33
|
+
|
|
34
|
+
if (!isMountedRef.current) return;
|
|
35
|
+
|
|
36
|
+
if (!input) {
|
|
37
|
+
dispatch({ type: "ERROR", error: "Failed to build generation input" });
|
|
38
|
+
onError?.("Failed to build generation input");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
dispatch({ type: "START_GENERATION" });
|
|
43
|
+
|
|
44
|
+
if (__DEV__) {
|
|
45
|
+
console.log("[WizardGeneration] GENERATING -", isVideoMode ? "VIDEO" : "PHOTO");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const typedInput = input as { prompt?: string };
|
|
49
|
+
const generationFn = isVideoMode ? videoGenerationFn : photoGenerationFn;
|
|
50
|
+
await generationFn(input, typedInput.prompt || "");
|
|
51
|
+
|
|
52
|
+
if (isMountedRef.current) {
|
|
53
|
+
dispatch({ type: "COMPLETE" });
|
|
54
|
+
}
|
|
55
|
+
} catch (error: any) {
|
|
56
|
+
if (!isMountedRef.current) return;
|
|
57
|
+
|
|
58
|
+
const errorMsg = error?.message || "error.generation.unknown";
|
|
59
|
+
if (__DEV__) {
|
|
60
|
+
console.error("[WizardGeneration] Error:", errorMsg, error);
|
|
61
|
+
}
|
|
62
|
+
dispatch({ type: "ERROR", error: errorMsg });
|
|
63
|
+
onError?.(errorMsg);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type GenerationStatus = "IDLE" | "PREPARING" | "GENERATING" | "ERROR" | "COMPLETED";
|
|
2
|
+
|
|
3
|
+
export interface GenerationState {
|
|
4
|
+
status: GenerationStatus;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type GenerationAction =
|
|
9
|
+
| { type: "START_PREPARATION" }
|
|
10
|
+
| { type: "START_GENERATION" }
|
|
11
|
+
| { type: "COMPLETE" }
|
|
12
|
+
| { type: "ERROR"; error: string }
|
|
13
|
+
| { type: "RESET" };
|
|
14
|
+
|
|
15
|
+
export const generationReducer = (
|
|
16
|
+
state: GenerationState,
|
|
17
|
+
action: GenerationAction,
|
|
18
|
+
): GenerationState => {
|
|
19
|
+
switch (action.type) {
|
|
20
|
+
case "START_PREPARATION":
|
|
21
|
+
return { status: "PREPARING" };
|
|
22
|
+
case "START_GENERATION":
|
|
23
|
+
return { status: "GENERATING" };
|
|
24
|
+
case "COMPLETE":
|
|
25
|
+
return { status: "COMPLETED" };
|
|
26
|
+
case "ERROR":
|
|
27
|
+
return { status: "ERROR", error: action.error };
|
|
28
|
+
case "RESET":
|
|
29
|
+
return { status: "IDLE" };
|
|
30
|
+
default:
|
|
31
|
+
return state;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const INITIAL_STATE: GenerationState = { status: "IDLE" };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Creation } from "../../../../creations/domain/entities/Creation";
|
|
2
|
+
|
|
3
|
+
export const isCreation = (result: unknown): result is Creation => {
|
|
4
|
+
if (!result || typeof result !== "object") return false;
|
|
5
|
+
const creation = result as Partial<Creation>;
|
|
6
|
+
return (
|
|
7
|
+
typeof creation.id === "string" &&
|
|
8
|
+
typeof creation.uri === "string" &&
|
|
9
|
+
typeof creation.type === "string" &&
|
|
10
|
+
creation.createdAt instanceof Date &&
|
|
11
|
+
typeof creation.isShared === "boolean"
|
|
12
|
+
);
|
|
13
|
+
};
|
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useVideoQueueGeneration Hook
|
|
3
|
-
* Handles video generation via FAL queue with background support
|
|
4
|
-
* - Submits to queue for non-blocking generation
|
|
5
|
-
* - Polls for completion status
|
|
6
|
-
* - Supports background generation (user can dismiss wizard)
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
1
|
import { useEffect, useRef, useCallback, useState } from "react";
|
|
10
|
-
import {
|
|
11
|
-
import { extractResultUrl, type FalResult, type GenerationUrls } from "./generation-result.utils";
|
|
12
|
-
import { QUEUE_STATUS } from "../../../../../domain/constants/queue-status.constants";
|
|
2
|
+
import { pollQueueStatus } from "./videoQueuePoller";
|
|
13
3
|
import { DEFAULT_POLL_INTERVAL_MS } from "../../../../../infrastructure/constants/polling.constants";
|
|
14
4
|
import type { CreationPersistence } from "../../infrastructure/utils/creation-persistence.util";
|
|
15
5
|
import type { WizardStrategy } from "../../infrastructure/strategies/wizard-strategy.types";
|
|
16
6
|
import type { WizardScenarioData } from "./wizard-generation.types";
|
|
7
|
+
import type { GenerationUrls } from "./generation-result.utils";
|
|
17
8
|
|
|
18
9
|
declare const __DEV__: boolean;
|
|
19
10
|
|
|
@@ -31,9 +22,7 @@ export interface UseVideoQueueGenerationReturn {
|
|
|
31
22
|
readonly startGeneration: (input: unknown, prompt: string) => Promise<void>;
|
|
32
23
|
}
|
|
33
24
|
|
|
34
|
-
export function useVideoQueueGeneration(
|
|
35
|
-
props: UseVideoQueueGenerationProps,
|
|
36
|
-
): UseVideoQueueGenerationReturn {
|
|
25
|
+
export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): UseVideoQueueGenerationReturn {
|
|
37
26
|
const { userId, scenario, persistence, strategy, onSuccess, onError } = props;
|
|
38
27
|
|
|
39
28
|
const creationIdRef = useRef<string | null>(null);
|
|
@@ -44,7 +33,6 @@ export function useVideoQueueGeneration(
|
|
|
44
33
|
const isPollingRef = useRef(false);
|
|
45
34
|
const [isGenerating, setIsGenerating] = useState(false);
|
|
46
35
|
|
|
47
|
-
// Cleanup polling on unmount
|
|
48
36
|
useEffect(() => {
|
|
49
37
|
return () => {
|
|
50
38
|
if (pollingRef.current) {
|
|
@@ -74,7 +62,7 @@ export function useVideoQueueGeneration(
|
|
|
74
62
|
videoUrl: urls.videoUrl,
|
|
75
63
|
});
|
|
76
64
|
} catch (err) {
|
|
77
|
-
if (
|
|
65
|
+
if (__DEV__) console.error("[VideoQueueGeneration] updateToCompleted error:", err);
|
|
78
66
|
}
|
|
79
67
|
}
|
|
80
68
|
resetRefs();
|
|
@@ -90,7 +78,7 @@ export function useVideoQueueGeneration(
|
|
|
90
78
|
try {
|
|
91
79
|
await persistence.updateToFailed(userId, creationId, errorMsg);
|
|
92
80
|
} catch (err) {
|
|
93
|
-
if (
|
|
81
|
+
if (__DEV__) console.error("[VideoQueueGeneration] updateToFailed error:", err);
|
|
94
82
|
}
|
|
95
83
|
}
|
|
96
84
|
resetRefs();
|
|
@@ -99,75 +87,52 @@ export function useVideoQueueGeneration(
|
|
|
99
87
|
[userId, persistence, onError, resetRefs],
|
|
100
88
|
);
|
|
101
89
|
|
|
102
|
-
const
|
|
103
|
-
// Guard against concurrent polls
|
|
104
|
-
if (isPollingRef.current) return;
|
|
105
|
-
|
|
90
|
+
const pollStatus = useCallback(async () => {
|
|
106
91
|
const requestId = requestIdRef.current;
|
|
107
92
|
const model = modelRef.current;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (status.status === QUEUE_STATUS.COMPLETED) {
|
|
119
|
-
try {
|
|
120
|
-
const result = await provider.getJobResult<FalResult>(model, requestId);
|
|
121
|
-
await handleComplete(extractResultUrl(result));
|
|
122
|
-
} catch (resultErr) {
|
|
123
|
-
// Handle errors when getting/extracting result (e.g., ValidationError, content policy)
|
|
124
|
-
const errorMessage = resultErr instanceof Error ? resultErr.message : "Generation failed";
|
|
125
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
126
|
-
console.error("[VideoQueueGeneration] Result error:", errorMessage);
|
|
127
|
-
}
|
|
128
|
-
await handleError(errorMessage);
|
|
129
|
-
}
|
|
130
|
-
} else {
|
|
131
|
-
await handleError("Generation failed");
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
} catch (err) {
|
|
135
|
-
// Handle polling errors - stop polling and show error to user
|
|
136
|
-
if (pollingRef.current) { clearInterval(pollingRef.current); pollingRef.current = null; }
|
|
137
|
-
const errorMessage = err instanceof Error ? err.message : "Generation failed";
|
|
138
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
139
|
-
console.error("[VideoQueueGeneration] Poll error:", errorMessage);
|
|
140
|
-
}
|
|
141
|
-
await handleError(errorMessage);
|
|
142
|
-
} finally {
|
|
143
|
-
isPollingRef.current = false;
|
|
144
|
-
}
|
|
93
|
+
if (!requestId || !model) return;
|
|
94
|
+
|
|
95
|
+
await pollQueueStatus({
|
|
96
|
+
requestId,
|
|
97
|
+
model,
|
|
98
|
+
isPollingRef,
|
|
99
|
+
pollingRef,
|
|
100
|
+
onComplete: handleComplete,
|
|
101
|
+
onError: handleError,
|
|
102
|
+
});
|
|
145
103
|
}, [handleComplete, handleError]);
|
|
146
104
|
|
|
147
105
|
const startGeneration = useCallback(
|
|
148
106
|
async (input: unknown, prompt: string) => {
|
|
149
|
-
if (!strategy.submitToQueue) {
|
|
107
|
+
if (!strategy.submitToQueue) {
|
|
108
|
+
onError?.("Queue submission not available");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
150
111
|
if (isGeneratingRef.current) return;
|
|
112
|
+
|
|
151
113
|
isGeneratingRef.current = true;
|
|
152
114
|
setIsGenerating(true);
|
|
153
115
|
|
|
154
|
-
// Save to Firestore FIRST (enables background visibility)
|
|
155
116
|
let creationId: string | null = null;
|
|
156
117
|
if (userId && prompt) {
|
|
157
118
|
try {
|
|
158
119
|
creationId = await persistence.saveAsProcessing(userId, {
|
|
159
|
-
scenarioId: scenario.id,
|
|
120
|
+
scenarioId: scenario.id,
|
|
121
|
+
scenarioTitle: scenario.title || scenario.id,
|
|
122
|
+
prompt,
|
|
160
123
|
});
|
|
161
124
|
creationIdRef.current = creationId;
|
|
162
|
-
if (
|
|
125
|
+
if (__DEV__) console.log("[VideoQueueGeneration] Saved:", creationId);
|
|
163
126
|
} catch (err) {
|
|
164
|
-
if (
|
|
127
|
+
if (__DEV__) console.error("[VideoQueueGeneration] save error:", err);
|
|
165
128
|
}
|
|
166
129
|
}
|
|
167
130
|
|
|
168
131
|
const queueResult = await strategy.submitToQueue(input);
|
|
169
132
|
if (!queueResult.success || !queueResult.requestId || !queueResult.model) {
|
|
170
|
-
if (creationId && userId)
|
|
133
|
+
if (creationId && userId) {
|
|
134
|
+
await persistence.updateToFailed(userId, creationId, queueResult.error || "Queue submission failed");
|
|
135
|
+
}
|
|
171
136
|
setIsGenerating(false);
|
|
172
137
|
onError?.(queueResult.error || "Queue submission failed");
|
|
173
138
|
return;
|
|
@@ -176,21 +141,19 @@ export function useVideoQueueGeneration(
|
|
|
176
141
|
requestIdRef.current = queueResult.requestId;
|
|
177
142
|
modelRef.current = queueResult.model;
|
|
178
143
|
|
|
179
|
-
// Update with requestId for background polling
|
|
180
144
|
if (creationId && userId) {
|
|
181
145
|
try {
|
|
182
146
|
await persistence.updateRequestId(userId, creationId, queueResult.requestId, queueResult.model);
|
|
183
|
-
if (
|
|
147
|
+
if (__DEV__) console.log("[VideoQueueGeneration] Updated requestId:", queueResult.requestId);
|
|
184
148
|
} catch (err) {
|
|
185
|
-
if (
|
|
149
|
+
if (__DEV__) console.error("[VideoQueueGeneration] updateRequestId error:", err);
|
|
186
150
|
}
|
|
187
151
|
}
|
|
188
152
|
|
|
189
|
-
pollingRef.current = setInterval(() => void
|
|
190
|
-
|
|
191
|
-
void pollQueueStatus();
|
|
153
|
+
pollingRef.current = setInterval(() => void pollStatus(), DEFAULT_POLL_INTERVAL_MS);
|
|
154
|
+
void pollStatus();
|
|
192
155
|
},
|
|
193
|
-
[userId, scenario, persistence, strategy,
|
|
156
|
+
[userId, scenario, persistence, strategy, pollStatus, onError],
|
|
194
157
|
);
|
|
195
158
|
|
|
196
159
|
return { isGenerating, startGeneration };
|
|
@@ -1,32 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wizard Flow Handlers Hook
|
|
3
|
-
* Extracts callback handlers from WizardFlowContent
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import { useCallback } from "react";
|
|
7
2
|
import { AlertType, AlertMode, useAlert } from "@umituz/react-native-design-system";
|
|
8
3
|
import { StepType, type StepDefinition } from "../../../../../domain/entities/flow-config.types";
|
|
9
4
|
import type { UploadedImage } from "../../../../../presentation/hooks/generation/useAIGenerateState";
|
|
10
5
|
import type { Creation } from "../../../../creations/domain/entities/Creation";
|
|
11
|
-
|
|
12
|
-
declare const __DEV__: boolean;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Type guard to check if result is a valid Creation object
|
|
16
|
-
*/
|
|
17
|
-
function isCreation(result: unknown): result is Creation {
|
|
18
|
-
if (!result || typeof result !== "object") {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
const creation = result as Partial<Creation>;
|
|
22
|
-
return (
|
|
23
|
-
typeof creation.id === "string" &&
|
|
24
|
-
typeof creation.uri === "string" &&
|
|
25
|
-
typeof creation.type === "string" &&
|
|
26
|
-
creation.createdAt instanceof Date &&
|
|
27
|
-
typeof creation.isShared === "boolean"
|
|
28
|
-
);
|
|
29
|
-
}
|
|
6
|
+
import { isCreation } from "./typeGuards";
|
|
30
7
|
|
|
31
8
|
export interface UseWizardFlowHandlersProps {
|
|
32
9
|
readonly currentStepIndex: number;
|
|
@@ -77,21 +54,8 @@ export function useWizardFlowHandlers(props: UseWizardFlowHandlersProps) {
|
|
|
77
54
|
|
|
78
55
|
const handleGenerationComplete = useCallback(
|
|
79
56
|
(result: unknown) => {
|
|
80
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
81
|
-
console.log("[WizardFlowHandlers] Generation completed");
|
|
82
|
-
}
|
|
83
57
|
setResult(result);
|
|
84
|
-
|
|
85
|
-
// Use type guard to safely check if result is a Creation
|
|
86
|
-
if (isCreation(result)) {
|
|
87
|
-
setCurrentCreation(result);
|
|
88
|
-
} else {
|
|
89
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
90
|
-
console.warn("[WizardFlowHandlers] Result is not a valid Creation object:", result);
|
|
91
|
-
}
|
|
92
|
-
setCurrentCreation(null);
|
|
93
|
-
}
|
|
94
|
-
|
|
58
|
+
setCurrentCreation(isCreation(result) ? result : null);
|
|
95
59
|
onGenerationComplete?.(result);
|
|
96
60
|
if (!skipResultStep) nextStep();
|
|
97
61
|
},
|
|
@@ -100,43 +64,17 @@ export function useWizardFlowHandlers(props: UseWizardFlowHandlersProps) {
|
|
|
100
64
|
|
|
101
65
|
const handleGenerationError = useCallback(
|
|
102
66
|
(errorMessage: string) => {
|
|
103
|
-
// Ensure we have a meaningful error message
|
|
104
67
|
const safeErrorMessage = errorMessage?.trim() || "error.generation.unknown";
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
original: errorMessage,
|
|
108
|
-
safe: safeErrorMessage,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
// Translate error key if it looks like a translation key
|
|
112
|
-
const displayMessage = safeErrorMessage.startsWith("error.")
|
|
113
|
-
? t(safeErrorMessage)
|
|
114
|
-
: safeErrorMessage;
|
|
115
|
-
// Show error alert to user
|
|
116
|
-
alert.show(
|
|
117
|
-
AlertType.ERROR,
|
|
118
|
-
AlertMode.MODAL,
|
|
119
|
-
t("common.error"),
|
|
120
|
-
displayMessage,
|
|
121
|
-
);
|
|
122
|
-
// Notify parent component
|
|
68
|
+
const displayMessage = safeErrorMessage.startsWith("error.") ? t(safeErrorMessage) : safeErrorMessage;
|
|
69
|
+
alert.show(AlertType.ERROR, AlertMode.MODAL, t("common.error"), displayMessage);
|
|
123
70
|
onGenerationError?.(safeErrorMessage);
|
|
124
|
-
// Close the wizard
|
|
125
71
|
onBack?.();
|
|
126
72
|
},
|
|
127
73
|
[alert, t, onGenerationError, onBack],
|
|
128
74
|
);
|
|
129
75
|
|
|
130
76
|
const handleDismissGenerating = useCallback(() => {
|
|
131
|
-
|
|
132
|
-
console.log("[WizardFlowHandlers] Dismissing - generation continues");
|
|
133
|
-
}
|
|
134
|
-
alert.show(
|
|
135
|
-
AlertType.INFO,
|
|
136
|
-
AlertMode.TOAST,
|
|
137
|
-
t("generator.backgroundTitle"),
|
|
138
|
-
t("generator.backgroundMessage"),
|
|
139
|
-
);
|
|
77
|
+
alert.show(AlertType.INFO, AlertMode.TOAST, t("generator.backgroundTitle"), t("generator.backgroundMessage"));
|
|
140
78
|
onBack?.();
|
|
141
79
|
}, [alert, t, onBack]);
|
|
142
80
|
|
|
@@ -147,18 +85,7 @@ export function useWizardFlowHandlers(props: UseWizardFlowHandlersProps) {
|
|
|
147
85
|
|
|
148
86
|
const handleNextStep = useCallback(() => {
|
|
149
87
|
const nextStepDef = flowSteps[currentStepIndex + 1];
|
|
150
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
151
|
-
console.log("[WizardFlowHandlers] handleNextStep", {
|
|
152
|
-
currentStepIndex,
|
|
153
|
-
nextStepType: nextStepDef?.type,
|
|
154
|
-
isGenerating: nextStepDef?.type === StepType.GENERATING,
|
|
155
|
-
hasOnGenerationStart: !!onGenerationStart,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
88
|
if (nextStepDef?.type === StepType.GENERATING && onGenerationStart) {
|
|
159
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
160
|
-
console.log("[WizardFlowHandlers] Calling onGenerationStart callback");
|
|
161
|
-
}
|
|
162
89
|
onGenerationStart(customData, nextStep);
|
|
163
90
|
return;
|
|
164
91
|
}
|
|
@@ -179,12 +106,7 @@ export function useWizardFlowHandlers(props: UseWizardFlowHandlersProps) {
|
|
|
179
106
|
const success = await repository.rate(userId, currentCreation.id, rating, description);
|
|
180
107
|
if (success) {
|
|
181
108
|
setHasRated(true);
|
|
182
|
-
alert.show(
|
|
183
|
-
AlertType.SUCCESS,
|
|
184
|
-
AlertMode.TOAST,
|
|
185
|
-
t("result.rateSuccessTitle"),
|
|
186
|
-
t("result.rateSuccessMessage"),
|
|
187
|
-
);
|
|
109
|
+
alert.show(AlertType.SUCCESS, AlertMode.TOAST, t("result.rateSuccessTitle"), t("result.rateSuccessMessage"));
|
|
188
110
|
}
|
|
189
111
|
setShowRatingPicker(false);
|
|
190
112
|
},
|
|
@@ -1,75 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Orchestrates wizard-based generation
|
|
4
|
-
* - Video: Queue-based generation with background support
|
|
5
|
-
* - Photo: Blocking execution for quick results
|
|
6
|
-
*
|
|
7
|
-
* Architecture: State machine pattern with useReducer
|
|
8
|
-
* States: IDLE → PREPARING → GENERATING → COMPLETED/ERROR → IDLE
|
|
2
|
+
* Wizard Generation Hook
|
|
3
|
+
* Orchestrates wizard-based generation (Video: queue, Photo: blocking)
|
|
9
4
|
*/
|
|
10
5
|
|
|
11
6
|
import { useEffect, useReducer, useMemo, useRef } from "react";
|
|
12
|
-
import { createWizardStrategy
|
|
7
|
+
import { createWizardStrategy } from "../../infrastructure/strategies";
|
|
13
8
|
import { createCreationPersistence } from "../../infrastructure/utils/creation-persistence.util";
|
|
14
9
|
import { useVideoQueueGeneration } from "./useVideoQueueGeneration";
|
|
15
10
|
import { usePhotoBlockingGeneration } from "./usePhotoBlockingGeneration";
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from "./wizard-generation.types";
|
|
20
|
-
|
|
21
|
-
declare const __DEV__: boolean;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Generation orchestration states
|
|
25
|
-
*/
|
|
26
|
-
type GenerationStatus =
|
|
27
|
-
| "IDLE" // Not started
|
|
28
|
-
| "PREPARING" // Building input
|
|
29
|
-
| "GENERATING" // Generation in progress
|
|
30
|
-
| "ERROR" // Failed (prevents retry)
|
|
31
|
-
| "COMPLETED"; // Success
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* State machine state
|
|
35
|
-
*/
|
|
36
|
-
interface GenerationState {
|
|
37
|
-
status: GenerationStatus;
|
|
38
|
-
error?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* State machine actions
|
|
43
|
-
*/
|
|
44
|
-
type GenerationAction =
|
|
45
|
-
| { type: "START_PREPARATION" }
|
|
46
|
-
| { type: "START_GENERATION" }
|
|
47
|
-
| { type: "COMPLETE" }
|
|
48
|
-
| { type: "ERROR"; error: string }
|
|
49
|
-
| { type: "RESET" };
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* State machine reducer
|
|
53
|
-
*/
|
|
54
|
-
const generationReducer = (
|
|
55
|
-
state: GenerationState,
|
|
56
|
-
action: GenerationAction,
|
|
57
|
-
): GenerationState => {
|
|
58
|
-
switch (action.type) {
|
|
59
|
-
case "START_PREPARATION":
|
|
60
|
-
return { status: "PREPARING" };
|
|
61
|
-
case "START_GENERATION":
|
|
62
|
-
return { status: "GENERATING" };
|
|
63
|
-
case "COMPLETE":
|
|
64
|
-
return { status: "COMPLETED" };
|
|
65
|
-
case "ERROR":
|
|
66
|
-
return { status: "ERROR", error: action.error };
|
|
67
|
-
case "RESET":
|
|
68
|
-
return { status: "IDLE" };
|
|
69
|
-
default:
|
|
70
|
-
return state;
|
|
71
|
-
}
|
|
72
|
-
};
|
|
11
|
+
import { generationReducer, INITIAL_STATE } from "./generationStateMachine";
|
|
12
|
+
import { executeWizardGeneration } from "./generationExecutor";
|
|
13
|
+
import type { UseWizardGenerationProps, UseWizardGenerationReturn } from "./wizard-generation.types";
|
|
73
14
|
|
|
74
15
|
export type {
|
|
75
16
|
WizardOutputType,
|
|
@@ -78,9 +19,7 @@ export type {
|
|
|
78
19
|
UseWizardGenerationReturn,
|
|
79
20
|
} from "./wizard-generation.types";
|
|
80
21
|
|
|
81
|
-
export const useWizardGeneration = (
|
|
82
|
-
props: UseWizardGenerationProps,
|
|
83
|
-
): UseWizardGenerationReturn => {
|
|
22
|
+
export const useWizardGeneration = (props: UseWizardGenerationProps): UseWizardGenerationReturn => {
|
|
84
23
|
const {
|
|
85
24
|
scenario,
|
|
86
25
|
wizardData,
|
|
@@ -93,10 +32,7 @@ export const useWizardGeneration = (
|
|
|
93
32
|
onCreditsExhausted,
|
|
94
33
|
} = props;
|
|
95
34
|
|
|
96
|
-
|
|
97
|
-
const [state, dispatch] = useReducer(generationReducer, { status: "IDLE" });
|
|
98
|
-
|
|
99
|
-
// Mounted ref to prevent state updates after unmount
|
|
35
|
+
const [state, dispatch] = useReducer(generationReducer, INITIAL_STATE);
|
|
100
36
|
const isMountedRef = useRef(true);
|
|
101
37
|
|
|
102
38
|
useEffect(() => {
|
|
@@ -107,14 +43,9 @@ export const useWizardGeneration = (
|
|
|
107
43
|
}, []);
|
|
108
44
|
|
|
109
45
|
const persistence = useMemo(() => createCreationPersistence(), []);
|
|
110
|
-
const strategy = useMemo(
|
|
111
|
-
() => createWizardStrategy({ scenario, creditCost }),
|
|
112
|
-
[scenario, creditCost],
|
|
113
|
-
);
|
|
114
|
-
|
|
46
|
+
const strategy = useMemo(() => createWizardStrategy({ scenario, creditCost }), [scenario, creditCost]);
|
|
115
47
|
const isVideoMode = scenario.outputType === "video" && !!strategy.submitToQueue;
|
|
116
48
|
|
|
117
|
-
// Video generation hook (queue-based)
|
|
118
49
|
const videoGeneration = useVideoQueueGeneration({
|
|
119
50
|
userId,
|
|
120
51
|
scenario,
|
|
@@ -124,7 +55,6 @@ export const useWizardGeneration = (
|
|
|
124
55
|
onError,
|
|
125
56
|
});
|
|
126
57
|
|
|
127
|
-
// Photo generation hook (blocking)
|
|
128
58
|
const photoGeneration = usePhotoBlockingGeneration({
|
|
129
59
|
userId,
|
|
130
60
|
scenario,
|
|
@@ -136,67 +66,25 @@ export const useWizardGeneration = (
|
|
|
136
66
|
onCreditsExhausted,
|
|
137
67
|
});
|
|
138
68
|
|
|
139
|
-
// Main effect: trigger generation when step becomes active
|
|
140
69
|
useEffect(() => {
|
|
141
70
|
const isAlreadyGenerating = videoGeneration.isGenerating || photoGeneration.isGenerating;
|
|
142
71
|
|
|
143
|
-
// Start generation: Simple single condition using state machine
|
|
144
72
|
if (isGeneratingStep && state.status === "IDLE" && !isAlreadyGenerating) {
|
|
145
73
|
dispatch({ type: "START_PREPARATION" });
|
|
146
74
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
dispatch({ type: "ERROR", error: "Failed to build generation input" });
|
|
158
|
-
onError?.("Failed to build generation input");
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
dispatch({ type: "START_GENERATION" });
|
|
163
|
-
|
|
164
|
-
const typedInput = input as { prompt?: string };
|
|
165
|
-
|
|
166
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
167
|
-
console.log("[WizardGeneration] State: GENERATING");
|
|
168
|
-
console.log("[WizardGeneration] Mode:", isVideoMode ? "VIDEO_QUEUE" : "PHOTO_BLOCKING");
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (isVideoMode) {
|
|
172
|
-
await videoGeneration.startGeneration(input, typedInput.prompt || "");
|
|
173
|
-
} else {
|
|
174
|
-
await photoGeneration.startGeneration(input, typedInput.prompt || "");
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Check again before final state update
|
|
178
|
-
if (isMountedRef.current) {
|
|
179
|
-
dispatch({ type: "COMPLETE" });
|
|
180
|
-
}
|
|
181
|
-
})
|
|
182
|
-
.catch((error) => {
|
|
183
|
-
// Check if component is still mounted before error handling
|
|
184
|
-
if (!isMountedRef.current) return;
|
|
185
|
-
|
|
186
|
-
const errorMsg = error?.message || "error.generation.unknown";
|
|
187
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
188
|
-
console.error("[WizardGeneration] Build input error:", errorMsg, error);
|
|
189
|
-
}
|
|
190
|
-
dispatch({ type: "ERROR", error: errorMsg });
|
|
191
|
-
onError?.(errorMsg);
|
|
192
|
-
});
|
|
75
|
+
executeWizardGeneration({
|
|
76
|
+
wizardData,
|
|
77
|
+
scenario,
|
|
78
|
+
isVideoMode,
|
|
79
|
+
isMountedRef,
|
|
80
|
+
dispatch,
|
|
81
|
+
onError,
|
|
82
|
+
videoGenerationFn: videoGeneration.startGeneration,
|
|
83
|
+
photoGenerationFn: photoGeneration.startGeneration,
|
|
84
|
+
});
|
|
193
85
|
}
|
|
194
86
|
|
|
195
|
-
// Reset state when leaving generating step
|
|
196
87
|
if (!isGeneratingStep && state.status !== "IDLE") {
|
|
197
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
198
|
-
console.log("[WizardGeneration] State: RESET");
|
|
199
|
-
}
|
|
200
88
|
dispatch({ type: "RESET" });
|
|
201
89
|
}
|
|
202
90
|
}, [
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { providerRegistry } from "../../../../../infrastructure/services/provider-registry.service";
|
|
2
|
+
import { extractResultUrl, type FalResult, type GenerationUrls } from "./generation-result.utils";
|
|
3
|
+
import { QUEUE_STATUS } from "../../../../../domain/constants/queue-status.constants";
|
|
4
|
+
|
|
5
|
+
declare const __DEV__: boolean;
|
|
6
|
+
|
|
7
|
+
interface PollParams {
|
|
8
|
+
requestId: string;
|
|
9
|
+
model: string;
|
|
10
|
+
isPollingRef: React.MutableRefObject<boolean>;
|
|
11
|
+
pollingRef: React.MutableRefObject<ReturnType<typeof setInterval> | null>;
|
|
12
|
+
onComplete: (urls: GenerationUrls) => Promise<void>;
|
|
13
|
+
onError: (error: string) => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const pollQueueStatus = async (params: PollParams): Promise<void> => {
|
|
17
|
+
const { requestId, model, isPollingRef, pollingRef, onComplete, onError } = params;
|
|
18
|
+
|
|
19
|
+
if (isPollingRef.current) return;
|
|
20
|
+
|
|
21
|
+
const provider = providerRegistry.getActiveProvider();
|
|
22
|
+
if (!provider) return;
|
|
23
|
+
|
|
24
|
+
isPollingRef.current = true;
|
|
25
|
+
try {
|
|
26
|
+
const status = await provider.getJobStatus(model, requestId);
|
|
27
|
+
if (__DEV__) console.log("[VideoQueueGeneration] Poll:", status.status);
|
|
28
|
+
|
|
29
|
+
if (status.status === QUEUE_STATUS.COMPLETED || status.status === QUEUE_STATUS.FAILED) {
|
|
30
|
+
if (pollingRef.current) {
|
|
31
|
+
clearInterval(pollingRef.current);
|
|
32
|
+
pollingRef.current = null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (status.status === QUEUE_STATUS.COMPLETED) {
|
|
36
|
+
try {
|
|
37
|
+
const result = await provider.getJobResult<FalResult>(model, requestId);
|
|
38
|
+
await onComplete(extractResultUrl(result));
|
|
39
|
+
} catch (resultErr) {
|
|
40
|
+
const errorMessage = resultErr instanceof Error ? resultErr.message : "Generation failed";
|
|
41
|
+
if (__DEV__) console.error("[VideoQueueGeneration] Result error:", errorMessage);
|
|
42
|
+
await onError(errorMessage);
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
await onError("Generation failed");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (pollingRef.current) {
|
|
50
|
+
clearInterval(pollingRef.current);
|
|
51
|
+
pollingRef.current = null;
|
|
52
|
+
}
|
|
53
|
+
const errorMessage = err instanceof Error ? err.message : "Generation failed";
|
|
54
|
+
if (__DEV__) console.error("[VideoQueueGeneration] Poll error:", errorMessage);
|
|
55
|
+
await onError(errorMessage);
|
|
56
|
+
} finally {
|
|
57
|
+
isPollingRef.current = false;
|
|
58
|
+
}
|
|
59
|
+
};
|