@umituz/react-native-ai-generation-content 1.70.2 → 1.70.4
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/screens/TextInputScreen.tsx +1 -1
- package/src/domains/image-to-video/presentation/components/ImageSelectionGrid.tsx +1 -1
- package/src/domains/text-to-video/presentation/hooks/useTextToVideoForm.ts +29 -10
- package/src/infrastructure/http/http-response-parser.ts +14 -2
- package/src/infrastructure/services/generation-orchestrator.service.ts +6 -6
- package/src/infrastructure/services/image-feature-executor.service.ts +10 -0
- package/src/infrastructure/services/multi-image-generation.executor.ts +28 -6
- package/src/infrastructure/services/video-feature-executor.service.ts +10 -0
- package/src/infrastructure/utils/error-extractors.ts +12 -11
- package/src/infrastructure/utils/error-handlers.ts +32 -1
- package/src/infrastructure/utils/feature-utils.ts +10 -0
- package/src/infrastructure/utils/url-extractor/base-extractor.ts +5 -2
- package/src/infrastructure/validation/ai-validator.ts +20 -6
- package/src/infrastructure/validation/sanitizer.ts +19 -6
- package/src/presentation/components/AIGenerationConfig.tsx +1 -1
- package/src/presentation/components/selectors/GridSelector.tsx +1 -1
- package/src/presentation/hooks/generation/moderation-handler.ts +6 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.70.
|
|
3
|
+
"version": "1.70.4",
|
|
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",
|
|
@@ -104,7 +104,7 @@ export const TextInputScreen: React.FC<TextInputScreenProps> = ({
|
|
|
104
104
|
</AtomicText>
|
|
105
105
|
{examplePrompts.slice(0, 4).map((example, index) => (
|
|
106
106
|
<AtomicButton
|
|
107
|
-
key={index}
|
|
107
|
+
key={`${example}-${index}`}
|
|
108
108
|
variant="outline"
|
|
109
109
|
size="sm"
|
|
110
110
|
onPress={() => handleExampleSelect(example)}
|
|
@@ -30,7 +30,7 @@ export interface UseTextToVideoFormReturn {
|
|
|
30
30
|
setSoundEnabled: (enabled: boolean) => void;
|
|
31
31
|
setProfessionalMode: (enabled: boolean) => void;
|
|
32
32
|
setFrames: (frames: FrameData[]) => void;
|
|
33
|
-
handleFrameChange: (
|
|
33
|
+
handleFrameChange: (fromIndex: number, toIndex: number) => void;
|
|
34
34
|
handleFrameDelete: (index: number) => void;
|
|
35
35
|
selectExamplePrompt: (prompt: string) => void;
|
|
36
36
|
reset: () => void;
|
|
@@ -88,19 +88,38 @@ export function useTextToVideoForm(
|
|
|
88
88
|
setState((prev) => ({ ...prev, professionalMode }));
|
|
89
89
|
}, []);
|
|
90
90
|
|
|
91
|
-
const handleFrameChange = useCallback(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
const handleFrameChange = useCallback(
|
|
92
|
+
(fromIndex: number, toIndex: number) => {
|
|
93
|
+
if (fromIndex < 0 || fromIndex >= frames.length || toIndex < 0 || toIndex >= frames.length) {
|
|
94
|
+
if (__DEV__) {
|
|
95
|
+
console.warn("[TextToVideoForm] Invalid frame indices:", { fromIndex, toIndex, length: frames.length });
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (fromIndex === toIndex) return;
|
|
101
|
+
|
|
102
|
+
setFrames((prevFrames) => {
|
|
103
|
+
const newFrames = [...prevFrames];
|
|
104
|
+
const [movedFrame] = newFrames.splice(fromIndex, 1);
|
|
105
|
+
newFrames.splice(toIndex, 0, movedFrame);
|
|
106
|
+
return newFrames;
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
[frames.length],
|
|
110
|
+
);
|
|
97
111
|
|
|
98
112
|
const handleFrameDelete = useCallback(
|
|
99
113
|
(index: number) => {
|
|
100
|
-
|
|
101
|
-
|
|
114
|
+
if (index < 0 || index >= frames.length) {
|
|
115
|
+
if (__DEV__) {
|
|
116
|
+
console.warn("[TextToVideoForm] Invalid frame index:", index);
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
setFrames((prevFrames) => prevFrames.filter((_, i) => i !== index));
|
|
102
121
|
},
|
|
103
|
-
[frames],
|
|
122
|
+
[frames.length],
|
|
104
123
|
);
|
|
105
124
|
|
|
106
125
|
const selectExamplePrompt = useCallback(
|
|
@@ -7,15 +7,27 @@ import { HTTP_CONTENT_TYPE } from "./http-methods.constants";
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Parses response based on content type
|
|
10
|
+
* Note: Type T is not validated at runtime - caller must ensure type safety
|
|
10
11
|
*/
|
|
11
12
|
export async function parseResponse<T>(response: Response): Promise<T> {
|
|
12
13
|
const contentType = response.headers.get("content-type");
|
|
13
14
|
|
|
14
15
|
if (contentType?.includes(HTTP_CONTENT_TYPE.JSON)) {
|
|
15
|
-
|
|
16
|
+
try {
|
|
17
|
+
return await response.json();
|
|
18
|
+
} catch (error) {
|
|
19
|
+
throw new Error(`Failed to parse JSON response: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
20
|
+
}
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
return
|
|
23
|
+
// For non-JSON responses, return text
|
|
24
|
+
// WARNING: Type T is not validated - caller is responsible for type safety
|
|
25
|
+
try {
|
|
26
|
+
const text = await response.text();
|
|
27
|
+
return text as T;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
throw new Error(`Failed to parse text response: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
30
|
+
}
|
|
19
31
|
}
|
|
20
32
|
|
|
21
33
|
/**
|
|
@@ -45,9 +45,9 @@ class GenerationOrchestratorService {
|
|
|
45
45
|
|
|
46
46
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
47
47
|
console.log("[Orchestrator] Generate started:", {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
hasModel: !!request.model,
|
|
49
|
+
hasCapability: !!request.capability,
|
|
50
|
+
timestamp: new Date().toISOString(),
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -56,8 +56,8 @@ class GenerationOrchestratorService {
|
|
|
56
56
|
|
|
57
57
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
58
58
|
console.log("[Orchestrator] Job submitted:", {
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
hasRequestId: !!submission.requestId,
|
|
60
|
+
timestamp: new Date().toISOString(),
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -91,9 +91,9 @@ class GenerationOrchestratorService {
|
|
|
91
91
|
|
|
92
92
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
93
93
|
console.log("[Orchestrator] Generate completed:", {
|
|
94
|
-
requestId: submission.requestId,
|
|
95
94
|
duration: `${duration}ms`,
|
|
96
95
|
success: true,
|
|
96
|
+
timestamp: new Date().toISOString(),
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { extractErrorMessage, validateProvider, prepareImageInputData } from "../utils";
|
|
8
8
|
import { extractImageResult } from "../utils/url-extractor";
|
|
9
|
+
import { validateURL } from "../validation/base-validator";
|
|
9
10
|
import type { ImageResultExtractor } from "../utils/url-extractor";
|
|
10
11
|
import type { ImageFeatureType } from "../../domain/interfaces";
|
|
11
12
|
|
|
@@ -62,6 +63,15 @@ export async function executeImageFeature(
|
|
|
62
63
|
return { success: false, error: "No image in response" };
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
// Validate the extracted URL for security (prevent SSRF, invalid protocols, etc.)
|
|
67
|
+
const urlValidation = validateURL(imageUrl);
|
|
68
|
+
if (!urlValidation.isValid) {
|
|
69
|
+
return {
|
|
70
|
+
success: false,
|
|
71
|
+
error: `Invalid image URL received: ${urlValidation.errors.join(", ")}`
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
65
75
|
return {
|
|
66
76
|
success: true,
|
|
67
77
|
imageUrl,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { validateProvider } from "../utils/provider-validator.util";
|
|
8
8
|
import { formatBase64 } from "../utils/base64.util";
|
|
9
|
+
import { validateURL } from "../validation/base-validator";
|
|
9
10
|
import { env } from "../config/env.config";
|
|
10
11
|
|
|
11
12
|
declare const __DEV__: boolean;
|
|
@@ -59,7 +60,9 @@ export async function executeMultiImageGeneration(
|
|
|
59
60
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
60
61
|
console.log("[MultiImageExecutor] Generation started", {
|
|
61
62
|
imageCount: imageUrls.length,
|
|
62
|
-
|
|
63
|
+
hasModel: !!input.model,
|
|
64
|
+
hasPrompt: !!input.prompt,
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
63
66
|
});
|
|
64
67
|
}
|
|
65
68
|
|
|
@@ -77,12 +80,31 @@ export async function executeMultiImageGeneration(
|
|
|
77
80
|
});
|
|
78
81
|
|
|
79
82
|
const rawResult = result as Record<string, unknown>;
|
|
80
|
-
const data =
|
|
81
|
-
const imageUrl = data?.images?.[0]?.url;
|
|
83
|
+
const data = rawResult?.data ?? rawResult;
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
// Type-safe extraction of image URL
|
|
86
|
+
if (data && typeof data === "object" && "images" in data) {
|
|
87
|
+
const images = (data as { images?: unknown }).images;
|
|
88
|
+
if (Array.isArray(images) && images.length > 0) {
|
|
89
|
+
const firstImage = images[0];
|
|
90
|
+
if (firstImage && typeof firstImage === "object" && "url" in firstImage) {
|
|
91
|
+
const imageUrl = (firstImage as { url?: unknown }).url;
|
|
92
|
+
if (typeof imageUrl === "string" && imageUrl.length > 0) {
|
|
93
|
+
// Validate URL for security
|
|
94
|
+
const urlValidation = validateURL(imageUrl);
|
|
95
|
+
if (!urlValidation.isValid) {
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
error: `Invalid image URL received: ${urlValidation.errors.join(", ")}`
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return { success: true, imageUrl };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { success: false, error: "No image generated" };
|
|
86
108
|
} catch (error) {
|
|
87
109
|
const message = error instanceof Error ? error.message : "Generation failed";
|
|
88
110
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { extractErrorMessage, validateProvider, prepareVideoInputData } from "../utils";
|
|
8
8
|
import { extractVideoResult } from "../utils/url-extractor";
|
|
9
|
+
import { validateURL } from "../validation/base-validator";
|
|
9
10
|
import { DEFAULT_MAX_POLL_TIME_MS } from "../constants";
|
|
10
11
|
import type { VideoFeatureType } from "../../domain/interfaces";
|
|
11
12
|
import type { ExecuteVideoFeatureOptions, VideoFeatureResult, VideoFeatureRequest } from "./video-feature-executor.types";
|
|
@@ -48,6 +49,15 @@ export async function executeVideoFeature(
|
|
|
48
49
|
return { success: false, error: "No video in response" };
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
// Validate URL for security (prevent SSRF, invalid protocols, etc.)
|
|
53
|
+
const urlValidation = validateURL(videoUrl);
|
|
54
|
+
if (!urlValidation.isValid) {
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
error: `Invalid video URL received: ${urlValidation.errors.join(", ")}`
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
return {
|
|
52
62
|
success: true,
|
|
53
63
|
videoUrl,
|
|
@@ -4,20 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Safely extracts error message from unknown error type
|
|
7
|
+
* @param error - The error to extract message from
|
|
8
|
+
* @param prefix - Optional prefix to prepend to error message
|
|
9
|
+
* @returns The extracted error message with optional prefix
|
|
7
10
|
*/
|
|
8
|
-
export function getErrorMessage(error: unknown): string {
|
|
9
|
-
|
|
10
|
-
return error.message;
|
|
11
|
-
}
|
|
11
|
+
export function getErrorMessage(error: unknown, prefix?: string): string {
|
|
12
|
+
let message = "An unknown error occurred";
|
|
12
13
|
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (error && typeof error === "object" && "message" in error) {
|
|
18
|
-
|
|
14
|
+
if (error instanceof Error) {
|
|
15
|
+
message = error.message;
|
|
16
|
+
} else if (typeof error === "string") {
|
|
17
|
+
message = error;
|
|
18
|
+
} else if (error && typeof error === "object" && "message" in error) {
|
|
19
|
+
message = String(error.message);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
return
|
|
22
|
+
return prefix ? `${prefix}: ${message}` : message;
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -4,6 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
import { getErrorMessage } from "./error-extractors";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Sanitizes error message to prevent information disclosure
|
|
9
|
+
* Removes sensitive data like file paths, API keys, stack traces
|
|
10
|
+
*/
|
|
11
|
+
function sanitizeErrorMessage(message: string): string {
|
|
12
|
+
if (!message) return "An error occurred";
|
|
13
|
+
|
|
14
|
+
// Remove file paths (Unix and Windows style)
|
|
15
|
+
let sanitized = message.replace(/\/[\w\/\-.]+/g, "[PATH]");
|
|
16
|
+
sanitized = sanitized.replace(/[A-Z]:\\[\w\\\-.]+/g, "[PATH]");
|
|
17
|
+
|
|
18
|
+
// Remove potential API keys or tokens (common patterns)
|
|
19
|
+
sanitized = sanitized.replace(/[a-z0-9]{32,}/gi, "[TOKEN]");
|
|
20
|
+
|
|
21
|
+
// Remove stack trace lines
|
|
22
|
+
sanitized = sanitized.split("\n")[0];
|
|
23
|
+
|
|
24
|
+
// Limit length
|
|
25
|
+
return sanitized.slice(0, 500);
|
|
26
|
+
}
|
|
27
|
+
|
|
7
28
|
/**
|
|
8
29
|
* Wraps an async function with error handling
|
|
9
30
|
*/
|
|
@@ -15,9 +36,19 @@ export async function withErrorHandling<T>(
|
|
|
15
36
|
const data = await operation();
|
|
16
37
|
return { data };
|
|
17
38
|
} catch (error) {
|
|
18
|
-
const
|
|
39
|
+
const errorMessage = getErrorMessage(error);
|
|
40
|
+
const sanitizedMessage = sanitizeErrorMessage(errorMessage);
|
|
41
|
+
const appError = new Error(sanitizedMessage);
|
|
42
|
+
|
|
43
|
+
// In production, don't expose original error details
|
|
44
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
45
|
+
console.error("[withErrorHandling] Original error:", error);
|
|
46
|
+
}
|
|
47
|
+
|
|
19
48
|
onError?.(appError);
|
|
20
49
|
return { error: appError };
|
|
21
50
|
}
|
|
22
51
|
}
|
|
23
52
|
|
|
53
|
+
declare const __DEV__: boolean;
|
|
54
|
+
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { readFileAsBase64 } from "@umituz/react-native-design-system";
|
|
7
7
|
import { getAuthService, getCreditService, getPaywallService, isAppServicesConfigured } from "../config/app-services.config";
|
|
8
|
+
import { env } from "../config/env.config";
|
|
8
9
|
|
|
9
10
|
declare const __DEV__: boolean;
|
|
10
11
|
|
|
@@ -37,6 +38,15 @@ export async function prepareImage(uri: string): Promise<string> {
|
|
|
37
38
|
throw new Error("[prepareImage] Failed to convert image to base64");
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
// Validate base64 size to prevent memory exhaustion
|
|
42
|
+
const maxSizeBytes = env.validationMaxBase64SizeMb * 1024 * 1024;
|
|
43
|
+
if (base64.length > maxSizeBytes) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`[prepareImage] Image exceeds maximum size of ${env.validationMaxBase64SizeMb}MB. ` +
|
|
46
|
+
`Current size: ${(base64.length / (1024 * 1024)).toFixed(2)}MB`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
return base64;
|
|
41
51
|
} catch (error) {
|
|
42
52
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -41,8 +41,11 @@ export function extractOutputUrl(
|
|
|
41
41
|
const topMedia =
|
|
42
42
|
(resultObj.image as Record<string, unknown>) ||
|
|
43
43
|
(resultObj.video as Record<string, unknown>);
|
|
44
|
-
if (topMedia && typeof topMedia === "object"
|
|
45
|
-
|
|
44
|
+
if (topMedia && typeof topMedia === "object") {
|
|
45
|
+
const url = topMedia.url;
|
|
46
|
+
if (typeof url === "string" && url.length > 0) {
|
|
47
|
+
return url;
|
|
48
|
+
}
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
// Check nested data/output objects
|
|
@@ -29,21 +29,35 @@ export function validateImageData(input: unknown): ValidationResult {
|
|
|
29
29
|
return { isValid: false, errors: ["Image data must be a string"] };
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
const trimmed = input.trim();
|
|
33
|
+
|
|
34
|
+
// Validate HTTP/HTTPS URLs
|
|
35
|
+
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
|
|
36
|
+
return validateURL(trimmed);
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
// Validate data URI format
|
|
40
|
+
if (trimmed.startsWith("data:image/")) {
|
|
41
|
+
const parts = trimmed.split(",");
|
|
42
|
+
if (parts.length !== 2) {
|
|
39
43
|
return { isValid: false, errors: ["Invalid data URI format"] };
|
|
40
44
|
}
|
|
45
|
+
const base64Part = parts[1];
|
|
46
|
+
if (!base64Part || base64Part.length === 0) {
|
|
47
|
+
return { isValid: false, errors: ["Invalid data URI: missing base64 data"] };
|
|
48
|
+
}
|
|
41
49
|
return validateBase64(base64Part);
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
// Validate standalone base64 string (fallback)
|
|
53
|
+
const base64Result = validateBase64(trimmed);
|
|
54
|
+
if (base64Result.isValid) {
|
|
55
|
+
return base64Result;
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
return {
|
|
45
59
|
isValid: false,
|
|
46
|
-
errors: ["Image data must be a URL
|
|
60
|
+
errors: ["Image data must be a URL, base64 data URI, or valid base64 string"],
|
|
47
61
|
};
|
|
48
62
|
}
|
|
49
63
|
|
|
@@ -11,17 +11,30 @@ export function sanitizeString(input: unknown): string {
|
|
|
11
11
|
return "";
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
let sanitized = input.trim();
|
|
15
|
+
|
|
16
|
+
// Remove dangerous HTML tags and protocols
|
|
17
|
+
sanitized = sanitized
|
|
16
18
|
.replace(/[<>]/g, "")
|
|
17
19
|
.replace(/javascript:/gi, "")
|
|
18
|
-
.replace(/data
|
|
20
|
+
.replace(/data:(?!image\/)/gi, "") // Allow data:image/ for valid use cases
|
|
19
21
|
.replace(/vbscript:/gi, "")
|
|
20
|
-
.replace(/
|
|
22
|
+
.replace(/file:/gi, "")
|
|
23
|
+
.replace(/on\w+\s*=/gi, "");
|
|
24
|
+
|
|
25
|
+
// Remove SQL injection patterns
|
|
26
|
+
sanitized = sanitized
|
|
21
27
|
.replace(/--/g, "")
|
|
22
28
|
.replace(/;\s*drop\s+/gi, "")
|
|
23
|
-
.replace(
|
|
24
|
-
.
|
|
29
|
+
.replace(/;\s*delete\s+/gi, "")
|
|
30
|
+
.replace(/;\s*insert\s+/gi, "")
|
|
31
|
+
.replace(/;\s*update\s+/gi, "");
|
|
32
|
+
|
|
33
|
+
// Remove Unicode escape sequences that could bypass filters
|
|
34
|
+
sanitized = sanitized.replace(/\\u[\da-fA-F]{4}/g, "");
|
|
35
|
+
|
|
36
|
+
// Limit length to prevent DoS
|
|
37
|
+
return sanitized.slice(0, 10000);
|
|
25
38
|
}
|
|
26
39
|
|
|
27
40
|
/**
|
|
@@ -36,7 +36,7 @@ export const AIGenerationConfig: React.FC<AIGenerationConfigProps> = ({
|
|
|
36
36
|
{images.length > 0 && (
|
|
37
37
|
<View style={styles.imagePreviewContainer}>
|
|
38
38
|
{images.map((img, index) => (
|
|
39
|
-
<View key={index} style={styles.imageWrapper}>
|
|
39
|
+
<View key={img.uri || `image-${index}`} style={styles.imageWrapper}>
|
|
40
40
|
<Image
|
|
41
41
|
source={{ uri: img.previewUrl || img.uri }}
|
|
42
42
|
style={styles.previewImage}
|
|
@@ -52,10 +52,9 @@ export async function handleModeration<TInput, TResult>(
|
|
|
52
52
|
isGeneratingRef.current = false;
|
|
53
53
|
if (isMountedRef.current) resetState();
|
|
54
54
|
},
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
} catch (err) {
|
|
55
|
+
() => {
|
|
56
|
+
// Return the promise to allow proper error handling chain
|
|
57
|
+
return executeGeneration(input).catch((err) => {
|
|
59
58
|
const error = parseError(err);
|
|
60
59
|
if (isMountedRef.current) {
|
|
61
60
|
setState({ status: "error", isGenerating: false, result: null, error });
|
|
@@ -63,9 +62,10 @@ export async function handleModeration<TInput, TResult>(
|
|
|
63
62
|
showError("Error", getAlertMessage(error, alertMessages));
|
|
64
63
|
onError?.(error);
|
|
65
64
|
handleLifecycleComplete("error", undefined, error);
|
|
66
|
-
|
|
65
|
+
throw error; // Re-throw to allow caller to handle
|
|
66
|
+
}).finally(() => {
|
|
67
67
|
isGeneratingRef.current = false;
|
|
68
|
-
}
|
|
68
|
+
});
|
|
69
69
|
},
|
|
70
70
|
);
|
|
71
71
|
return undefined;
|