@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-fal-provider",
3
- "version": "2.1.8",
3
+ "version": "2.1.10",
4
4
  "description": "FAL AI provider for React Native - implements IAIProvider interface for unified AI generation",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -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
- error instanceof Error ? error.message : String(error)
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
- costError instanceof Error ? costError.message : String(costError),
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
- failError instanceof Error ? failError.message : String(failError),
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 || userMessage.trim().length === 0) {
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
- cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
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}: ${error instanceof Error ? error.message : String(error)}`
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
- const IMAGE_URL_KEYS = [
9
- "image_url",
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 IMAGE_URL_KEYS) {
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 instanceof Error ? error.message : "Unknown 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 instanceof Error ? error.message : "Unknown 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: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`
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
- ? (result.reason instanceof Error ? result.reason.message : String(result.reason))
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.startsWith('data:image/')) {
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 if present
138
- const imageFields = [
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 || value.trim().length === 0) {
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
- error instanceof Error ? error.message : String(error),
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
- error instanceof Error ? error.message : String(error),
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
- error instanceof Error ? error.message : String(error),
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
- error instanceof Error ? error.message : String(error)
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(text: string, maxLength: number): string {
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 - 3) + "...";
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
- return typeof value === "string" && value.trim().length > 0 && value.length <= MAX_PROMPT_LENGTH;
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
- // Direct effect - no intermediate callback needed
47
- useEffect(() => {
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
- // Separate refresh callback for manual reloads
63
- const loadModels = useCallback(() => {
64
- setIsLoading(true);
65
- setError(null);
62
+ // Auto-load on mount and when dependencies change
63
+ useEffect(() => {
64
+ performLoad();
65
+ }, [performLoad]);
66
66
 
67
- try {
68
- const selectionData = falModelsService.getModelSelectionData(type, memoizedConfig);
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
- }