@umituz/react-native-ai-generation-content 1.61.32 → 1.61.34
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/domain/entities/error.types.ts +0 -1
- package/src/domain/entities/job.types.ts +0 -4
- package/src/domain/entities/polling.types.ts +1 -3
- package/src/domain/interfaces/app-services.interface.ts +20 -2
- package/src/domains/content-moderation/infrastructure/services/content-moderation.service.ts +2 -1
- package/src/domains/content-moderation/infrastructure/services/moderators/text.moderator.ts +84 -4
- package/src/domains/content-moderation/infrastructure/services/pattern-matcher.service.ts +85 -2
- package/src/domains/creations/infrastructure/repositories/CreationsWriter.ts +102 -19
- package/src/domains/creations/presentation/hooks/useAdvancedFilter.ts +13 -4
- package/src/domains/creations/presentation/hooks/useProcessingJobsPoller.ts +10 -9
- package/src/domains/face-detection/presentation/hooks/useFaceDetection.ts +1 -1
- package/src/domains/generation/infrastructure/flow/useFlow.ts +11 -6
- package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +2 -3
- package/src/exports/infrastructure.ts +24 -1
- package/src/exports/presentation.ts +1 -1
- package/src/features/image-to-video/presentation/components/index.ts +0 -4
- package/src/index.ts +0 -4
- package/src/infrastructure/constants/index.ts +14 -2
- package/src/infrastructure/constants/polling.constants.ts +34 -0
- package/src/infrastructure/constants/storage.constants.ts +25 -0
- package/src/infrastructure/constants/validation.constants.ts +40 -0
- package/src/infrastructure/logging/logger.ts +185 -0
- package/src/infrastructure/services/job-poller.service.ts +13 -28
- package/src/infrastructure/utils/status-checker.util.ts +27 -7
- package/src/infrastructure/validation/input-validator.ts +406 -0
- package/src/presentation/components/AIGenerationForm.tsx +0 -11
- package/src/presentation/components/ErrorBoundary.tsx +141 -0
- package/src/presentation/components/index.ts +1 -0
- package/src/presentation/hooks/use-background-generation.ts +0 -12
- package/src/presentation/hooks/useAIFeatureCallbacks.ts +3 -4
- package/src/presentation/types/result-config.types.ts +0 -7
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Validation Utilities
|
|
3
|
+
* Provides comprehensive input validation for security and data integrity
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
MAX_PROMPT_LENGTH,
|
|
8
|
+
MIN_PROMPT_LENGTH,
|
|
9
|
+
MAX_USER_ID_LENGTH,
|
|
10
|
+
MAX_CREATION_ID_LENGTH,
|
|
11
|
+
} from "../constants/validation.constants";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validation result type
|
|
15
|
+
*/
|
|
16
|
+
export interface ValidationResult {
|
|
17
|
+
readonly isValid: boolean;
|
|
18
|
+
readonly errors: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* String validation options
|
|
23
|
+
*/
|
|
24
|
+
export interface StringValidationOptions {
|
|
25
|
+
readonly minLength?: number;
|
|
26
|
+
readonly maxLength?: number;
|
|
27
|
+
readonly pattern?: RegExp;
|
|
28
|
+
readonly allowedCharacters?: RegExp;
|
|
29
|
+
readonly trim?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Numeric validation options
|
|
34
|
+
*/
|
|
35
|
+
export interface NumericValidationOptions {
|
|
36
|
+
readonly min?: number;
|
|
37
|
+
readonly max?: number;
|
|
38
|
+
readonly integer?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Sanitizes user input to prevent XSS and injection attacks
|
|
43
|
+
*/
|
|
44
|
+
export function sanitizeString(input: unknown): string {
|
|
45
|
+
if (typeof input !== "string") {
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return input
|
|
50
|
+
.trim()
|
|
51
|
+
.replace(/[<>]/g, "") // Remove potential HTML tags
|
|
52
|
+
.replace(/javascript:/gi, "") // Remove javascript: protocol
|
|
53
|
+
.replace(/on\w+\s*=/gi, "") // Remove event handlers
|
|
54
|
+
.slice(0, 10000); // Limit length
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Validates a string input against provided rules
|
|
59
|
+
*/
|
|
60
|
+
export function validateString(
|
|
61
|
+
input: unknown,
|
|
62
|
+
options: StringValidationOptions = {}
|
|
63
|
+
): ValidationResult {
|
|
64
|
+
const errors: string[] = [];
|
|
65
|
+
|
|
66
|
+
// Check if input is a string
|
|
67
|
+
if (typeof input !== "string") {
|
|
68
|
+
return {
|
|
69
|
+
isValid: false,
|
|
70
|
+
errors: ["Input must be a string"],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let value = options.trim !== false ? input.trim() : input;
|
|
75
|
+
|
|
76
|
+
// Check min length
|
|
77
|
+
if (options.minLength !== undefined && value.length < options.minLength) {
|
|
78
|
+
errors.push(`Input must be at least ${options.minLength} characters`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check max length
|
|
82
|
+
if (options.maxLength !== undefined && value.length > options.maxLength) {
|
|
83
|
+
errors.push(`Input must be at most ${options.maxLength} characters`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check pattern
|
|
87
|
+
if (options.pattern && !options.pattern.test(value)) {
|
|
88
|
+
errors.push("Input format is invalid");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check allowed characters
|
|
92
|
+
if (options.allowedCharacters && !options.allowedCharacters.test(value)) {
|
|
93
|
+
errors.push("Input contains invalid characters");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
isValid: errors.length === 0,
|
|
98
|
+
errors,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validates a numeric input
|
|
104
|
+
*/
|
|
105
|
+
export function validateNumber(
|
|
106
|
+
input: unknown,
|
|
107
|
+
options: NumericValidationOptions = {}
|
|
108
|
+
): ValidationResult {
|
|
109
|
+
const errors: string[] = [];
|
|
110
|
+
|
|
111
|
+
// Check if input is a number
|
|
112
|
+
if (typeof input !== "number" || isNaN(input)) {
|
|
113
|
+
return {
|
|
114
|
+
isValid: false,
|
|
115
|
+
errors: ["Input must be a number"],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check if integer
|
|
120
|
+
if (options.integer && !Number.isInteger(input)) {
|
|
121
|
+
errors.push("Input must be an integer");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check min value
|
|
125
|
+
if (options.min !== undefined && input < options.min) {
|
|
126
|
+
errors.push(`Input must be at least ${options.min}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check max value
|
|
130
|
+
if (options.max !== undefined && input > options.max) {
|
|
131
|
+
errors.push(`Input must be at most ${options.max}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
isValid: errors.length === 0,
|
|
136
|
+
errors,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Validates URL format
|
|
142
|
+
*/
|
|
143
|
+
export function validateURL(input: unknown): ValidationResult {
|
|
144
|
+
if (typeof input !== "string") {
|
|
145
|
+
return {
|
|
146
|
+
isValid: false,
|
|
147
|
+
errors: ["URL must be a string"],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const url = new URL(input);
|
|
153
|
+
|
|
154
|
+
// Only allow http and https protocols
|
|
155
|
+
if (!["http:", "https:"].includes(url.protocol)) {
|
|
156
|
+
return {
|
|
157
|
+
isValid: false,
|
|
158
|
+
errors: ["Only HTTP and HTTPS protocols are allowed"],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { isValid: true, errors: [] };
|
|
163
|
+
} catch {
|
|
164
|
+
return {
|
|
165
|
+
isValid: false,
|
|
166
|
+
errors: ["Invalid URL format"],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Validates email format
|
|
173
|
+
*/
|
|
174
|
+
export function validateEmail(input: unknown): ValidationResult {
|
|
175
|
+
if (typeof input !== "string") {
|
|
176
|
+
return {
|
|
177
|
+
isValid: false,
|
|
178
|
+
errors: ["Email must be a string"],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Basic email validation regex
|
|
183
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
184
|
+
|
|
185
|
+
if (!emailRegex.test(input)) {
|
|
186
|
+
return {
|
|
187
|
+
isValid: false,
|
|
188
|
+
errors: ["Invalid email format"],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { isValid: true, errors: [] };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Validates base64 string
|
|
197
|
+
*/
|
|
198
|
+
export function validateBase64(input: unknown): ValidationResult {
|
|
199
|
+
if (typeof input !== "string") {
|
|
200
|
+
return {
|
|
201
|
+
isValid: false,
|
|
202
|
+
errors: ["Input must be a string"],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check if it's a valid base64 string
|
|
207
|
+
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
208
|
+
|
|
209
|
+
if (!base64Regex.test(input)) {
|
|
210
|
+
return {
|
|
211
|
+
isValid: false,
|
|
212
|
+
errors: ["Invalid base64 format"],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check if length is valid (must be multiple of 4)
|
|
217
|
+
if (input.length % 4 !== 0) {
|
|
218
|
+
return {
|
|
219
|
+
isValid: false,
|
|
220
|
+
errors: ["Base64 string length must be a multiple of 4"],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { isValid: true, errors: [] };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Validates object structure
|
|
229
|
+
*/
|
|
230
|
+
export function validateObject(
|
|
231
|
+
input: unknown,
|
|
232
|
+
requiredFields: readonly string[] = []
|
|
233
|
+
): ValidationResult {
|
|
234
|
+
const errors: string[] = [];
|
|
235
|
+
|
|
236
|
+
if (typeof input !== "object" || input === null) {
|
|
237
|
+
return {
|
|
238
|
+
isValid: false,
|
|
239
|
+
errors: ["Input must be an object"],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check required fields
|
|
244
|
+
for (const field of requiredFields) {
|
|
245
|
+
if (!(field in input)) {
|
|
246
|
+
errors.push(`Missing required field: ${field}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
isValid: errors.length === 0,
|
|
252
|
+
errors,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Validates array input
|
|
258
|
+
*/
|
|
259
|
+
export function validateArray(
|
|
260
|
+
input: unknown,
|
|
261
|
+
options: {
|
|
262
|
+
readonly minLength?: number;
|
|
263
|
+
readonly maxLength?: number;
|
|
264
|
+
readonly itemType?: "string" | "number" | "object";
|
|
265
|
+
} = {}
|
|
266
|
+
): ValidationResult {
|
|
267
|
+
const errors: string[] = [];
|
|
268
|
+
|
|
269
|
+
if (!Array.isArray(input)) {
|
|
270
|
+
return {
|
|
271
|
+
isValid: false,
|
|
272
|
+
errors: ["Input must be an array"],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Check min length
|
|
277
|
+
if (options.minLength !== undefined && input.length < options.minLength) {
|
|
278
|
+
errors.push(`Array must have at least ${options.minLength} items`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Check max length
|
|
282
|
+
if (options.maxLength !== undefined && input.length > options.maxLength) {
|
|
283
|
+
errors.push(`Array must have at most ${options.maxLength} items`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check item types
|
|
287
|
+
if (options.itemType) {
|
|
288
|
+
for (let i = 0; i < input.length; i++) {
|
|
289
|
+
const item = input[i];
|
|
290
|
+
const isValidType =
|
|
291
|
+
options.itemType === "string"
|
|
292
|
+
? typeof item === "string"
|
|
293
|
+
: options.itemType === "number"
|
|
294
|
+
? typeof item === "number"
|
|
295
|
+
: typeof item === "object" && item !== null;
|
|
296
|
+
|
|
297
|
+
if (!isValidType) {
|
|
298
|
+
errors.push(`Item at index ${i} is not a ${options.itemType}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
isValid: errors.length === 0,
|
|
305
|
+
errors,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Combines multiple validation results
|
|
311
|
+
*/
|
|
312
|
+
export function combineValidationResults(
|
|
313
|
+
results: readonly ValidationResult[]
|
|
314
|
+
): ValidationResult {
|
|
315
|
+
const allErrors = results.flatMap((r) => r.errors);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
isValid: allErrors.length === 0,
|
|
319
|
+
errors: allErrors,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Sanitizes and validates user input in one step
|
|
325
|
+
*/
|
|
326
|
+
export function sanitizeAndValidate(
|
|
327
|
+
input: unknown,
|
|
328
|
+
options: StringValidationOptions = {}
|
|
329
|
+
): { readonly sanitized: string; readonly validation: ValidationResult } {
|
|
330
|
+
const sanitized = sanitizeString(input);
|
|
331
|
+
const validation = validateString(sanitized, options);
|
|
332
|
+
|
|
333
|
+
return { sanitized, validation };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Validates prompt/input text for AI generation
|
|
338
|
+
*/
|
|
339
|
+
export function validateAIPrompt(input: unknown): ValidationResult {
|
|
340
|
+
const options: StringValidationOptions = {
|
|
341
|
+
minLength: MIN_PROMPT_LENGTH,
|
|
342
|
+
maxLength: MAX_PROMPT_LENGTH,
|
|
343
|
+
trim: true,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
return validateString(input, options);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Validates image data (base64 or URL)
|
|
351
|
+
*/
|
|
352
|
+
export function validateImageData(input: unknown): ValidationResult {
|
|
353
|
+
if (typeof input !== "string") {
|
|
354
|
+
return {
|
|
355
|
+
isValid: false,
|
|
356
|
+
errors: ["Image data must be a string"],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Check if it's a URL
|
|
361
|
+
if (input.startsWith("http://") || input.startsWith("https://")) {
|
|
362
|
+
return validateURL(input);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Check if it's base64
|
|
366
|
+
if (input.startsWith("data:image/")) {
|
|
367
|
+
const base64Part = input.split(",")[1];
|
|
368
|
+
if (!base64Part) {
|
|
369
|
+
return {
|
|
370
|
+
isValid: false,
|
|
371
|
+
errors: ["Invalid data URI format"],
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
return validateBase64(base64Part);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
isValid: false,
|
|
379
|
+
errors: ["Image data must be a URL or base64 data URI"],
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Validates user ID
|
|
385
|
+
*/
|
|
386
|
+
export function validateUserId(input: unknown): ValidationResult {
|
|
387
|
+
const options: StringValidationOptions = {
|
|
388
|
+
minLength: 1,
|
|
389
|
+
maxLength: MAX_USER_ID_LENGTH,
|
|
390
|
+
pattern: /^[a-zA-Z0-9_-]+$/,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
return validateString(input, options);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Validates creation ID
|
|
398
|
+
*/
|
|
399
|
+
export function validateCreationId(input: unknown): ValidationResult {
|
|
400
|
+
const options: StringValidationOptions = {
|
|
401
|
+
minLength: 1,
|
|
402
|
+
maxLength: MAX_CREATION_ID_LENGTH,
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
return validateString(input, options);
|
|
406
|
+
}
|
|
@@ -43,22 +43,11 @@ export const AIGenerationForm: React.FC<AIGenerationFormProps> = ({
|
|
|
43
43
|
translations,
|
|
44
44
|
children,
|
|
45
45
|
}) => {
|
|
46
|
-
if (__DEV__) {
|
|
47
|
-
console.log("[AIGenerationForm] RENDERING NOW - hideGenerateButton:", hideGenerateButton);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
46
|
const tokens = useAppDesignTokens();
|
|
51
47
|
const isAdvancedVisible = showAdvanced !== undefined ? showAdvanced : true;
|
|
52
|
-
// Button is disabled if: external isDisabled is true, OR prompt validation fails (when prompt is used)
|
|
53
48
|
const promptInvalid = onPromptChange ? !prompt?.trim() : false;
|
|
54
49
|
const buttonIsDisabled = isDisabled || promptInvalid;
|
|
55
50
|
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
if (__DEV__) {
|
|
58
|
-
console.log("[AIGenerationForm] MOUNTED/UPDATED - prompt:", prompt, "isGenerating:", isGenerating, "buttonIsDisabled:", buttonIsDisabled, "hideGenerateButton:", hideGenerateButton);
|
|
59
|
-
}
|
|
60
|
-
}, [prompt, isGenerating, buttonIsDisabled, hideGenerateButton]);
|
|
61
|
-
|
|
62
51
|
return (
|
|
63
52
|
<>
|
|
64
53
|
{presets && presets.length > 0 && onPresetPress && (
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Boundary Component
|
|
3
|
+
* Catches JavaScript errors anywhere in the child component tree
|
|
4
|
+
* Displays a fallback UI instead of crashing the entire app
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { Component, ErrorInfo, ReactNode } from "react";
|
|
8
|
+
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
|
|
9
|
+
|
|
10
|
+
interface ErrorBoundaryProps {
|
|
11
|
+
readonly children: ReactNode;
|
|
12
|
+
readonly fallback?: ReactNode;
|
|
13
|
+
readonly onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ErrorBoundaryState {
|
|
17
|
+
readonly hasError: boolean;
|
|
18
|
+
readonly error?: Error;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class ErrorBoundary extends Component<
|
|
22
|
+
ErrorBoundaryProps,
|
|
23
|
+
ErrorBoundaryState
|
|
24
|
+
> {
|
|
25
|
+
constructor(props: ErrorBoundaryProps) {
|
|
26
|
+
super(props);
|
|
27
|
+
this.state = { hasError: false };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
31
|
+
return { hasError: true, error };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
|
35
|
+
// Log error to error reporting service
|
|
36
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
37
|
+
console.error("[ErrorBoundary] Caught error:", error, errorInfo);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.props.onError?.(error, errorInfo);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
handleReset = (): void => {
|
|
44
|
+
this.setState({ hasError: false, error: undefined });
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
render(): ReactNode {
|
|
48
|
+
if (this.state.hasError) {
|
|
49
|
+
// Use custom fallback if provided
|
|
50
|
+
if (this.props.fallback) {
|
|
51
|
+
return this.props.fallback;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Default error UI
|
|
55
|
+
return (
|
|
56
|
+
<View style={styles.container}>
|
|
57
|
+
<View style={styles.content}>
|
|
58
|
+
<Text style={styles.title}>Something went wrong</Text>
|
|
59
|
+
<Text style={styles.message}>
|
|
60
|
+
An unexpected error occurred. Please try again.
|
|
61
|
+
</Text>
|
|
62
|
+
{typeof __DEV__ !== "undefined" && __DEV__ && this.state.error && (
|
|
63
|
+
<Text style={styles.errorText}>
|
|
64
|
+
{this.state.error.message}
|
|
65
|
+
</Text>
|
|
66
|
+
)}
|
|
67
|
+
<TouchableOpacity
|
|
68
|
+
style={styles.button}
|
|
69
|
+
onPress={this.handleReset}
|
|
70
|
+
>
|
|
71
|
+
<Text style={styles.buttonText}>Try Again</Text>
|
|
72
|
+
</TouchableOpacity>
|
|
73
|
+
</View>
|
|
74
|
+
</View>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return this.props.children;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const styles = StyleSheet.create({
|
|
83
|
+
container: {
|
|
84
|
+
flex: 1,
|
|
85
|
+
backgroundColor: "#fff",
|
|
86
|
+
justifyContent: "center",
|
|
87
|
+
alignItems: "center",
|
|
88
|
+
padding: 20,
|
|
89
|
+
},
|
|
90
|
+
content: {
|
|
91
|
+
alignItems: "center",
|
|
92
|
+
maxWidth: 400,
|
|
93
|
+
},
|
|
94
|
+
title: {
|
|
95
|
+
fontSize: 24,
|
|
96
|
+
fontWeight: "bold",
|
|
97
|
+
marginBottom: 12,
|
|
98
|
+
color: "#d32f2f",
|
|
99
|
+
},
|
|
100
|
+
message: {
|
|
101
|
+
fontSize: 16,
|
|
102
|
+
textAlign: "center",
|
|
103
|
+
marginBottom: 20,
|
|
104
|
+
color: "#666",
|
|
105
|
+
lineHeight: 24,
|
|
106
|
+
},
|
|
107
|
+
errorText: {
|
|
108
|
+
fontSize: 12,
|
|
109
|
+
textAlign: "center",
|
|
110
|
+
marginBottom: 20,
|
|
111
|
+
color: "#999",
|
|
112
|
+
fontFamily: "monospace",
|
|
113
|
+
},
|
|
114
|
+
button: {
|
|
115
|
+
backgroundColor: "#2196F3",
|
|
116
|
+
paddingHorizontal: 24,
|
|
117
|
+
paddingVertical: 12,
|
|
118
|
+
borderRadius: 8,
|
|
119
|
+
minWidth: 120,
|
|
120
|
+
alignItems: "center",
|
|
121
|
+
},
|
|
122
|
+
buttonText: {
|
|
123
|
+
color: "#fff",
|
|
124
|
+
fontSize: 16,
|
|
125
|
+
fontWeight: "600",
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Higher-order component version of ErrorBoundary
|
|
131
|
+
*/
|
|
132
|
+
export function withErrorBoundary<P extends object>(
|
|
133
|
+
WrappedComponent: React.ComponentType<P>,
|
|
134
|
+
fallback?: ReactNode,
|
|
135
|
+
): React.ComponentType<P> {
|
|
136
|
+
return (props: P) => (
|
|
137
|
+
<ErrorBoundary fallback={fallback}>
|
|
138
|
+
<WrappedComponent {...props} />
|
|
139
|
+
</ErrorBoundary>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -5,6 +5,7 @@ export { PendingJobProgressBar } from "./PendingJobProgressBar";
|
|
|
5
5
|
export { PendingJobCardActions } from "./PendingJobCardActions";
|
|
6
6
|
export { PromptInput } from "./PromptInput";
|
|
7
7
|
export { AIGenerationHero } from "./AIGenerationHero";
|
|
8
|
+
export { ErrorBoundary, withErrorBoundary } from "./ErrorBoundary";
|
|
8
9
|
export * from "./StylePresetsGrid";
|
|
9
10
|
export * from "./AIGenerationForm";
|
|
10
11
|
export * from "./AIGenerationForm.types";
|
|
@@ -35,7 +35,6 @@ export interface UseBackgroundGenerationReturn<TInput, TResult> {
|
|
|
35
35
|
input: TInput,
|
|
36
36
|
) => Promise<DirectExecutionResult<TResult>>;
|
|
37
37
|
readonly cancelJob: (id: string) => void;
|
|
38
|
-
readonly retryJob: (id: string) => void;
|
|
39
38
|
readonly pendingJobs: BackgroundJob<TInput, TResult>[];
|
|
40
39
|
readonly activeJobCount: number;
|
|
41
40
|
readonly hasActiveJobs: boolean;
|
|
@@ -173,21 +172,10 @@ export function useBackgroundGeneration<TInput = unknown, TResult = unknown>(
|
|
|
173
172
|
[removeJob],
|
|
174
173
|
);
|
|
175
174
|
|
|
176
|
-
const retryJob = useCallback(
|
|
177
|
-
(id: string) => {
|
|
178
|
-
const jobData = jobInputsRef.current.get(id);
|
|
179
|
-
if (!jobData) return;
|
|
180
|
-
removeJob(id);
|
|
181
|
-
void startJob(jobData.input, jobData.type);
|
|
182
|
-
},
|
|
183
|
-
[removeJob, startJob],
|
|
184
|
-
);
|
|
185
|
-
|
|
186
175
|
return {
|
|
187
176
|
startJob,
|
|
188
177
|
executeDirectly,
|
|
189
178
|
cancelJob,
|
|
190
|
-
retryJob,
|
|
191
179
|
pendingJobs: jobs,
|
|
192
180
|
activeJobCount: activeJobsRef.current.size,
|
|
193
181
|
hasActiveJobs: activeJobsRef.current.size > 0,
|
|
@@ -53,8 +53,7 @@ export interface AIFeatureCallbacks<TRequest = unknown, TResult = unknown> {
|
|
|
53
53
|
calculateCost: (multiplier?: number, _model?: string | null) => number;
|
|
54
54
|
canAfford: (cost: number) => boolean;
|
|
55
55
|
isAuthenticated: () => boolean;
|
|
56
|
-
|
|
57
|
-
onAuthRequired: (retryCallback?: () => void) => void;
|
|
56
|
+
onAuthRequired: () => void;
|
|
58
57
|
onCreditsRequired: (cost?: number) => void;
|
|
59
58
|
onSuccess?: (result: TResult) => void;
|
|
60
59
|
onError?: (error: string) => void;
|
|
@@ -98,8 +97,8 @@ export function useAIFeatureCallbacks<TRequest = unknown, TResult = unknown>(
|
|
|
98
97
|
[creditCostPerUnit],
|
|
99
98
|
);
|
|
100
99
|
|
|
101
|
-
const onAuthRequired = useCallback((
|
|
102
|
-
showAuthModal(
|
|
100
|
+
const onAuthRequired = useCallback(() => {
|
|
101
|
+
showAuthModal();
|
|
103
102
|
}, [showAuthModal]);
|
|
104
103
|
|
|
105
104
|
const onCreditsRequired = useCallback(
|
|
@@ -78,7 +78,6 @@ export interface ResultActionButton {
|
|
|
78
78
|
export interface ResultActionsConfig {
|
|
79
79
|
share?: ResultActionButton;
|
|
80
80
|
save?: ResultActionButton;
|
|
81
|
-
retry?: ResultActionButton;
|
|
82
81
|
layout?: "horizontal" | "vertical" | "grid";
|
|
83
82
|
buttonSpacing?: number;
|
|
84
83
|
spacing?: {
|
|
@@ -169,12 +168,6 @@ export const DEFAULT_RESULT_CONFIG: ResultConfig = {
|
|
|
169
168
|
variant: "secondary",
|
|
170
169
|
position: "bottom",
|
|
171
170
|
},
|
|
172
|
-
retry: {
|
|
173
|
-
enabled: true,
|
|
174
|
-
icon: "refresh",
|
|
175
|
-
variant: "text",
|
|
176
|
-
position: "top",
|
|
177
|
-
},
|
|
178
171
|
layout: "horizontal",
|
|
179
172
|
buttonSpacing: 10,
|
|
180
173
|
spacing: {
|