@umituz/react-native-ai-generation-content 1.87.0 → 1.87.2
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/background/infrastructure/executors/backgroundJobExecutor.ts +25 -2
- package/src/domains/background/infrastructure/services/job-poller.service.ts +11 -0
- package/src/domains/creations/infrastructure/repositories/CreationsQuery.ts +9 -1
- package/src/domains/creations/infrastructure/repositories/creation-create.operations.ts +10 -10
- package/src/domains/image-to-video/index.ts +2 -2
- package/src/domains/image-to-video/presentation/components/index.ts +2 -2
- package/src/domains/text-to-image/infrastructure-exports.ts +2 -2
- package/src/domains/text-to-image/presentation/components/index.ts +4 -4
- package/src/domains/text-to-video/index.ts +2 -2
- package/src/infrastructure/services/image-feature-executor.service.ts +2 -2
- package/src/infrastructure/services/video-feature-executor.service.ts +2 -2
- package/src/infrastructure/utils/feature-utils.ts +7 -2
- package/src/presentation/components/buttons/index.ts +2 -1
- package/src/presentation/components/image-picker/index.ts +2 -1
- package/src/presentation/hooks/ai-feature-callbacks-execution.hooks.ts +3 -1
- package/src/presentation/hooks/generation/moderation-handler.ts +9 -5
- package/src/presentation/hooks/generation/orchestrator.ts +48 -7
- package/src/presentation/hooks/generation/repositorySingleton.ts +17 -0
- package/src/presentation/hooks/generation/useAudioGenerationExecutor.ts +4 -7
- package/src/presentation/hooks/generation/useImageGenerationExecutor.ts +3 -6
- package/src/presentation/hooks/generation/useImagePicker.ts +1 -1
- package/src/shared/hooks/factories/createFormStateHook.ts +0 -4
- package/src/shared/utils/credit.ts +69 -0
- package/src/shared/utils/date/index.ts +3 -7
- package/src/shared/utils/date/timestamp.ts +71 -0
- package/src/shared/utils/progress.ts +123 -0
- package/src/domains/text-to-image/infrastructure/index.ts +0 -1
- package/src/domains/text-to-video/infrastructure/index.ts +0 -1
- package/src/presentation/components/prompts/index.ts +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.87.
|
|
3
|
+
"version": "1.87.2",
|
|
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",
|
|
@@ -89,6 +89,14 @@ export const executeQueuedJob = async <TInput, TResult>(
|
|
|
89
89
|
if (completedJob) {
|
|
90
90
|
await executor.onComplete?.(completedJob);
|
|
91
91
|
onJobComplete?.(completedJob);
|
|
92
|
+
} else {
|
|
93
|
+
// Job reference lost - log for debugging
|
|
94
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
95
|
+
console.warn("[BackgroundJobExecutor] Completed job not found in cache:", {
|
|
96
|
+
jobId,
|
|
97
|
+
timestamp: new Date().toISOString(),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
// Await removal to ensure cleanup happens before checking activeJobs
|
|
@@ -103,13 +111,28 @@ export const executeQueuedJob = async <TInput, TResult>(
|
|
|
103
111
|
if (failedJob) {
|
|
104
112
|
await executor.onError?.(failedJob, error instanceof Error ? error : new Error(errorMsg));
|
|
105
113
|
onJobError?.(failedJob);
|
|
114
|
+
} else {
|
|
115
|
+
// Job reference lost - log for debugging
|
|
116
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
117
|
+
console.warn("[BackgroundJobExecutor] Failed job not found in cache:", {
|
|
118
|
+
jobId,
|
|
119
|
+
error: errorMsg,
|
|
120
|
+
timestamp: new Date().toISOString(),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
106
123
|
}
|
|
107
124
|
|
|
108
125
|
// Remove failed job from cache to prevent accumulation
|
|
109
126
|
try {
|
|
110
127
|
await removeJobAsync(jobId);
|
|
111
|
-
} catch {
|
|
112
|
-
//
|
|
128
|
+
} catch (removeError) {
|
|
129
|
+
// Log cleanup failure but don't throw - state is already consistent
|
|
130
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
131
|
+
console.error("[BackgroundJobExecutor] Failed to remove job after error:", {
|
|
132
|
+
jobId,
|
|
133
|
+
error: removeError instanceof Error ? removeError.message : String(removeError),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
113
136
|
}
|
|
114
137
|
} finally {
|
|
115
138
|
// Use atomic Set operation to prevent race conditions
|
|
@@ -31,7 +31,11 @@ function withAbortSignal<T>(
|
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
let isResolved = false;
|
|
34
35
|
const abortHandler = () => {
|
|
36
|
+
if (isResolved) return;
|
|
37
|
+
isResolved = true;
|
|
38
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
35
39
|
reject(new Error("Operation aborted"));
|
|
36
40
|
};
|
|
37
41
|
|
|
@@ -41,17 +45,24 @@ function withAbortSignal<T>(
|
|
|
41
45
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
42
46
|
if (timeoutMs) {
|
|
43
47
|
timeoutId = setTimeout(() => {
|
|
48
|
+
if (isResolved) return;
|
|
49
|
+
isResolved = true;
|
|
50
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
44
51
|
reject(new Error(`Operation timeout after ${timeoutMs}ms`));
|
|
45
52
|
}, timeoutMs);
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
promise
|
|
49
56
|
.then((result) => {
|
|
57
|
+
if (isResolved) return;
|
|
58
|
+
isResolved = true;
|
|
50
59
|
signal?.removeEventListener("abort", abortHandler);
|
|
51
60
|
if (timeoutId) clearTimeout(timeoutId);
|
|
52
61
|
resolve(result);
|
|
53
62
|
})
|
|
54
63
|
.catch((error) => {
|
|
64
|
+
if (isResolved) return;
|
|
65
|
+
isResolved = true;
|
|
55
66
|
signal?.removeEventListener("abort", abortHandler);
|
|
56
67
|
if (timeoutId) clearTimeout(timeoutId);
|
|
57
68
|
reject(error);
|
|
@@ -19,7 +19,15 @@ export class CreationsQuery {
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Get all creations for a user
|
|
22
|
-
*
|
|
22
|
+
*
|
|
23
|
+
* NOTE: Client-side filtering for deletedAt is used here for simplicity.
|
|
24
|
+
* For better performance with large datasets, consider:
|
|
25
|
+
* 1. Adding a where clause: where(CREATION_FIELDS.DELETED_AT, "==", null)
|
|
26
|
+
* 2. Creating a composite index on (deletedAt, createdAt)
|
|
27
|
+
*
|
|
28
|
+
* Current implementation fetches all documents including soft-deleted ones,
|
|
29
|
+
* which is acceptable for small-to-medium datasets but may impact performance
|
|
30
|
+
* at scale.
|
|
23
31
|
*/
|
|
24
32
|
async getAll(userId: string): Promise<Creation[]> {
|
|
25
33
|
const userCollection = this.pathResolver.getUserCollection(userId);
|
|
@@ -19,16 +19,16 @@ export async function createCreation(
|
|
|
19
19
|
uri: creation.uri,
|
|
20
20
|
createdAt: creation.createdAt,
|
|
21
21
|
deletedAt: null,
|
|
22
|
-
metadata: creation.metadata
|
|
23
|
-
isShared: creation.isShared
|
|
24
|
-
isFavorite: creation.isFavorite
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
metadata: creation.metadata ?? {},
|
|
23
|
+
isShared: creation.isShared ?? false,
|
|
24
|
+
isFavorite: creation.isFavorite ?? false,
|
|
25
|
+
status: creation.status ?? null,
|
|
26
|
+
output: creation.output ?? null,
|
|
27
|
+
prompt: creation.prompt ?? null,
|
|
28
|
+
provider: creation.provider ?? null,
|
|
29
|
+
requestId: creation.requestId ?? null,
|
|
30
|
+
model: creation.model ?? null,
|
|
31
|
+
startedAt: creation.startedAt ?? null,
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
try {
|
|
@@ -48,8 +48,8 @@ export type {
|
|
|
48
48
|
// INFRASTRUCTURE LAYER
|
|
49
49
|
// =============================================================================
|
|
50
50
|
|
|
51
|
-
export { executeImageToVideo, hasImageToVideoSupport } from "./infrastructure";
|
|
52
|
-
export type { ExecuteImageToVideoOptions } from "./infrastructure";
|
|
51
|
+
export { executeImageToVideo, hasImageToVideoSupport } from "./infrastructure/services";
|
|
52
|
+
export type { ExecuteImageToVideoOptions } from "./infrastructure/services";
|
|
53
53
|
|
|
54
54
|
// =============================================================================
|
|
55
55
|
// PRESENTATION LAYER - Hooks
|
|
@@ -18,5 +18,5 @@ export type {
|
|
|
18
18
|
} from "./ImageSelectionGrid";
|
|
19
19
|
|
|
20
20
|
// Action Components
|
|
21
|
-
export { GenerateButton as ImageToVideoGenerateButton } from "../../../../presentation/components/buttons";
|
|
22
|
-
export type { GenerateButtonProps as ImageToVideoGenerateButtonProps } from "../../../../presentation/components/buttons";
|
|
21
|
+
export { GenerateButton as ImageToVideoGenerateButton } from "../../../../presentation/components/buttons/GenerateButton";
|
|
22
|
+
export type { GenerateButtonProps as ImageToVideoGenerateButtonProps } from "../../../../presentation/components/buttons/GenerateButton";
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Text-to-Image Infrastructure Layer Exports
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export { executeTextToImage, hasTextToImageSupport } from "./infrastructure";
|
|
6
|
-
export type { ExecuteTextToImageOptions } from "./infrastructure";
|
|
5
|
+
export { executeTextToImage, hasTextToImageSupport } from "./infrastructure/services";
|
|
6
|
+
export type { ExecuteTextToImageOptions } from "./infrastructure/services";
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
export { PromptInput as TextToImagePromptInput } from "../../../../presentation/components/PromptInput";
|
|
8
8
|
export type { PromptInputProps as TextToImagePromptInputProps } from "../../../../presentation/components/PromptInput";
|
|
9
9
|
|
|
10
|
-
export { ExamplePrompts as TextToImageExamplePrompts } from "../../../../presentation/components/prompts";
|
|
11
|
-
export type { ExamplePromptsProps as TextToImageExamplePromptsProps } from "../../../../presentation/components/prompts";
|
|
10
|
+
export { ExamplePrompts as TextToImageExamplePrompts } from "../../../../presentation/components/prompts/ExamplePrompts";
|
|
11
|
+
export type { ExamplePromptsProps as TextToImageExamplePromptsProps } from "../../../../presentation/components/prompts/ExamplePrompts";
|
|
12
12
|
|
|
13
13
|
// Selector Components
|
|
14
14
|
export { StyleSelector as TextToImageStyleSelector } from "../../../../presentation/components/selectors";
|
|
@@ -22,8 +22,8 @@ export { GridSelector as TextToImageOutputFormatSelector } from "../../../../pre
|
|
|
22
22
|
export { GridSelector as TextToImageNumImagesSelector } from "../../../../presentation/components/selectors";
|
|
23
23
|
|
|
24
24
|
// Action Components
|
|
25
|
-
export { GenerateButton as TextToImageGenerateButton } from "../../../../presentation/components/buttons";
|
|
26
|
-
export type { GenerateButtonProps as TextToImageGenerateButtonProps } from "../../../../presentation/components/buttons";
|
|
25
|
+
export { GenerateButton as TextToImageGenerateButton } from "../../../../presentation/components/buttons/GenerateButton";
|
|
26
|
+
export type { GenerateButtonProps as TextToImageGenerateButtonProps } from "../../../../presentation/components/buttons/GenerateButton";
|
|
27
27
|
|
|
28
28
|
// Sheet Components
|
|
29
29
|
export { SettingsSheet as TextToImageSettingsSheet } from "../../../../presentation/components/modals/SettingsSheet";
|
|
@@ -41,8 +41,8 @@ export type {
|
|
|
41
41
|
export { INITIAL_FORM_STATE, INITIAL_GENERATION_STATE } from "./domain";
|
|
42
42
|
|
|
43
43
|
// Infrastructure Services
|
|
44
|
-
export { executeTextToVideo, hasTextToVideoSupport } from "./infrastructure";
|
|
45
|
-
export type { ExecuteTextToVideoOptions } from "./infrastructure";
|
|
44
|
+
export { executeTextToVideo, hasTextToVideoSupport } from "./infrastructure/services";
|
|
45
|
+
export type { ExecuteTextToVideoOptions } from "./infrastructure/services";
|
|
46
46
|
|
|
47
47
|
// Presentation Hooks
|
|
48
48
|
export {
|
|
@@ -35,7 +35,7 @@ export interface ImageFeatureRequest {
|
|
|
35
35
|
export async function executeImageFeature(
|
|
36
36
|
featureType: ImageFeatureType,
|
|
37
37
|
request: ImageFeatureRequest,
|
|
38
|
-
options
|
|
38
|
+
options: ExecuteImageFeatureOptions = {},
|
|
39
39
|
): Promise<ImageFeatureResult> {
|
|
40
40
|
const validation = validateProvider(`Image:${featureType}`);
|
|
41
41
|
if (!validation.success) {
|
|
@@ -43,7 +43,7 @@ export async function executeImageFeature(
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
const { provider } = validation;
|
|
46
|
-
const { extractResult, onProgress } = options
|
|
46
|
+
const { extractResult, onProgress } = options;
|
|
47
47
|
const model = provider.getImageFeatureModel(featureType);
|
|
48
48
|
|
|
49
49
|
try {
|
|
@@ -18,7 +18,7 @@ import type { ExecuteVideoFeatureOptions, VideoFeatureResult, VideoFeatureReques
|
|
|
18
18
|
export async function executeVideoFeature(
|
|
19
19
|
featureType: VideoFeatureType,
|
|
20
20
|
request: VideoFeatureRequest,
|
|
21
|
-
options
|
|
21
|
+
options: ExecuteVideoFeatureOptions = {},
|
|
22
22
|
): Promise<VideoFeatureResult> {
|
|
23
23
|
const validation = validateProvider(`VideoExecutor:${featureType}`);
|
|
24
24
|
if (!validation.success) {
|
|
@@ -26,7 +26,7 @@ export async function executeVideoFeature(
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const { provider } = validation;
|
|
29
|
-
const { extractResult, onStatusChange } = options
|
|
29
|
+
const { extractResult, onStatusChange } = options;
|
|
30
30
|
const model = provider.getVideoFeatureModel(featureType);
|
|
31
31
|
|
|
32
32
|
try {
|
|
@@ -87,15 +87,20 @@ async function checkCreditGuard(cost: number, featureName: string): Promise<bool
|
|
|
87
87
|
|
|
88
88
|
if (!authService.isAuthenticated()) {
|
|
89
89
|
if (__DEV__) {
|
|
90
|
-
|
|
91
90
|
console.log(`[${featureName}] Auth required`);
|
|
92
91
|
}
|
|
93
92
|
try {
|
|
94
93
|
authService.requireAuth();
|
|
94
|
+
// If requireAuth succeeds (e.g., user was already logged in), check auth status again
|
|
95
|
+
if (!authService.isAuthenticated()) {
|
|
96
|
+
// requireAuth succeeded but user is still not authenticated
|
|
97
|
+
// This can happen if requireAuth just navigated without throwing
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
95
100
|
} catch {
|
|
101
|
+
// requireAuth threw an error (e.g., user cancelled login, navigation failed)
|
|
96
102
|
return false;
|
|
97
103
|
}
|
|
98
|
-
return false;
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
const hasCredits = await creditService.checkCredits(cost);
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { GenerateButton
|
|
1
|
+
export { GenerateButton } from "./GenerateButton";
|
|
2
|
+
export type { GenerateButtonProps } from "./GenerateButton";
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { DualImagePicker
|
|
1
|
+
export { DualImagePicker } from "./DualImagePicker";
|
|
2
|
+
export type { DualImagePickerProps } from "./DualImagePicker";
|
|
@@ -43,7 +43,9 @@ export function useExecutionCallback<TRequest = unknown, TResult = unknown>(
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
if (result.success) {
|
|
46
|
-
|
|
46
|
+
// imageUrls is required when result.success is true
|
|
47
|
+
// executor must provide it
|
|
48
|
+
return { success: true, imageUrls: result.imageUrls! };
|
|
47
49
|
}
|
|
48
50
|
return { success: false, error: result.error ?? "Unknown error" };
|
|
49
51
|
} catch (error) {
|
|
@@ -49,12 +49,16 @@ export async function handleModeration<TInput, TResult>(
|
|
|
49
49
|
moderation.onShowWarning(
|
|
50
50
|
moderationResult.warnings,
|
|
51
51
|
() => {
|
|
52
|
+
// Cancel callback - reset state
|
|
52
53
|
isGeneratingRef.current = false;
|
|
53
54
|
if (isMountedRef.current) resetState();
|
|
54
55
|
},
|
|
55
|
-
() => {
|
|
56
|
-
//
|
|
57
|
-
|
|
56
|
+
async () => {
|
|
57
|
+
// Proceed callback - execute generation with full error handling
|
|
58
|
+
try {
|
|
59
|
+
const result = await executeGeneration(input);
|
|
60
|
+
return result;
|
|
61
|
+
} catch (err) {
|
|
58
62
|
const error = parseError(err);
|
|
59
63
|
if (isMountedRef.current) {
|
|
60
64
|
setState({ status: "error", isGenerating: false, result: null, error });
|
|
@@ -63,9 +67,9 @@ export async function handleModeration<TInput, TResult>(
|
|
|
63
67
|
await onError?.(error);
|
|
64
68
|
handleLifecycleComplete("error", undefined, error);
|
|
65
69
|
throw error; // Re-throw to allow caller to handle
|
|
66
|
-
}
|
|
70
|
+
} finally {
|
|
67
71
|
isGeneratingRef.current = false;
|
|
68
|
-
}
|
|
72
|
+
}
|
|
69
73
|
},
|
|
70
74
|
);
|
|
71
75
|
return undefined;
|
|
@@ -16,8 +16,12 @@ import type {
|
|
|
16
16
|
UseGenerationOrchestratorReturn,
|
|
17
17
|
} from "./types";
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const getInitialState = <T>(): GenerationState<T> => ({
|
|
20
|
+
status: "idle",
|
|
21
|
+
isGenerating: false,
|
|
22
|
+
result: null,
|
|
23
|
+
error: null,
|
|
24
|
+
});
|
|
21
25
|
|
|
22
26
|
export const useGenerationOrchestrator = <TInput, TResult>(
|
|
23
27
|
strategy: GenerationStrategy<TInput, TResult>,
|
|
@@ -25,7 +29,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
25
29
|
): UseGenerationOrchestratorReturn<TInput, TResult> => {
|
|
26
30
|
const { userId, alertMessages, onSuccess, onError, moderation, lifecycle } = config;
|
|
27
31
|
|
|
28
|
-
const [state, setState] = useState<GenerationState<TResult>>(
|
|
32
|
+
const [state, setState] = useState<GenerationState<TResult>>(getInitialState<TResult>());
|
|
29
33
|
const isGeneratingRef = useRef(false);
|
|
30
34
|
const isMountedRef = useRef(true);
|
|
31
35
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
@@ -83,17 +87,42 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
83
87
|
console.log("[Orchestrator] executeGeneration() called");
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
// Check abort signal before starting
|
|
91
|
+
if (abortControllerRef.current?.signal.aborted) {
|
|
92
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
93
|
+
console.log("[Orchestrator] Aborted before generation started");
|
|
94
|
+
}
|
|
95
|
+
throw new Error("Generation aborted");
|
|
96
|
+
}
|
|
97
|
+
|
|
86
98
|
setState((prev) => ({ ...prev, status: "generating" }));
|
|
87
99
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
88
100
|
console.log("[Orchestrator] State: generating - calling strategy.execute()");
|
|
89
101
|
}
|
|
90
102
|
|
|
91
103
|
const result = await strategy.execute(input);
|
|
104
|
+
|
|
105
|
+
// Check abort signal after execution
|
|
106
|
+
if (abortControllerRef.current?.signal.aborted) {
|
|
107
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
108
|
+
console.log("[Orchestrator] Aborted after generation completed");
|
|
109
|
+
}
|
|
110
|
+
throw new Error("Generation aborted");
|
|
111
|
+
}
|
|
112
|
+
|
|
92
113
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
93
114
|
console.log("[Orchestrator] strategy.execute() completed");
|
|
94
115
|
}
|
|
95
116
|
|
|
96
117
|
if (strategy.save && userId) {
|
|
118
|
+
// Check abort signal before save
|
|
119
|
+
if (abortControllerRef.current?.signal.aborted) {
|
|
120
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
121
|
+
console.log("[Orchestrator] Aborted before save");
|
|
122
|
+
}
|
|
123
|
+
throw new Error("Generation aborted");
|
|
124
|
+
}
|
|
125
|
+
|
|
97
126
|
if (isMountedRef.current) setState((prev) => ({ ...prev, status: "saving" }));
|
|
98
127
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
99
128
|
console.log("[Orchestrator] Saving result to Firestore");
|
|
@@ -107,10 +136,22 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
107
136
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
108
137
|
console.log("[Orchestrator] ERROR: Save failed:", saveErr);
|
|
109
138
|
}
|
|
139
|
+
// Update state to error on save failure
|
|
140
|
+
if (isMountedRef.current) {
|
|
141
|
+
setState((prev) => ({ ...prev, status: "error", isGenerating: false }));
|
|
142
|
+
}
|
|
110
143
|
throw createGenerationError("save", alertMessages.saveFailed, saveErr instanceof Error ? saveErr : undefined);
|
|
111
144
|
}
|
|
112
145
|
}
|
|
113
146
|
|
|
147
|
+
// Final abort check before success
|
|
148
|
+
if (abortControllerRef.current?.signal.aborted) {
|
|
149
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
150
|
+
console.log("[Orchestrator] Aborted before success callback");
|
|
151
|
+
}
|
|
152
|
+
throw new Error("Generation aborted");
|
|
153
|
+
}
|
|
154
|
+
|
|
114
155
|
if (isMountedRef.current) setState({ status: "success", isGenerating: false, result, error: null });
|
|
115
156
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
116
157
|
console.log("[Orchestrator] ✅ Generation SUCCESS");
|
|
@@ -126,7 +167,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
126
167
|
);
|
|
127
168
|
|
|
128
169
|
const generate = useCallback(
|
|
129
|
-
async (input: TInput) => {
|
|
170
|
+
async (input: TInput): Promise<TResult | void> => {
|
|
130
171
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
131
172
|
console.log("[Orchestrator] ========================================");
|
|
132
173
|
console.log("[Orchestrator] generate() called with input:", JSON.stringify(input).substring(0, 200));
|
|
@@ -145,7 +186,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
145
186
|
abortControllerRef.current = new AbortController();
|
|
146
187
|
isGeneratingRef.current = true;
|
|
147
188
|
let moderationPending = false;
|
|
148
|
-
setState({ ...
|
|
189
|
+
setState({ ...getInitialState<TResult>(), status: "checking", isGenerating: true });
|
|
149
190
|
|
|
150
191
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
151
192
|
console.log("[Orchestrator] State set to 'checking', isGenerating: true");
|
|
@@ -183,7 +224,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
183
224
|
isMountedRef,
|
|
184
225
|
isGeneratingRef,
|
|
185
226
|
setState: (s) => setState(s as GenerationState<TResult>),
|
|
186
|
-
resetState: () => setState(
|
|
227
|
+
resetState: () => setState(getInitialState<TResult>()),
|
|
187
228
|
executeGeneration,
|
|
188
229
|
showError,
|
|
189
230
|
onError,
|
|
@@ -227,7 +268,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
227
268
|
const reset = useCallback(() => {
|
|
228
269
|
abortControllerRef.current?.abort();
|
|
229
270
|
abortControllerRef.current = null;
|
|
230
|
-
setState(
|
|
271
|
+
setState(getInitialState<TResult>());
|
|
231
272
|
isGeneratingRef.current = false;
|
|
232
273
|
}, []);
|
|
233
274
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository Singleton
|
|
3
|
+
* Prevents multiple repository instances across hooks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createCreationsRepository } from "../../../domains/creations/infrastructure/adapters";
|
|
7
|
+
|
|
8
|
+
const REPO_COLLECTION = "creations";
|
|
9
|
+
|
|
10
|
+
let repositoryInstance: ReturnType<typeof createCreationsRepository> | null = null;
|
|
11
|
+
|
|
12
|
+
export function getCreationsRepository() {
|
|
13
|
+
if (!repositoryInstance) {
|
|
14
|
+
repositoryInstance = createCreationsRepository(REPO_COLLECTION);
|
|
15
|
+
}
|
|
16
|
+
return repositoryInstance;
|
|
17
|
+
}
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* - buildMetadata: creation metadata
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { useState, useCallback
|
|
12
|
+
import { useState, useCallback } from "react";
|
|
13
13
|
import { useGenerationServices } from "../../../infrastructure/providers/generation-services.provider";
|
|
14
14
|
import { resolveProvider } from "../../../infrastructure/services/provider-resolver";
|
|
15
|
-
import {
|
|
15
|
+
import { getCreationsRepository } from "./repositorySingleton";
|
|
16
16
|
import type { GenerationTarget } from "./useImageGenerationExecutor";
|
|
17
17
|
import { handleCreditRefund, logGenerationError, generateCreationId } from "./generation-execution.utils";
|
|
18
18
|
|
|
@@ -60,10 +60,7 @@ export function useAudioGenerationExecutor<P>(
|
|
|
60
60
|
): AudioGenerationExecutorReturn<P> {
|
|
61
61
|
const { userId, deductCredits, refundCredits, onGenerationSuccess } =
|
|
62
62
|
useGenerationServices();
|
|
63
|
-
const repository =
|
|
64
|
-
() => createCreationsRepository("creations"),
|
|
65
|
-
[],
|
|
66
|
-
);
|
|
63
|
+
const repository = getCreationsRepository();
|
|
67
64
|
const [error, setError] = useState<string | null>(null);
|
|
68
65
|
const [isLoading, setIsLoading] = useState(false);
|
|
69
66
|
|
|
@@ -106,7 +103,7 @@ export function useAudioGenerationExecutor<P>(
|
|
|
106
103
|
output: { audioUrl },
|
|
107
104
|
model: input.target.model,
|
|
108
105
|
prompt: input.prompt,
|
|
109
|
-
metadata: config.buildMetadata?.(params)
|
|
106
|
+
metadata: config.buildMetadata?.(params),
|
|
110
107
|
});
|
|
111
108
|
|
|
112
109
|
if (config.trackRating && onGenerationSuccess) {
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
* Auth and credit services come from GenerationServicesProvider context.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { useState, useCallback
|
|
14
|
+
import { useState, useCallback } from "react";
|
|
15
15
|
import { useGenerationServices } from "../../../infrastructure/providers/generation-services.provider";
|
|
16
16
|
import { resolveProvider } from "../../../infrastructure/services/provider-resolver";
|
|
17
|
-
import {
|
|
17
|
+
import { getCreationsRepository } from "./repositorySingleton";
|
|
18
18
|
import { preprocessImageInputs } from "../../../infrastructure/utils/image-input-preprocessor.util";
|
|
19
19
|
import { resolveType, handleCreditRefund, logGenerationError, generateCreationId } from "./generation-execution.utils";
|
|
20
20
|
|
|
@@ -66,10 +66,7 @@ export function useImageGenerationExecutor<P>(
|
|
|
66
66
|
): ImageGenerationExecutorReturn<P> {
|
|
67
67
|
const { userId, deductCredits, refundCredits, onGenerationSuccess } =
|
|
68
68
|
useGenerationServices();
|
|
69
|
-
const repository =
|
|
70
|
-
() => createCreationsRepository("creations"),
|
|
71
|
-
[],
|
|
72
|
-
);
|
|
69
|
+
const repository = getCreationsRepository();
|
|
73
70
|
const [error, setError] = useState<string | null>(null);
|
|
74
71
|
const [isLoading, setIsLoading] = useState(false);
|
|
75
72
|
|
|
@@ -26,7 +26,7 @@ export interface UseImagePickerReturn {
|
|
|
26
26
|
reset(): void;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export const useImagePicker = (options: UseImagePickerOptions
|
|
29
|
+
export const useImagePicker = (options: UseImagePickerOptions): UseImagePickerReturn => {
|
|
30
30
|
const { aspect = [1, 1], onError } = options;
|
|
31
31
|
|
|
32
32
|
const [uri, setUri] = useState<string | null>(null);
|
|
@@ -113,7 +113,3 @@ export function createFormStateHook<
|
|
|
113
113
|
return { state, actions };
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
|
-
|
|
117
|
-
// Note: createFormStateHookWithIndividualState removed due to type complexity
|
|
118
|
-
// and lack of usage. Use createFormStateHook instead which provides the same
|
|
119
|
-
// functionality with better type safety.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit Calculation Utilities
|
|
3
|
+
* Provides consistent credit calculation operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validates if a value is a valid credit amount
|
|
8
|
+
* @param credits - Credit value to validate
|
|
9
|
+
* @returns True if valid (non-negative number)
|
|
10
|
+
*/
|
|
11
|
+
export function isValidCreditAmount(credits: number): boolean {
|
|
12
|
+
return typeof credits === "number" && !isNaN(credits) && credits >= 0 && isFinite(credits);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Calculates total credits from multiple items
|
|
17
|
+
* @param items - Array of credit costs
|
|
18
|
+
* @returns Total credits
|
|
19
|
+
*/
|
|
20
|
+
export function sumCredits(...items: number[]): number {
|
|
21
|
+
return items.reduce((sum, credits) => {
|
|
22
|
+
if (!isValidCreditAmount(credits)) return sum;
|
|
23
|
+
return sum + credits;
|
|
24
|
+
}, 0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Calculates credits per unit from total cost
|
|
29
|
+
* @param totalCost - Total cost in base currency
|
|
30
|
+
* @param units - Number of units
|
|
31
|
+
* @returns Cost per unit
|
|
32
|
+
*/
|
|
33
|
+
export function calculateCostPerUnit(totalCost: number, units: number): number {
|
|
34
|
+
if (units <= 0) return 0;
|
|
35
|
+
return totalCost / units;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Calculates total cost from per-unit cost
|
|
40
|
+
* @param costPerUnit - Cost per single unit
|
|
41
|
+
* @param units - Number of units
|
|
42
|
+
* @returns Total cost
|
|
43
|
+
*/
|
|
44
|
+
export function calculateTotalCost(costPerUnit: number, units: number): number {
|
|
45
|
+
if (units <= 0) return 0;
|
|
46
|
+
return costPerUnit * units;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Formats credits for display
|
|
51
|
+
* @param credits - Credit amount
|
|
52
|
+
* @returns Formatted string
|
|
53
|
+
*/
|
|
54
|
+
export function formatCredits(credits: number): string {
|
|
55
|
+
if (!isValidCreditAmount(credits)) return "0";
|
|
56
|
+
return Math.floor(credits).toString();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parses credits from a string value
|
|
61
|
+
* @param value - String value to parse
|
|
62
|
+
* @param defaultValue - Default value if parsing fails (default: 0)
|
|
63
|
+
* @returns Parsed credits
|
|
64
|
+
*/
|
|
65
|
+
export function parseCredits(value: string, defaultValue: number = 0): number {
|
|
66
|
+
const parsed = parseFloat(value);
|
|
67
|
+
if (isNaN(parsed)) return defaultValue;
|
|
68
|
+
return Math.max(0, parsed);
|
|
69
|
+
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Date Utilities
|
|
3
|
-
*
|
|
3
|
+
* Date normalization and timestamp operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
normalizeToDate,
|
|
9
|
-
isValidDate,
|
|
10
|
-
compareDates,
|
|
11
|
-
} from "./normalization";
|
|
6
|
+
export * from "./normalization";
|
|
7
|
+
export * from "./timestamp";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timestamp Utilities
|
|
3
|
+
* Provides consistent timestamp operations across the application
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Gets the current timestamp in milliseconds
|
|
8
|
+
* @returns Current timestamp in milliseconds
|
|
9
|
+
*/
|
|
10
|
+
export function getCurrentTimestamp(): number {
|
|
11
|
+
return Date.now();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Calculates elapsed time in milliseconds
|
|
16
|
+
* @param startTime - Start timestamp in milliseconds
|
|
17
|
+
* @param endTime - End timestamp in milliseconds (defaults to current time)
|
|
18
|
+
* @returns Elapsed time in milliseconds
|
|
19
|
+
*/
|
|
20
|
+
export function getElapsedMs(startTime: number, endTime?: number): number {
|
|
21
|
+
const end = endTime ?? Date.now();
|
|
22
|
+
return end - startTime;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Formats elapsed time in milliseconds to a human-readable string
|
|
27
|
+
* @param elapsedMs - Elapsed time in milliseconds
|
|
28
|
+
* @returns Formatted string (e.g., "1.5s", "500ms", "2m 30s")
|
|
29
|
+
*/
|
|
30
|
+
export function formatElapsedMs(elapsedMs: number): string {
|
|
31
|
+
if (elapsedMs < 1000) {
|
|
32
|
+
return `${elapsedMs}ms`;
|
|
33
|
+
}
|
|
34
|
+
if (elapsedMs < 60000) {
|
|
35
|
+
return `${(elapsedMs / 1000).toFixed(1)}s`;
|
|
36
|
+
}
|
|
37
|
+
const minutes = Math.floor(elapsedMs / 60000);
|
|
38
|
+
const seconds = Math.floor((elapsedMs % 60000) / 1000);
|
|
39
|
+
return `${minutes}m ${seconds}s`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates a unique timestamp-based ID
|
|
44
|
+
* @param prefix - Optional prefix for the ID
|
|
45
|
+
* @returns Unique ID with timestamp
|
|
46
|
+
*/
|
|
47
|
+
export function createTimestampId(prefix: string = ""): string {
|
|
48
|
+
const timestamp = Date.now();
|
|
49
|
+
const random = Math.random().toString(36).slice(2, 9);
|
|
50
|
+
return prefix ? `${prefix}-${timestamp}-${random}` : `${timestamp}-${random}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Converts a timestamp to ISO string
|
|
55
|
+
* @param timestamp - Timestamp in milliseconds
|
|
56
|
+
* @returns ISO 8601 formatted string
|
|
57
|
+
*/
|
|
58
|
+
export function timestampToISOString(timestamp: number): string {
|
|
59
|
+
return new Date(timestamp).toISOString();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Checks if a timestamp is recent (within the specified milliseconds)
|
|
64
|
+
* @param timestamp - Timestamp to check
|
|
65
|
+
* @param recentMs - Threshold for "recent" in milliseconds (default: 5000ms)
|
|
66
|
+
* @returns True if timestamp is within the threshold
|
|
67
|
+
*/
|
|
68
|
+
export function isTimestampRecent(timestamp: number, recentMs: number = 5000): boolean {
|
|
69
|
+
const elapsed = Date.now() - timestamp;
|
|
70
|
+
return elapsed >= 0 && elapsed <= recentMs;
|
|
71
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress Utilities
|
|
3
|
+
* Provides consistent progress calculations and mappings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Clamps a value between 0 and 100
|
|
8
|
+
* @param value - Value to clamp
|
|
9
|
+
* @returns Clamped value between 0 and 100
|
|
10
|
+
*/
|
|
11
|
+
export function clampProgress(value: number): number {
|
|
12
|
+
return Math.max(0, Math.min(100, value));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Maps a value from one range to another (0-100)
|
|
17
|
+
* @param value - Value to map
|
|
18
|
+
* @param min - Minimum of input range
|
|
19
|
+
* @param max - Maximum of input range
|
|
20
|
+
* @returns Mapped value in 0-100 range
|
|
21
|
+
*/
|
|
22
|
+
export function mapToProgress(value: number, min: number, max: number): number {
|
|
23
|
+
if (max === min) return 0;
|
|
24
|
+
const normalized = (value - min) / (max - min);
|
|
25
|
+
return clampProgress(normalized * 100);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Maps a value from one range to another (0-100) with optional clamping
|
|
30
|
+
* @param value - Value to map
|
|
31
|
+
* @param min - Minimum of input range
|
|
32
|
+
* @param max - Maximum of input range
|
|
33
|
+
* @param clamp - Whether to clamp the result (default: true)
|
|
34
|
+
* @returns Mapped value in 0-100 range
|
|
35
|
+
*/
|
|
36
|
+
export function mapRangeToProgress(
|
|
37
|
+
value: number,
|
|
38
|
+
min: number,
|
|
39
|
+
max: number,
|
|
40
|
+
clamp: boolean = true
|
|
41
|
+
): number {
|
|
42
|
+
if (max === min) return 0;
|
|
43
|
+
const normalized = (value - min) / (max - min);
|
|
44
|
+
const result = normalized * 100;
|
|
45
|
+
return clamp ? clampProgress(result) : result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Calculates progress percentage from current and total
|
|
50
|
+
* @param current - Current value
|
|
51
|
+
* @param total - Total value
|
|
52
|
+
* @returns Progress percentage (0-100)
|
|
53
|
+
*/
|
|
54
|
+
export function calculateProgress(current: number, total: number): number {
|
|
55
|
+
if (total <= 0) return 0;
|
|
56
|
+
if (current >= total) return 100;
|
|
57
|
+
return clampProgress((current / total) * 100);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Interpolates between two values based on progress (0-100)
|
|
62
|
+
* @param start - Start value
|
|
63
|
+
* @param end - End value
|
|
64
|
+
* @param progress - Progress (0-100)
|
|
65
|
+
* @returns Interpolated value
|
|
66
|
+
*/
|
|
67
|
+
export function interpolateProgress(start: number, end: number, progress: number): number {
|
|
68
|
+
const clampedProgress = clampProgress(progress);
|
|
69
|
+
const factor = clampedProgress / 100;
|
|
70
|
+
return start + (end - start) * factor;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Converts linear progress to eased progress (smooth animation)
|
|
75
|
+
* @param progress - Linear progress (0-100)
|
|
76
|
+
* @param easing - Easing function (default: ease-out cubic)
|
|
77
|
+
* @returns Eased progress (0-100)
|
|
78
|
+
*/
|
|
79
|
+
export function easeProgress(
|
|
80
|
+
progress: number,
|
|
81
|
+
easing: "linear" | "easeIn" | "easeOut" | "easeInOut" = "easeOut"
|
|
82
|
+
): number {
|
|
83
|
+
const clamped = clampProgress(progress);
|
|
84
|
+
const normalized = clamped / 100;
|
|
85
|
+
|
|
86
|
+
let eased: number;
|
|
87
|
+
switch (easing) {
|
|
88
|
+
case "linear":
|
|
89
|
+
eased = normalized;
|
|
90
|
+
break;
|
|
91
|
+
case "easeIn":
|
|
92
|
+
eased = normalized * normalized;
|
|
93
|
+
break;
|
|
94
|
+
case "easeOut":
|
|
95
|
+
eased = 1 - Math.pow(1 - normalized, 2);
|
|
96
|
+
break;
|
|
97
|
+
case "easeInOut":
|
|
98
|
+
eased = normalized < 0.5
|
|
99
|
+
? 2 * normalized * normalized
|
|
100
|
+
: 1 - Math.pow(-2 * normalized + 2, 2) / 2;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return eased * 100;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Checks if progress is complete (100)
|
|
109
|
+
* @param progress - Progress value to check
|
|
110
|
+
* @returns True if progress is 100
|
|
111
|
+
*/
|
|
112
|
+
export function isProgressComplete(progress: number): boolean {
|
|
113
|
+
return progress >= 100;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Checks if progress has started (> 0)
|
|
118
|
+
* @param progress - Progress value to check
|
|
119
|
+
* @returns True if progress is greater than 0
|
|
120
|
+
*/
|
|
121
|
+
export function hasProgressStarted(progress: number): boolean {
|
|
122
|
+
return progress > 0;
|
|
123
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./services";
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./services";
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { ExamplePrompts, type ExamplePromptsProps } from "./ExamplePrompts";
|