@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.61.
|
|
3
|
+
"version": "1.61.34",
|
|
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",
|
|
@@ -47,15 +47,11 @@ export type GenerationMode = "direct" | "queued";
|
|
|
47
47
|
export interface BackgroundQueueConfig {
|
|
48
48
|
readonly mode?: GenerationMode;
|
|
49
49
|
readonly maxConcurrent?: number;
|
|
50
|
-
readonly retryCount?: number;
|
|
51
|
-
readonly retryDelayMs?: number;
|
|
52
50
|
readonly queryKey?: readonly string[];
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
export const DEFAULT_QUEUE_CONFIG: Required<BackgroundQueueConfig> = {
|
|
56
54
|
mode: "queued",
|
|
57
55
|
maxConcurrent: 1,
|
|
58
|
-
retryCount: 2,
|
|
59
|
-
retryDelayMs: 2000,
|
|
60
56
|
queryKey: ["ai", "background-jobs"],
|
|
61
57
|
};
|
|
@@ -8,7 +8,7 @@ export interface PollingConfig {
|
|
|
8
8
|
initialIntervalMs: number;
|
|
9
9
|
maxIntervalMs: number;
|
|
10
10
|
backoffMultiplier: number;
|
|
11
|
-
|
|
11
|
+
maxTotalTimeMs?: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export const DEFAULT_POLLING_CONFIG: PollingConfig = {
|
|
@@ -16,13 +16,11 @@ export const DEFAULT_POLLING_CONFIG: PollingConfig = {
|
|
|
16
16
|
initialIntervalMs: 1000,
|
|
17
17
|
maxIntervalMs: 3000,
|
|
18
18
|
backoffMultiplier: 1.2,
|
|
19
|
-
maxConsecutiveErrors: 5,
|
|
20
19
|
};
|
|
21
20
|
|
|
22
21
|
export interface PollingState {
|
|
23
22
|
attempt: number;
|
|
24
23
|
lastProgress: number;
|
|
25
|
-
consecutiveErrors: number;
|
|
26
24
|
startTime: number;
|
|
27
25
|
}
|
|
28
26
|
|
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
* Apps implement these interfaces to provide their specific logic
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Metadata types for credit cost calculation
|
|
9
|
+
*/
|
|
10
|
+
export interface CreditCostMetadata {
|
|
11
|
+
readonly model?: string;
|
|
12
|
+
readonly duration?: number;
|
|
13
|
+
readonly resolution?: string;
|
|
14
|
+
readonly quality?: string;
|
|
15
|
+
readonly [key: string]: string | number | boolean | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
/**
|
|
8
19
|
* Network service interface
|
|
9
20
|
* Handles network availability checks
|
|
@@ -52,7 +63,7 @@ export interface ICreditService {
|
|
|
52
63
|
*/
|
|
53
64
|
calculateCost: (
|
|
54
65
|
capability: string,
|
|
55
|
-
metadata?:
|
|
66
|
+
metadata?: CreditCostMetadata,
|
|
56
67
|
) => number;
|
|
57
68
|
}
|
|
58
69
|
|
|
@@ -91,6 +102,13 @@ export interface IAuthService {
|
|
|
91
102
|
requireAuth: () => string;
|
|
92
103
|
}
|
|
93
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Analytics event data
|
|
107
|
+
*/
|
|
108
|
+
export interface AnalyticsEventData {
|
|
109
|
+
readonly [key: string]: string | number | boolean | null | undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
94
112
|
/**
|
|
95
113
|
* Analytics service interface (optional)
|
|
96
114
|
* Tracks events for analytics
|
|
@@ -101,7 +119,7 @@ export interface IAnalyticsService {
|
|
|
101
119
|
* @param event - Event name
|
|
102
120
|
* @param data - Event data
|
|
103
121
|
*/
|
|
104
|
-
track: (event: string, data:
|
|
122
|
+
track: (event: string, data: AnalyticsEventData) => void;
|
|
105
123
|
}
|
|
106
124
|
|
|
107
125
|
/**
|
package/src/domains/content-moderation/infrastructure/services/content-moderation.service.ts
CHANGED
|
@@ -103,7 +103,8 @@ class ContentModerationService {
|
|
|
103
103
|
0
|
|
104
104
|
);
|
|
105
105
|
|
|
106
|
-
return
|
|
106
|
+
// Only divide by 2 if we have violations, otherwise return 1.0
|
|
107
|
+
return violations.length > 0 ? Math.min(1.0, score / Math.max(1, violations.length)) : 1.0;
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
private determineAction(
|
|
@@ -7,11 +7,91 @@ import type { Violation } from "../../../domain/entities/moderation.types";
|
|
|
7
7
|
import { patternMatcherService } from "../pattern-matcher.service";
|
|
8
8
|
import { rulesRegistry } from "../../rules/rules-registry";
|
|
9
9
|
import { BaseModerator, type ModerationResult } from "./base.moderator";
|
|
10
|
+
import { DEFAULT_MAX_TEXT_LENGTH } from "../../../../infrastructure/constants/content.constants";
|
|
10
11
|
|
|
11
12
|
declare const __DEV__: boolean;
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* HTML entity encoding detection
|
|
16
|
+
* More reliable than regex for detecting encoded malicious content
|
|
17
|
+
*/
|
|
18
|
+
function containsHTMLEntities(content: string): boolean {
|
|
19
|
+
const htmlEntities = [
|
|
20
|
+
/</gi, />/gi, /"/gi, /&/gi, /'/gi,
|
|
21
|
+
/&#\d+;/gi, /&#x[0-9a-fA-F]+;/gi,
|
|
22
|
+
];
|
|
23
|
+
return htmlEntities.some(entity => entity.test(content));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Safe string matching for malicious code detection
|
|
28
|
+
* Uses string operations instead of regex where possible
|
|
29
|
+
*/
|
|
30
|
+
function containsMaliciousPatterns(content: string): boolean {
|
|
31
|
+
const lowerContent = content.toLowerCase();
|
|
32
|
+
|
|
33
|
+
// Check for script tags (case-insensitive)
|
|
34
|
+
const scriptPatterns = ["<script", "</script>", "javascript:", "onclick=", "onerror=", "onload="];
|
|
35
|
+
for (const pattern of scriptPatterns) {
|
|
36
|
+
if (lowerContent.includes(pattern)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check for HTML entities (potential evasion)
|
|
42
|
+
if (containsHTMLEntities(content)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Multi-layered prompt injection detection
|
|
51
|
+
* Combines regex with string matching for better security
|
|
52
|
+
*/
|
|
53
|
+
function containsPromptInjection(content: string): boolean {
|
|
54
|
+
const lowerContent = content.toLowerCase();
|
|
55
|
+
|
|
56
|
+
// Critical injection patterns (string-based for safety)
|
|
57
|
+
const criticalPatterns = [
|
|
58
|
+
"ignore all instructions",
|
|
59
|
+
"ignore previous instructions",
|
|
60
|
+
"disregard all instructions",
|
|
61
|
+
"forget all instructions",
|
|
62
|
+
"you are now a",
|
|
63
|
+
"jailbreak",
|
|
64
|
+
"dan mode",
|
|
65
|
+
"developer mode",
|
|
66
|
+
"system:",
|
|
67
|
+
"[system]",
|
|
68
|
+
"<<system>>",
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
for (const pattern of criticalPatterns) {
|
|
72
|
+
if (lowerContent.includes(pattern)) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Additional regex patterns for more complex matching
|
|
78
|
+
const regexPatterns = [
|
|
79
|
+
/act\s+as\s+(if|though)\s+you/gi,
|
|
80
|
+
/pretend\s+(you\s+are|to\s+be)/gi,
|
|
81
|
+
/bypass\s+(your\s+)?(safety|content|moderation)/gi,
|
|
82
|
+
/override\s+(your\s+)?(restrictions?|limitations?|rules?)/gi,
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
return regexPatterns.some(pattern => {
|
|
86
|
+
try {
|
|
87
|
+
return pattern.test(content);
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
14
93
|
|
|
94
|
+
// Kept for reference but no longer used directly - using safer functions above
|
|
15
95
|
const MALICIOUS_CODE_PATTERNS = [
|
|
16
96
|
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
|
|
17
97
|
/javascript:/gi,
|
|
@@ -36,7 +116,7 @@ const PROMPT_INJECTION_PATTERNS = [
|
|
|
36
116
|
];
|
|
37
117
|
|
|
38
118
|
class TextModerator extends BaseModerator {
|
|
39
|
-
private maxLength =
|
|
119
|
+
private maxLength = DEFAULT_MAX_TEXT_LENGTH;
|
|
40
120
|
|
|
41
121
|
setMaxLength(length: number): void {
|
|
42
122
|
this.maxLength = length;
|
|
@@ -106,11 +186,11 @@ class TextModerator extends BaseModerator {
|
|
|
106
186
|
}
|
|
107
187
|
|
|
108
188
|
private containsMaliciousCode(content: string): boolean {
|
|
109
|
-
return
|
|
189
|
+
return containsMaliciousPatterns(content);
|
|
110
190
|
}
|
|
111
191
|
|
|
112
192
|
private containsPromptInjection(content: string): boolean {
|
|
113
|
-
return
|
|
193
|
+
return containsPromptInjection(content);
|
|
114
194
|
}
|
|
115
195
|
|
|
116
196
|
private evaluateRules(content: string): Violation[] {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pattern Matcher Service
|
|
3
|
-
* Utility service for regex pattern matching
|
|
3
|
+
* Utility service for regex pattern matching with security validations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export interface PatternMatch {
|
|
@@ -10,8 +10,70 @@ export interface PatternMatch {
|
|
|
10
10
|
position?: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Validates that a regex pattern is safe to use
|
|
15
|
+
* Prevents ReDoS (Regular Expression Denial of Service) attacks
|
|
16
|
+
*/
|
|
17
|
+
function isValidRegexPattern(pattern: string): boolean {
|
|
18
|
+
if (!pattern || typeof pattern !== "string") {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Check for dangerous patterns that could cause ReDoS
|
|
23
|
+
const dangerousPatterns = [
|
|
24
|
+
/\([^)]*\+\([^)]*\+\)/, // Nested repeated groups
|
|
25
|
+
/\([^)]*\*[^)]*\*\)/, // Multiple nested stars
|
|
26
|
+
/\.\*\.*/, // Multiple wildcards
|
|
27
|
+
/\.\+\.+/, // Multiple repeated wildcards
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
for (const dangerous of dangerousPatterns) {
|
|
31
|
+
if (dangerous.test(pattern)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Limit pattern length to prevent potential attacks
|
|
37
|
+
const MAX_PATTERN_LENGTH = 1000;
|
|
38
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Test if pattern compiles without errors
|
|
44
|
+
new RegExp(pattern);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Safely escapes special regex characters in user input
|
|
53
|
+
*/
|
|
54
|
+
function escapeRegExp(str: string): string {
|
|
55
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
56
|
+
}
|
|
57
|
+
|
|
13
58
|
class PatternMatcherService {
|
|
14
59
|
matchPattern(content: string, pattern: string): PatternMatch {
|
|
60
|
+
// Validate inputs
|
|
61
|
+
if (!content || typeof content !== "string") {
|
|
62
|
+
return { pattern, matched: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!pattern || typeof pattern !== "string") {
|
|
66
|
+
return { pattern, matched: false };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Validate pattern safety before using it
|
|
70
|
+
if (!isValidRegexPattern(pattern)) {
|
|
71
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
72
|
+
console.warn("[PatternMatcher] Invalid or unsafe pattern rejected:", pattern);
|
|
73
|
+
}
|
|
74
|
+
return { pattern, matched: false };
|
|
75
|
+
}
|
|
76
|
+
|
|
15
77
|
try {
|
|
16
78
|
const regex = new RegExp(pattern, "gi");
|
|
17
79
|
const match = regex.exec(content);
|
|
@@ -26,11 +88,32 @@ class PatternMatcherService {
|
|
|
26
88
|
}
|
|
27
89
|
|
|
28
90
|
return { pattern, matched: false };
|
|
29
|
-
} catch {
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
93
|
+
console.warn("[PatternMatcher] Regex error:", error);
|
|
94
|
+
}
|
|
30
95
|
return { pattern, matched: false };
|
|
31
96
|
}
|
|
32
97
|
}
|
|
33
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Safe string matching without regex (for user-provided search terms)
|
|
101
|
+
*/
|
|
102
|
+
safeStringMatch(content: string, searchTerm: string): boolean {
|
|
103
|
+
if (!content || !searchTerm) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const escaped = escapeRegExp(searchTerm);
|
|
109
|
+
const regex = new RegExp(escaped, "gi");
|
|
110
|
+
return regex.test(content);
|
|
111
|
+
} catch {
|
|
112
|
+
// Fallback to simple includes if regex fails
|
|
113
|
+
return content.toLowerCase().includes(searchTerm.toLowerCase());
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
34
117
|
matchAnyPattern(content: string, patterns: string[]): PatternMatch[] {
|
|
35
118
|
return patterns.map((pattern) => this.matchPattern(content, pattern));
|
|
36
119
|
}
|
|
@@ -43,8 +43,15 @@ export class CreationsWriter {
|
|
|
43
43
|
await setDoc(docRef, data);
|
|
44
44
|
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[CreationsWriter] create() success");
|
|
45
45
|
} catch (error) {
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
47
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
48
|
+
console.error("[CreationsWriter] create() error", {
|
|
49
|
+
userId,
|
|
50
|
+
creationId: creation.id,
|
|
51
|
+
error: errorMessage,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`Failed to create creation ${creation.id}: ${errorMessage}`);
|
|
48
55
|
}
|
|
49
56
|
}
|
|
50
57
|
|
|
@@ -52,7 +59,12 @@ export class CreationsWriter {
|
|
|
52
59
|
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[CreationsWriter] update()", { userId, id });
|
|
53
60
|
|
|
54
61
|
const docRef = this.pathResolver.getDocRef(userId, id);
|
|
55
|
-
if (!docRef)
|
|
62
|
+
if (!docRef) {
|
|
63
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
64
|
+
console.error("[CreationsWriter] update() - Firestore not initialized", { userId, id });
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
56
68
|
|
|
57
69
|
try {
|
|
58
70
|
const updateData: Record<string, unknown> = {};
|
|
@@ -62,7 +74,14 @@ export class CreationsWriter {
|
|
|
62
74
|
await updateDoc(docRef, updateData);
|
|
63
75
|
return true;
|
|
64
76
|
} catch (error) {
|
|
65
|
-
|
|
77
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
78
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
79
|
+
console.error("[CreationsWriter] update() error", {
|
|
80
|
+
userId,
|
|
81
|
+
creationId: id,
|
|
82
|
+
error: errorMessage,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
66
85
|
return false;
|
|
67
86
|
}
|
|
68
87
|
}
|
|
@@ -73,7 +92,15 @@ export class CreationsWriter {
|
|
|
73
92
|
try {
|
|
74
93
|
await updateDoc(docRef, { deletedAt: new Date() });
|
|
75
94
|
return true;
|
|
76
|
-
} catch {
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
97
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
98
|
+
console.error("[CreationsWriter] delete() error", {
|
|
99
|
+
userId,
|
|
100
|
+
creationId,
|
|
101
|
+
error: errorMessage,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
77
104
|
return false;
|
|
78
105
|
}
|
|
79
106
|
}
|
|
@@ -84,7 +111,15 @@ export class CreationsWriter {
|
|
|
84
111
|
try {
|
|
85
112
|
await deleteDoc(docRef);
|
|
86
113
|
return true;
|
|
87
|
-
} catch {
|
|
114
|
+
} catch (error) {
|
|
115
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
116
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
117
|
+
console.error("[CreationsWriter] hardDelete() error", {
|
|
118
|
+
userId,
|
|
119
|
+
creationId,
|
|
120
|
+
error: errorMessage,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
88
123
|
return false;
|
|
89
124
|
}
|
|
90
125
|
}
|
|
@@ -95,7 +130,15 @@ export class CreationsWriter {
|
|
|
95
130
|
try {
|
|
96
131
|
await updateDoc(docRef, { deletedAt: null });
|
|
97
132
|
return true;
|
|
98
|
-
} catch {
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
135
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
136
|
+
console.error("[CreationsWriter] restore() error", {
|
|
137
|
+
userId,
|
|
138
|
+
creationId,
|
|
139
|
+
error: errorMessage,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
99
142
|
return false;
|
|
100
143
|
}
|
|
101
144
|
}
|
|
@@ -106,7 +149,16 @@ export class CreationsWriter {
|
|
|
106
149
|
try {
|
|
107
150
|
await updateDoc(docRef, { isShared });
|
|
108
151
|
return true;
|
|
109
|
-
} catch {
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
154
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
155
|
+
console.error("[CreationsWriter] updateShared() error", {
|
|
156
|
+
userId,
|
|
157
|
+
creationId,
|
|
158
|
+
isShared,
|
|
159
|
+
error: errorMessage,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
110
162
|
return false;
|
|
111
163
|
}
|
|
112
164
|
}
|
|
@@ -118,7 +170,10 @@ export class CreationsWriter {
|
|
|
118
170
|
const docRef = this.pathResolver.getDocRef(userId, creationId);
|
|
119
171
|
if (!docRef) {
|
|
120
172
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
121
|
-
console.
|
|
173
|
+
console.warn("[CreationsWriter] updateFavorite() - Firestore not initialized", {
|
|
174
|
+
userId,
|
|
175
|
+
creationId,
|
|
176
|
+
});
|
|
122
177
|
}
|
|
123
178
|
return false;
|
|
124
179
|
}
|
|
@@ -129,8 +184,14 @@ export class CreationsWriter {
|
|
|
129
184
|
}
|
|
130
185
|
return true;
|
|
131
186
|
} catch (error) {
|
|
187
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
132
188
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
133
|
-
console.error("[CreationsWriter] updateFavorite() error",
|
|
189
|
+
console.error("[CreationsWriter] updateFavorite() error", {
|
|
190
|
+
userId,
|
|
191
|
+
creationId,
|
|
192
|
+
isFavorite,
|
|
193
|
+
error: errorMessage,
|
|
194
|
+
});
|
|
134
195
|
}
|
|
135
196
|
return false;
|
|
136
197
|
}
|
|
@@ -143,19 +204,41 @@ export class CreationsWriter {
|
|
|
143
204
|
try {
|
|
144
205
|
await updateDoc(docRef, { rating, ratedAt: new Date() });
|
|
145
206
|
if (description || rating) {
|
|
146
|
-
|
|
207
|
+
try {
|
|
208
|
+
await submitFeedback({
|
|
209
|
+
userId,
|
|
210
|
+
userEmail: null,
|
|
211
|
+
type: "creation_rating",
|
|
212
|
+
title: `Creation Rating: ${rating} Stars`,
|
|
213
|
+
description: description || `User rated creation ${rating} stars`,
|
|
214
|
+
rating,
|
|
215
|
+
status: "pending",
|
|
216
|
+
});
|
|
217
|
+
} catch (feedbackError) {
|
|
218
|
+
// Log but don't fail - the rating was saved successfully
|
|
219
|
+
const feedbackErrorMessage = feedbackError instanceof Error
|
|
220
|
+
? feedbackError.message
|
|
221
|
+
: String(feedbackError);
|
|
222
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
223
|
+
console.warn("[CreationsWriter] rate() - feedback submission failed", {
|
|
224
|
+
userId,
|
|
225
|
+
creationId,
|
|
226
|
+
error: feedbackErrorMessage,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
234
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
235
|
+
console.error("[CreationsWriter] rate() error", {
|
|
147
236
|
userId,
|
|
148
|
-
|
|
149
|
-
type: "creation_rating",
|
|
150
|
-
title: `Creation Rating: ${rating} Stars`,
|
|
151
|
-
description: description || `User rated creation ${rating} stars`,
|
|
237
|
+
creationId,
|
|
152
238
|
rating,
|
|
153
|
-
|
|
239
|
+
error: errorMessage,
|
|
154
240
|
});
|
|
155
241
|
}
|
|
156
|
-
return true;
|
|
157
|
-
} catch (error) {
|
|
158
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) console.error("[CreationsWriter] rate() error", error);
|
|
159
242
|
return false;
|
|
160
243
|
}
|
|
161
244
|
}
|
|
@@ -88,10 +88,19 @@ export function useAdvancedFilter<T extends FilterableCreation>({
|
|
|
88
88
|
setFilter(DEFAULT_CREATION_FILTER);
|
|
89
89
|
}, []);
|
|
90
90
|
|
|
91
|
-
const hasActiveFilters =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
const hasActiveFilters = useMemo(
|
|
92
|
+
() =>
|
|
93
|
+
filter.type !== "all" || filter.status !== "all" || !!filter.searchQuery,
|
|
94
|
+
[filter.type, filter.status, filter.searchQuery]
|
|
95
|
+
);
|
|
96
|
+
const activeMediaFilter = useMemo(
|
|
97
|
+
() => (filter.type as string) || "all",
|
|
98
|
+
[filter.type]
|
|
99
|
+
);
|
|
100
|
+
const activeStatusFilter = useMemo(
|
|
101
|
+
() => (filter.status as string) || "all",
|
|
102
|
+
[filter.status]
|
|
103
|
+
);
|
|
95
104
|
|
|
96
105
|
return {
|
|
97
106
|
filtered,
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { useEffect, useRef, useCallback, useMemo } from "react";
|
|
9
9
|
import { providerRegistry } from "../../../../infrastructure/services/provider-registry.service";
|
|
10
10
|
import { QUEUE_STATUS, CREATION_STATUS } from "../../../../domain/constants/queue-status.constants";
|
|
11
|
+
import { GALLERY_POLL_INTERVAL_MS } from "../../../../infrastructure/constants/polling.constants";
|
|
11
12
|
import {
|
|
12
13
|
extractResultUrl,
|
|
13
14
|
type FalResult,
|
|
@@ -17,8 +18,6 @@ import type { ICreationsRepository } from "../../domain/repositories/ICreationsR
|
|
|
17
18
|
|
|
18
19
|
declare const __DEV__: boolean;
|
|
19
20
|
|
|
20
|
-
const POLL_INTERVAL_MS = 5000; // Gallery polls slower than wizard
|
|
21
|
-
|
|
22
21
|
export interface UseProcessingJobsPollerConfig {
|
|
23
22
|
readonly userId?: string | null;
|
|
24
23
|
readonly creations: Creation[];
|
|
@@ -43,12 +42,13 @@ export function useProcessingJobsPoller(
|
|
|
43
42
|
const pollingRef = useRef<Set<string>>(new Set());
|
|
44
43
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
45
44
|
|
|
46
|
-
// Find creations that need polling -
|
|
45
|
+
// Find creations that need polling - use Set for O(1) lookups
|
|
47
46
|
const processingJobIds = useMemo(
|
|
48
|
-
() =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
() => new Set(
|
|
48
|
+
creations
|
|
49
|
+
.filter((c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model)
|
|
50
|
+
.map((c) => c.id)
|
|
51
|
+
),
|
|
52
52
|
[creations],
|
|
53
53
|
);
|
|
54
54
|
|
|
@@ -56,8 +56,7 @@ export function useProcessingJobsPoller(
|
|
|
56
56
|
() => creations.filter(
|
|
57
57
|
(c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model,
|
|
58
58
|
),
|
|
59
|
-
|
|
60
|
-
[processingJobIds],
|
|
59
|
+
[creations],
|
|
61
60
|
);
|
|
62
61
|
|
|
63
62
|
const pollJob = useCallback(
|
|
@@ -134,6 +133,8 @@ export function useProcessingJobsPoller(
|
|
|
134
133
|
clearInterval(intervalRef.current);
|
|
135
134
|
intervalRef.current = null;
|
|
136
135
|
}
|
|
136
|
+
// Clear polling set to prevent memory leak
|
|
137
|
+
pollingRef.current.clear();
|
|
137
138
|
};
|
|
138
139
|
}, [enabled, userId, processingJobs, pollJob]);
|
|
139
140
|
|
|
@@ -52,7 +52,7 @@ export const useFaceDetection = ({ aiAnalyzer, model }: UseFaceDetectionProps):
|
|
|
52
52
|
setState(initialState);
|
|
53
53
|
}, []);
|
|
54
54
|
|
|
55
|
-
const isValid = state.result ? isValidFace(state.result) : false;
|
|
55
|
+
const isValid = state.result !== null && state.result !== undefined ? isValidFace(state.result) : false;
|
|
56
56
|
|
|
57
57
|
return {
|
|
58
58
|
state,
|
|
@@ -29,25 +29,29 @@ let flowStoreInstance: FlowStoreType | null = null;
|
|
|
29
29
|
|
|
30
30
|
export const useFlow = (config: UseFlowConfig): UseFlowReturn => {
|
|
31
31
|
const storeRef = useRef<FlowStoreType | null>(null);
|
|
32
|
-
const prevConfigRef = useRef<{ initialStepIndex?: number; initialStepId?: string } | undefined>(undefined);
|
|
32
|
+
const prevConfigRef = useRef<{ initialStepIndex?: number; initialStepId?: string; stepsCount: number } | undefined>(undefined);
|
|
33
|
+
const isResettingRef = useRef(false);
|
|
33
34
|
|
|
34
|
-
// Detect config changes (initialStepIndex or
|
|
35
|
+
// Detect config changes (initialStepIndex, initialStepId, or steps changed)
|
|
35
36
|
const configChanged =
|
|
36
37
|
prevConfigRef.current !== undefined &&
|
|
37
38
|
(prevConfigRef.current.initialStepIndex !== config.initialStepIndex ||
|
|
38
|
-
prevConfigRef.current.initialStepId !== config.initialStepId
|
|
39
|
+
prevConfigRef.current.initialStepId !== config.initialStepId ||
|
|
40
|
+
prevConfigRef.current.stepsCount !== config.steps.length);
|
|
39
41
|
|
|
40
|
-
// If config changed, reset and recreate store
|
|
41
|
-
if (configChanged) {
|
|
42
|
+
// If config changed, reset and recreate store (with guard against multiple resets)
|
|
43
|
+
if (configChanged && !isResettingRef.current) {
|
|
44
|
+
isResettingRef.current = true;
|
|
42
45
|
if (flowStoreInstance) {
|
|
43
46
|
flowStoreInstance.getState().reset();
|
|
44
47
|
}
|
|
45
48
|
flowStoreInstance = null;
|
|
46
49
|
storeRef.current = null;
|
|
50
|
+
isResettingRef.current = false;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
// Initialize store if needed
|
|
50
|
-
if (!storeRef.current) {
|
|
54
|
+
if (!storeRef.current && !isResettingRef.current) {
|
|
51
55
|
if (!flowStoreInstance) {
|
|
52
56
|
flowStoreInstance = createFlowStore({
|
|
53
57
|
steps: config.steps,
|
|
@@ -62,6 +66,7 @@ export const useFlow = (config: UseFlowConfig): UseFlowReturn => {
|
|
|
62
66
|
prevConfigRef.current = {
|
|
63
67
|
initialStepIndex: config.initialStepIndex,
|
|
64
68
|
initialStepId: config.initialStepId,
|
|
69
|
+
stepsCount: config.steps.length,
|
|
65
70
|
};
|
|
66
71
|
|
|
67
72
|
const store = storeRef.current;
|