@umituz/react-native-ai-fal-provider 2.1.8 → 2.1.10
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/exports/infrastructure.ts +29 -1
- package/src/infrastructure/utils/constants/image-fields.constants.ts +59 -0
- package/src/infrastructure/utils/cost-tracker.ts +2 -1
- package/src/infrastructure/utils/cost-tracking-executor.util.ts +3 -2
- package/src/infrastructure/utils/fal-error-handler.util.ts +2 -1
- package/src/infrastructure/utils/fal-storage.util.ts +3 -2
- package/src/infrastructure/utils/helpers/error-helpers.util.ts +86 -0
- package/src/infrastructure/utils/image-helpers.util.ts +0 -7
- package/src/infrastructure/utils/index.ts +1 -7
- package/src/infrastructure/utils/input-preprocessor.util.ts +8 -21
- package/src/infrastructure/utils/input-validator.util.ts +7 -12
- package/src/infrastructure/utils/parsers/json-parsers.util.ts +5 -3
- package/src/infrastructure/utils/parsers/object-transformers.util.ts +3 -1
- package/src/infrastructure/utils/string-format.util.ts +61 -3
- package/src/infrastructure/utils/type-guards/validation-guards.util.ts +5 -2
- package/src/infrastructure/utils/validators/data-uri-validator.util.ts +91 -0
- package/src/infrastructure/utils/validators/string-validator.util.ts +64 -0
- package/src/presentation/hooks/use-models.ts +8 -16
- package/src/infrastructure/utils/prompt-helpers.util.ts +0 -27
package/package.json
CHANGED
|
@@ -21,6 +21,35 @@ export {
|
|
|
21
21
|
buildDualImageInput,
|
|
22
22
|
} from "../infrastructure/utils";
|
|
23
23
|
|
|
24
|
+
// Error handling utilities
|
|
25
|
+
export {
|
|
26
|
+
getErrorMessage,
|
|
27
|
+
getErrorMessageOr,
|
|
28
|
+
formatErrorMessage,
|
|
29
|
+
} from "../infrastructure/utils/helpers/error-helpers.util";
|
|
30
|
+
|
|
31
|
+
// Image field constants
|
|
32
|
+
export {
|
|
33
|
+
IMAGE_URL_FIELDS,
|
|
34
|
+
isImageField,
|
|
35
|
+
} from "../infrastructure/utils/constants/image-fields.constants";
|
|
36
|
+
export type {
|
|
37
|
+
ImageUrlField,
|
|
38
|
+
} from "../infrastructure/utils/constants/image-fields.constants";
|
|
39
|
+
|
|
40
|
+
// Validators
|
|
41
|
+
export {
|
|
42
|
+
isDataUri,
|
|
43
|
+
isBase64DataUri,
|
|
44
|
+
extractMimeType,
|
|
45
|
+
extractBase64Content,
|
|
46
|
+
} from "../infrastructure/utils/validators/data-uri-validator.util";
|
|
47
|
+
export {
|
|
48
|
+
isEmptyString,
|
|
49
|
+
isNonEmptyString,
|
|
50
|
+
isString,
|
|
51
|
+
} from "../infrastructure/utils/validators/string-validator.util";
|
|
52
|
+
|
|
24
53
|
export { CostTracker } from "../infrastructure/utils/cost-tracker";
|
|
25
54
|
|
|
26
55
|
export {
|
|
@@ -39,7 +68,6 @@ export {
|
|
|
39
68
|
formatImageDataUri,
|
|
40
69
|
extractBase64,
|
|
41
70
|
getDataUriExtension,
|
|
42
|
-
isImageDataUri,
|
|
43
71
|
uploadToFalStorage,
|
|
44
72
|
uploadMultipleToFalStorage,
|
|
45
73
|
formatNumber,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Field Constants
|
|
3
|
+
* Single source of truth for all image-related field names
|
|
4
|
+
*
|
|
5
|
+
* CRITICAL: This file consolidates image field names that were previously
|
|
6
|
+
* duplicated and inconsistent across the codebase:
|
|
7
|
+
* - input-validator.util.ts had 5 fields
|
|
8
|
+
* - input-preprocessor.util.ts had 9 fields
|
|
9
|
+
*
|
|
10
|
+
* All image field definitions must be maintained here to prevent future inconsistencies.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* All supported image URL field names across the FAL AI API
|
|
15
|
+
* Used for preprocessing (base64 to URL conversion) and validation
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* for (const field of IMAGE_URL_FIELDS) {
|
|
20
|
+
* if (field in input && isBase64(input[field])) {
|
|
21
|
+
* input[field] = await uploadToStorage(input[field]);
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const IMAGE_URL_FIELDS = [
|
|
27
|
+
"image_url",
|
|
28
|
+
"second_image_url",
|
|
29
|
+
"third_image_url",
|
|
30
|
+
"fourth_image_url",
|
|
31
|
+
"base_image_url",
|
|
32
|
+
"swap_image_url",
|
|
33
|
+
"driver_image_url",
|
|
34
|
+
"mask_url",
|
|
35
|
+
"input_image_url",
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Type-safe image URL field names
|
|
40
|
+
*/
|
|
41
|
+
export type ImageUrlField = typeof IMAGE_URL_FIELDS[number];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if a field name is a known image field
|
|
45
|
+
*
|
|
46
|
+
* @param fieldName - The field name to check
|
|
47
|
+
* @returns Type guard indicating if the field is an ImageUrlField
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* if (isImageField(fieldName)) {
|
|
52
|
+
* // TypeScript knows fieldName is ImageUrlField here
|
|
53
|
+
* console.log(`Processing image field: ${fieldName}`);
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function isImageField(fieldName: string): fieldName is ImageUrlField {
|
|
58
|
+
return IMAGE_URL_FIELDS.includes(fieldName as ImageUrlField);
|
|
59
|
+
}
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
} from "../../domain/entities/cost-tracking.types";
|
|
11
11
|
import { findModelById } from "../../domain/constants/default-models.constants";
|
|
12
12
|
import { filterByProperty, filterByTimeRange } from "./collections";
|
|
13
|
+
import { getErrorMessage } from './helpers/error-helpers.util';
|
|
13
14
|
|
|
14
15
|
export type { GenerationCost } from "../../domain/entities/cost-tracking.types";
|
|
15
16
|
|
|
@@ -61,7 +62,7 @@ export class CostTracker {
|
|
|
61
62
|
// Log error but continue with default cost info
|
|
62
63
|
console.warn(
|
|
63
64
|
`[cost-tracker] Failed to get model cost info for ${modelId}:`,
|
|
64
|
-
|
|
65
|
+
getErrorMessage(error)
|
|
65
66
|
);
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { CostTracker } from "./cost-tracker";
|
|
7
|
+
import { getErrorMessage } from './helpers/error-helpers.util';
|
|
7
8
|
|
|
8
9
|
interface ExecuteWithCostTrackingOptions<T> {
|
|
9
10
|
tracker: CostTracker | null;
|
|
@@ -39,7 +40,7 @@ export async function executeWithCostTracking<T>(
|
|
|
39
40
|
// Log for debugging and audit trail
|
|
40
41
|
console.error(
|
|
41
42
|
`[cost-tracking] Failed to complete cost tracking for ${operation} on ${model}:`,
|
|
42
|
-
|
|
43
|
+
getErrorMessage(costError),
|
|
43
44
|
{ operationId, model, operation }
|
|
44
45
|
);
|
|
45
46
|
}
|
|
@@ -53,7 +54,7 @@ export async function executeWithCostTracking<T>(
|
|
|
53
54
|
// Log for debugging and audit trail
|
|
54
55
|
console.error(
|
|
55
56
|
`[cost-tracking] Failed to mark operation as failed for ${operation} on ${model}:`,
|
|
56
|
-
|
|
57
|
+
getErrorMessage(failError),
|
|
57
58
|
{ operationId, model, operation }
|
|
58
59
|
);
|
|
59
60
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { FalErrorInfo, FalErrorCategory, FalErrorType } from "../../domain/entities/error.types";
|
|
7
7
|
import { FalErrorType as ErrorTypeEnum } from "../../domain/entities/error.types";
|
|
8
8
|
import { safeJsonParseOrNull } from "./parsers";
|
|
9
|
+
import { isNonEmptyString } from './validators/string-validator.util';
|
|
9
10
|
|
|
10
11
|
const STATUS_CODES = ["400", "401", "402", "403", "404", "422", "429", "500", "502", "503", "504"];
|
|
11
12
|
|
|
@@ -130,7 +131,7 @@ export function mapFalError(error: unknown): FalErrorInfo {
|
|
|
130
131
|
*/
|
|
131
132
|
export function parseFalError(error: unknown): string {
|
|
132
133
|
const userMessage = parseFalApiError(error);
|
|
133
|
-
if (!userMessage
|
|
134
|
+
if (!isNonEmptyString(userMessage)) {
|
|
134
135
|
return "An unknown error occurred. Please try again.";
|
|
135
136
|
}
|
|
136
137
|
return userMessage;
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
base64ToTempFile,
|
|
9
9
|
deleteTempFile,
|
|
10
10
|
} from "@umituz/react-native-design-system/filesystem";
|
|
11
|
+
import { getErrorMessage } from './helpers/error-helpers.util';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Upload base64 image to FAL storage
|
|
@@ -34,7 +35,7 @@ export async function uploadToFalStorage(base64: string): Promise<string> {
|
|
|
34
35
|
// Log cleanup errors to prevent disk space leaks
|
|
35
36
|
console.warn(
|
|
36
37
|
`[fal-storage] Failed to delete temp file: ${tempUri}`,
|
|
37
|
-
|
|
38
|
+
getErrorMessage(cleanupError)
|
|
38
39
|
);
|
|
39
40
|
// Don't throw - cleanup errors shouldn't fail the upload
|
|
40
41
|
}
|
|
@@ -68,7 +69,7 @@ export async function uploadMultipleToFalStorage(
|
|
|
68
69
|
if (failures.length > 0) {
|
|
69
70
|
const errorMessage = failures
|
|
70
71
|
.map(({ index, error }) =>
|
|
71
|
-
`Image ${index}: ${
|
|
72
|
+
`Image ${index}: ${getErrorMessage(error)}`
|
|
72
73
|
)
|
|
73
74
|
.join('; ');
|
|
74
75
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Helper Utilities
|
|
3
|
+
* Centralized error message extraction and formatting
|
|
4
|
+
*
|
|
5
|
+
* This utility eliminates the repeated pattern:
|
|
6
|
+
* `error instanceof Error ? error.message : String(error)`
|
|
7
|
+
* which appeared 8 times across 6 files in the codebase.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extract error message from any error type
|
|
12
|
+
* Handles Error instances, strings, and unknown types
|
|
13
|
+
*
|
|
14
|
+
* @param error - The error to extract message from
|
|
15
|
+
* @returns The error message as a string
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* try {
|
|
20
|
+
* await riskyOperation();
|
|
21
|
+
* } catch (error) {
|
|
22
|
+
* console.error('Operation failed:', getErrorMessage(error));
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function getErrorMessage(error: unknown): string {
|
|
27
|
+
return error instanceof Error ? error.message : String(error);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extract error message with fallback
|
|
32
|
+
* Returns fallback if error message is empty or whitespace-only
|
|
33
|
+
*
|
|
34
|
+
* @param error - The error to extract message from
|
|
35
|
+
* @param fallback - Fallback message if error message is empty
|
|
36
|
+
* @returns The error message or fallback
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* const message = getErrorMessageOr(error, 'An unknown error occurred');
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function getErrorMessageOr(error: unknown, fallback: string): string {
|
|
44
|
+
const message = getErrorMessage(error);
|
|
45
|
+
return message && message.trim().length > 0 ? message : fallback;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create error message with context prefix
|
|
50
|
+
* Useful for adding operation context to error messages
|
|
51
|
+
*
|
|
52
|
+
* @param error - The error to extract message from
|
|
53
|
+
* @param context - Context to prepend to the error message
|
|
54
|
+
* @returns Formatted error message with context
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* throw new Error(formatErrorMessage(error, 'Failed to upload image'));
|
|
59
|
+
* // Output: "Failed to upload image: Network timeout"
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function formatErrorMessage(error: unknown, context: string): string {
|
|
63
|
+
return `${context}: ${getErrorMessage(error)}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extract error name (for Error instances)
|
|
68
|
+
* Returns undefined for non-Error types
|
|
69
|
+
*
|
|
70
|
+
* @param error - The error to extract name from
|
|
71
|
+
* @returns The error name or undefined
|
|
72
|
+
*/
|
|
73
|
+
export function getErrorName(error: unknown): string | undefined {
|
|
74
|
+
return error instanceof Error ? error.name : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Extract error stack trace
|
|
79
|
+
* Returns undefined for non-Error types
|
|
80
|
+
*
|
|
81
|
+
* @param error - The error to extract stack from
|
|
82
|
+
* @returns The error stack trace or undefined
|
|
83
|
+
*/
|
|
84
|
+
export function getErrorStack(error: unknown): string | undefined {
|
|
85
|
+
return error instanceof Error ? error.stack : undefined;
|
|
86
|
+
}
|
|
@@ -45,10 +45,3 @@ export function getDataUriExtension(dataUri: string): string | null {
|
|
|
45
45
|
const match = dataUri.match(/^data:image\/(\w+);base64/);
|
|
46
46
|
return match ? match[1] : null;
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Check if data URI is an image
|
|
51
|
-
*/
|
|
52
|
-
export function isImageDataUri(value: string): boolean {
|
|
53
|
-
return value.startsWith("data:image/");
|
|
54
|
-
}
|
|
@@ -31,7 +31,7 @@ export {
|
|
|
31
31
|
|
|
32
32
|
export { formatDate } from "./date-format.util";
|
|
33
33
|
export { formatNumber, formatBytes, formatDuration } from "./number-format.util";
|
|
34
|
-
export { truncateText } from "./string-format.util";
|
|
34
|
+
export { truncateText, truncatePrompt, sanitizePrompt } from "./string-format.util";
|
|
35
35
|
|
|
36
36
|
export {
|
|
37
37
|
buildSingleImageInput,
|
|
@@ -54,7 +54,6 @@ export {
|
|
|
54
54
|
formatImageDataUri,
|
|
55
55
|
extractBase64,
|
|
56
56
|
getDataUriExtension,
|
|
57
|
-
isImageDataUri,
|
|
58
57
|
} from "./image-helpers.util";
|
|
59
58
|
|
|
60
59
|
export {
|
|
@@ -62,11 +61,6 @@ export {
|
|
|
62
61
|
uploadMultipleToFalStorage,
|
|
63
62
|
} from "./fal-storage.util";
|
|
64
63
|
|
|
65
|
-
export {
|
|
66
|
-
truncatePrompt,
|
|
67
|
-
sanitizePrompt,
|
|
68
|
-
} from "./prompt-helpers.util";
|
|
69
|
-
|
|
70
64
|
export {
|
|
71
65
|
buildErrorMessage,
|
|
72
66
|
isDefined,
|
|
@@ -4,22 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { uploadToFalStorage } from "./fal-storage.util";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"second_image_url",
|
|
11
|
-
"third_image_url",
|
|
12
|
-
"fourth_image_url",
|
|
13
|
-
"driver_image_url",
|
|
14
|
-
"base_image_url",
|
|
15
|
-
"swap_image_url",
|
|
16
|
-
"mask_url",
|
|
17
|
-
"input_image_url",
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
function isBase64DataUri(value: unknown): value is string {
|
|
21
|
-
return typeof value === "string" && value.startsWith("data:image/");
|
|
22
|
-
}
|
|
7
|
+
import { getErrorMessage } from './helpers/error-helpers.util';
|
|
8
|
+
import { IMAGE_URL_FIELDS } from './constants/image-fields.constants';
|
|
9
|
+
import { isImageDataUri as isBase64DataUri } from './validators/data-uri-validator.util';
|
|
23
10
|
|
|
24
11
|
/**
|
|
25
12
|
* Preprocess input by uploading base64 images to FAL storage
|
|
@@ -32,7 +19,7 @@ export async function preprocessInput(
|
|
|
32
19
|
const uploadPromises: Promise<unknown>[] = [];
|
|
33
20
|
|
|
34
21
|
// Handle individual image URL keys
|
|
35
|
-
for (const key of
|
|
22
|
+
for (const key of IMAGE_URL_FIELDS) {
|
|
36
23
|
const value = result[key];
|
|
37
24
|
if (isBase64DataUri(value)) {
|
|
38
25
|
const uploadPromise = uploadToFalStorage(value)
|
|
@@ -41,7 +28,7 @@ export async function preprocessInput(
|
|
|
41
28
|
return url;
|
|
42
29
|
})
|
|
43
30
|
.catch((error) => {
|
|
44
|
-
const errorMessage = `Failed to upload ${key}: ${error
|
|
31
|
+
const errorMessage = `Failed to upload ${key}: ${getErrorMessage(error)}`;
|
|
45
32
|
console.error(`[preprocessInput] ${errorMessage}`);
|
|
46
33
|
throw new Error(errorMessage);
|
|
47
34
|
});
|
|
@@ -68,7 +55,7 @@ export async function preprocessInput(
|
|
|
68
55
|
const uploadPromise = uploadToFalStorage(imageUrl)
|
|
69
56
|
.then((url) => url)
|
|
70
57
|
.catch((error) => {
|
|
71
|
-
const errorMessage = `Failed to upload image_urls[${i}]: ${error
|
|
58
|
+
const errorMessage = `Failed to upload image_urls[${i}]: ${getErrorMessage(error)}`;
|
|
72
59
|
console.error(`[preprocessInput] ${errorMessage}`);
|
|
73
60
|
errors.push(errorMessage);
|
|
74
61
|
throw new Error(errorMessage);
|
|
@@ -104,7 +91,7 @@ export async function preprocessInput(
|
|
|
104
91
|
processedUrls.push(result.value);
|
|
105
92
|
} else {
|
|
106
93
|
uploadErrors.push(
|
|
107
|
-
`Upload ${index} failed: ${
|
|
94
|
+
`Upload ${index} failed: ${getErrorMessage(result.reason)}`
|
|
108
95
|
);
|
|
109
96
|
}
|
|
110
97
|
});
|
|
@@ -139,7 +126,7 @@ export async function preprocessInput(
|
|
|
139
126
|
|
|
140
127
|
const errorMessages = failedUploads.map((result) =>
|
|
141
128
|
result.status === 'rejected'
|
|
142
|
-
? (
|
|
129
|
+
? (getErrorMessage(result.reason))
|
|
143
130
|
: 'Unknown error'
|
|
144
131
|
);
|
|
145
132
|
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { isValidModelId, isValidPrompt } from "./type-guards";
|
|
7
|
+
import { IMAGE_URL_FIELDS } from './constants/image-fields.constants';
|
|
8
|
+
import { isImageDataUri } from './validators/data-uri-validator.util';
|
|
9
|
+
import { isNonEmptyString } from './validators/string-validator.util';
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* Detect potentially malicious content in strings
|
|
@@ -49,7 +52,7 @@ function isValidAndSafeUrl(value: string): boolean {
|
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
// Allow base64 image data URIs only
|
|
52
|
-
if (value
|
|
55
|
+
if (isImageDataUri(value)) {
|
|
53
56
|
// Check for suspicious content in data URI
|
|
54
57
|
const dataContent = value.substring(0, 200); // Check first 200 chars
|
|
55
58
|
if (hasSuspiciousContent(dataContent)) {
|
|
@@ -134,16 +137,8 @@ export function validateInput(
|
|
|
134
137
|
}
|
|
135
138
|
}
|
|
136
139
|
|
|
137
|
-
// Validate image_url fields
|
|
138
|
-
const
|
|
139
|
-
"image_url",
|
|
140
|
-
"second_image_url",
|
|
141
|
-
"base_image_url",
|
|
142
|
-
"swap_image_url",
|
|
143
|
-
"mask_url",
|
|
144
|
-
];
|
|
145
|
-
|
|
146
|
-
for (const field of imageFields) {
|
|
140
|
+
// Validate all image_url fields
|
|
141
|
+
for (const field of IMAGE_URL_FIELDS) {
|
|
147
142
|
const value = input[field];
|
|
148
143
|
if (value !== undefined) {
|
|
149
144
|
if (typeof value !== "string") {
|
|
@@ -151,7 +146,7 @@ export function validateInput(
|
|
|
151
146
|
field,
|
|
152
147
|
message: `${field} must be a string`,
|
|
153
148
|
});
|
|
154
|
-
} else if (!value
|
|
149
|
+
} else if (!isNonEmptyString(value)) {
|
|
155
150
|
// Explicitly check for empty/whitespace-only strings
|
|
156
151
|
errors.push({
|
|
157
152
|
field,
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Safe JSON parsing and validation operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { getErrorMessage } from '../helpers/error-helpers.util';
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Safely parse JSON with fallback
|
|
8
10
|
*/
|
|
@@ -15,7 +17,7 @@ export function safeJsonParse<T = unknown>(
|
|
|
15
17
|
} catch (error) {
|
|
16
18
|
console.warn(
|
|
17
19
|
'[json-parsers] Failed to parse JSON, using fallback:',
|
|
18
|
-
|
|
20
|
+
getErrorMessage(error),
|
|
19
21
|
{ dataPreview: data.substring(0, 100) }
|
|
20
22
|
);
|
|
21
23
|
return fallback;
|
|
@@ -31,7 +33,7 @@ export function safeJsonParseOrNull<T = unknown>(data: string): T | null {
|
|
|
31
33
|
} catch (error) {
|
|
32
34
|
console.warn(
|
|
33
35
|
'[json-parsers] Failed to parse JSON, returning null:',
|
|
34
|
-
|
|
36
|
+
getErrorMessage(error),
|
|
35
37
|
{ dataPreview: data.substring(0, 100) }
|
|
36
38
|
);
|
|
37
39
|
return null;
|
|
@@ -50,7 +52,7 @@ export function safeJsonStringify(
|
|
|
50
52
|
} catch (error) {
|
|
51
53
|
console.warn(
|
|
52
54
|
'[json-parsers] Failed to stringify object, using fallback:',
|
|
53
|
-
|
|
55
|
+
getErrorMessage(error),
|
|
54
56
|
{ dataType: typeof data }
|
|
55
57
|
);
|
|
56
58
|
return fallback;
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Clone, merge, pick, and omit operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { getErrorMessage } from '../helpers/error-helpers.util';
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Deep clone object using JSON serialization
|
|
8
10
|
* NOTE: This has limitations:
|
|
@@ -20,7 +22,7 @@ export function deepClone<T>(data: T): T {
|
|
|
20
22
|
// Fallback for circular references or other JSON errors
|
|
21
23
|
console.warn(
|
|
22
24
|
'[object-transformers] deepClone failed, returning original:',
|
|
23
|
-
|
|
25
|
+
getErrorMessage(error)
|
|
24
26
|
);
|
|
25
27
|
// Return original data if cloning fails
|
|
26
28
|
return data;
|
|
@@ -1,14 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* String Formatting Utilities
|
|
3
3
|
* Functions for formatting and manipulating strings
|
|
4
|
+
*
|
|
5
|
+
* Consolidates text truncation and prompt sanitization utilities.
|
|
6
|
+
* Previously duplicated across string-format.util.ts and prompt-helpers.util.ts.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
|
-
* Truncate text with ellipsis
|
|
10
|
+
* Truncate text with customizable ellipsis
|
|
11
|
+
*
|
|
12
|
+
* @param text - The text to truncate
|
|
13
|
+
* @param maxLength - Maximum length including ellipsis
|
|
14
|
+
* @param ellipsis - String to append when truncating (default: "...")
|
|
15
|
+
* @returns Truncated text with ellipsis or original if under max length
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* truncateText("Hello World", 8); // "Hello..."
|
|
20
|
+
* truncateText("Hello World", 8, "…"); // "Hello W…"
|
|
21
|
+
* ```
|
|
8
22
|
*/
|
|
9
|
-
export function truncateText(
|
|
23
|
+
export function truncateText(
|
|
24
|
+
text: string,
|
|
25
|
+
maxLength: number,
|
|
26
|
+
ellipsis: string = "..."
|
|
27
|
+
): string {
|
|
10
28
|
if (text.length <= maxLength) {
|
|
11
29
|
return text;
|
|
12
30
|
}
|
|
13
|
-
return text.slice(0, maxLength -
|
|
31
|
+
return text.slice(0, maxLength - ellipsis.length) + ellipsis;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Truncate prompt to maximum length
|
|
36
|
+
* Specialized wrapper for prompt truncation with 5000 char default
|
|
37
|
+
*
|
|
38
|
+
* @param prompt - The prompt text to truncate
|
|
39
|
+
* @param maxLength - Maximum length (default: 5000)
|
|
40
|
+
* @returns Truncated prompt
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* truncatePrompt(longPrompt); // Truncates to 5000 chars
|
|
45
|
+
* truncatePrompt(longPrompt, 1000); // Truncates to 1000 chars
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function truncatePrompt(prompt: string, maxLength: number = 5000): string {
|
|
49
|
+
return truncateText(prompt, maxLength);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sanitize prompt by removing excessive whitespace and control characters
|
|
54
|
+
* Also enforces maximum length of 5000 characters
|
|
55
|
+
*
|
|
56
|
+
* @param prompt - The prompt text to sanitize
|
|
57
|
+
* @returns Sanitized prompt
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* sanitizePrompt("Hello \n\n World\x00"); // "Hello World"
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function sanitizePrompt(prompt: string): string {
|
|
65
|
+
return prompt
|
|
66
|
+
.trim()
|
|
67
|
+
.replace(/\s+/g, " ")
|
|
68
|
+
// Remove control characters except tab, newline, carriage return
|
|
69
|
+
// eslint-disable-next-line no-control-regex
|
|
70
|
+
.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, '')
|
|
71
|
+
.slice(0, 5000);
|
|
14
72
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
MAX_TIMEOUT_MS,
|
|
11
11
|
MAX_RETRY_COUNT,
|
|
12
12
|
} from './constants';
|
|
13
|
+
import { isNonEmptyString } from '../validators/string-validator.util';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Validate base64 image string
|
|
@@ -19,7 +20,7 @@ export function isValidBase64Image(value: unknown): boolean {
|
|
|
19
20
|
return false;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
// Check data URI prefix
|
|
23
|
+
// Check data URI prefix - use direct check instead of type guard to avoid type narrowing issues
|
|
23
24
|
if (value.startsWith("data:image/")) {
|
|
24
25
|
const base64Part = value.split("base64,")[1];
|
|
25
26
|
if (!base64Part) return false;
|
|
@@ -67,7 +68,9 @@ export function isValidModelId(value: unknown): boolean {
|
|
|
67
68
|
* Validate prompt string
|
|
68
69
|
*/
|
|
69
70
|
export function isValidPrompt(value: unknown): boolean {
|
|
70
|
-
|
|
71
|
+
// Use type guard first, then check length
|
|
72
|
+
if (!isNonEmptyString(value)) return false;
|
|
73
|
+
return value.length <= MAX_PROMPT_LENGTH;
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
/**
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data URI Validation Utilities
|
|
3
|
+
* Centralized data URI format checking and validation
|
|
4
|
+
*
|
|
5
|
+
* Eliminates the duplicated pattern:
|
|
6
|
+
* `value.startsWith("data:image/")`
|
|
7
|
+
* which appeared in 4 different files.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if value is a data URI (any type)
|
|
12
|
+
*
|
|
13
|
+
* @param value - Value to check
|
|
14
|
+
* @returns Type guard indicating if the value is a data URI string
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* isDataUri("data:text/plain;base64,SGVsbG8="); // true
|
|
19
|
+
* isDataUri("https://example.com/image.png"); // false
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function isDataUri(value: unknown): value is string {
|
|
23
|
+
return typeof value === "string" && value.startsWith("data:");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if value is an image data URI
|
|
28
|
+
*
|
|
29
|
+
* @param value - Value to check
|
|
30
|
+
* @returns Type guard indicating if the value is an image data URI string
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* isImageDataUri("data:image/png;base64,iVBOR..."); // true
|
|
35
|
+
* isImageDataUri("data:text/plain;base64,SGVsbG8="); // false
|
|
36
|
+
* isImageDataUri("https://example.com/image.png"); // false
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function isImageDataUri(value: unknown): value is string {
|
|
40
|
+
return typeof value === "string" && value.startsWith("data:image/");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if value is a base64-encoded data URI
|
|
45
|
+
*
|
|
46
|
+
* @param value - Value to check
|
|
47
|
+
* @returns Type guard indicating if the value contains base64 encoding
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* isBase64DataUri("data:image/png;base64,iVBOR..."); // true
|
|
52
|
+
* isBase64DataUri("data:image/svg+xml,<svg>...</svg>"); // false
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function isBase64DataUri(value: unknown): value is string {
|
|
56
|
+
return isDataUri(value) && value.includes("base64,");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extract MIME type from data URI
|
|
61
|
+
*
|
|
62
|
+
* @param dataUri - Data URI string
|
|
63
|
+
* @returns MIME type or null if not found
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* extractMimeType("data:image/png;base64,iVBOR..."); // "image/png"
|
|
68
|
+
* extractMimeType("data:text/plain;charset=utf-8,Hello"); // "text/plain"
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function extractMimeType(dataUri: string): string | null {
|
|
72
|
+
const match = dataUri.match(/^data:([^;,]+)/);
|
|
73
|
+
return match ? match[1] : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extract base64 content from data URI
|
|
78
|
+
*
|
|
79
|
+
* @param dataUri - Data URI string
|
|
80
|
+
* @returns Base64 content or null if not base64-encoded
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* extractBase64Content("data:image/png;base64,iVBOR..."); // "iVBOR..."
|
|
85
|
+
* extractBase64Content("data:text/plain,Hello"); // null
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function extractBase64Content(dataUri: string): string | null {
|
|
89
|
+
const parts = dataUri.split("base64,");
|
|
90
|
+
return parts.length === 2 ? parts[1] : null;
|
|
91
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* String Validation Utilities
|
|
3
|
+
* Common string validation patterns
|
|
4
|
+
*
|
|
5
|
+
* Eliminates the duplicated pattern:
|
|
6
|
+
* `value.trim().length === 0`
|
|
7
|
+
* which appeared in 3+ files.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if string is empty or whitespace-only
|
|
12
|
+
*
|
|
13
|
+
* @param value - Value to check
|
|
14
|
+
* @returns True if the value is an empty string or contains only whitespace
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* isEmptyString(""); // true
|
|
19
|
+
* isEmptyString(" "); // true
|
|
20
|
+
* isEmptyString("Hello"); // false
|
|
21
|
+
* isEmptyString(null); // false
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function isEmptyString(value: unknown): boolean {
|
|
25
|
+
return typeof value === "string" && value.trim().length === 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if value is a non-empty string
|
|
30
|
+
* Type guard version for better TypeScript inference
|
|
31
|
+
*
|
|
32
|
+
* @param value - Value to check
|
|
33
|
+
* @returns Type guard indicating if the value is a non-empty string
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* if (isNonEmptyString(input)) {
|
|
38
|
+
* // TypeScript knows input is a string here
|
|
39
|
+
* console.log(input.toUpperCase());
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function isNonEmptyString(value: unknown): value is string {
|
|
44
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if value is a string (empty or non-empty)
|
|
49
|
+
* Basic type guard for string validation
|
|
50
|
+
*
|
|
51
|
+
* @param value - Value to check
|
|
52
|
+
* @returns Type guard indicating if the value is a string
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* if (isString(value)) {
|
|
57
|
+
* // TypeScript knows value is a string here
|
|
58
|
+
* console.log(value.length);
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function isString(value: unknown): value is string {
|
|
63
|
+
return typeof value === "string";
|
|
64
|
+
}
|
|
@@ -43,8 +43,8 @@ export function useModels(props: UseModelsProps): UseModelsReturn {
|
|
|
43
43
|
config?.defaultModelId,
|
|
44
44
|
]);
|
|
45
45
|
|
|
46
|
-
//
|
|
47
|
-
|
|
46
|
+
// Unified load function - eliminates duplication between effect and manual reload
|
|
47
|
+
const performLoad = useCallback(() => {
|
|
48
48
|
setIsLoading(true);
|
|
49
49
|
setError(null);
|
|
50
50
|
|
|
@@ -59,21 +59,13 @@ export function useModels(props: UseModelsProps): UseModelsReturn {
|
|
|
59
59
|
}
|
|
60
60
|
}, [type, memoizedConfig]);
|
|
61
61
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
// Auto-load on mount and when dependencies change
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
performLoad();
|
|
65
|
+
}, [performLoad]);
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
setModels(selectionData.models);
|
|
70
|
-
setSelectedModel(selectionData.selectedModel);
|
|
71
|
-
} catch (err) {
|
|
72
|
-
setError(err instanceof Error ? err.message : 'Failed to load models');
|
|
73
|
-
} finally {
|
|
74
|
-
setIsLoading(false);
|
|
75
|
-
}
|
|
76
|
-
}, [type, memoizedConfig]);
|
|
67
|
+
// Alias for manual reloads (same function, clearer name for external API)
|
|
68
|
+
const loadModels = performLoad;
|
|
77
69
|
|
|
78
70
|
const selectModel = useCallback(
|
|
79
71
|
(modelId: string) => {
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Prompt Helper Utilities
|
|
3
|
-
* Functions for prompt manipulation and sanitization
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Truncate prompt to maximum length
|
|
8
|
-
*/
|
|
9
|
-
export function truncatePrompt(prompt: string, maxLength: number = 5000): string {
|
|
10
|
-
if (prompt.length <= maxLength) {
|
|
11
|
-
return prompt;
|
|
12
|
-
}
|
|
13
|
-
return prompt.slice(0, maxLength - 3) + "...";
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Sanitize prompt by removing excessive whitespace and control characters
|
|
18
|
-
*/
|
|
19
|
-
export function sanitizePrompt(prompt: string): string {
|
|
20
|
-
return prompt
|
|
21
|
-
.trim()
|
|
22
|
-
.replace(/\s+/g, " ")
|
|
23
|
-
// Remove control characters except tab, newline, carriage return
|
|
24
|
-
// eslint-disable-next-line no-control-regex
|
|
25
|
-
.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, '')
|
|
26
|
-
.slice(0, 5000);
|
|
27
|
-
}
|