@umituz/react-native-ai-generation-content 1.61.54 → 1.61.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/domains/content-moderation/infrastructure/constants/moderation.constants.ts +9 -0
- package/src/domains/content-moderation/infrastructure/services/moderators/image.moderator.ts +2 -4
- package/src/domains/content-moderation/infrastructure/services/moderators/text.moderator.ts +4 -89
- package/src/domains/content-moderation/infrastructure/services/moderators/video.moderator.ts +2 -4
- package/src/domains/content-moderation/infrastructure/utils/content-security.util.ts +39 -0
- package/src/domains/content-moderation/infrastructure/utils/prompt-injection.util.ts +49 -0
- package/src/domains/content-moderation/infrastructure/utils/validators.util.ts +51 -0
- package/src/domains/creations/index.ts +1 -1
- package/src/domains/creations/presentation/components/CreationsFilterBar.tsx +1 -1
- package/src/domains/creations/presentation/hooks/useGalleryState.ts +74 -0
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +13 -28
- package/src/domains/prompts/infrastructure/services/ImagePromptBuilder.ts +2 -33
- package/src/domains/prompts/infrastructure/utils/prompt-creators.util.ts +41 -0
- package/src/domains/scenarios/index.ts +2 -2
- package/src/infrastructure/utils/api-client.util.ts +205 -0
- package/src/infrastructure/utils/error-handling.util.ts +190 -0
- package/src/infrastructure/utils/index.ts +4 -0
- package/src/infrastructure/utils/type-guards.util.ts +153 -0
- package/src/infrastructure/utils/validation.util.ts +188 -0
- package/src/presentation/layouts/DualImageFeatureLayout.tsx +8 -27
- package/src/presentation/layouts/DualImageVideoFeatureLayout.tsx +8 -26
- package/src/presentation/layouts/SingleImageFeatureLayout.tsx +10 -29
- package/src/presentation/layouts/SingleImageWithPromptFeatureLayout.tsx +8 -26
- package/src/presentation/layouts/layout-styles.ts +26 -0
- package/src/presentation/layouts/types/layout-props.ts +20 -43
- /package/src/domains/creations/presentation/components/{CreationsFilterBar.helpers.ts → filter-bar-utils.ts} +0 -0
- /package/src/domains/creations/presentation/utils/{filterUtils.ts → gallery-filters.ts} +0 -0
- /package/src/domains/scenarios/infrastructure/{scenario-helpers.ts → scenario-utils.ts} +0 -0
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.56",
|
|
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",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Moderation Constants
|
|
3
|
+
* Shared constants for content moderation validators
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_PROTOCOLS = ["http:", "https:", "file:", "data:"] as const;
|
|
7
|
+
export const VIDEO_PROTOCOLS = ["http:", "https:", "file:"] as const;
|
|
8
|
+
export const DEFAULT_MAX_URI_LENGTH = 2048;
|
|
9
|
+
export const DEFAULT_MAX_TEXT_LENGTH = 5000;
|
package/src/domains/content-moderation/infrastructure/services/moderators/image.moderator.ts
CHANGED
|
@@ -5,12 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Violation } from "../../../domain/entities/moderation.types";
|
|
7
7
|
import { BaseModerator, type ModerationResult } from "./base.moderator";
|
|
8
|
-
|
|
9
|
-
const DEFAULT_PROTOCOLS = ["http:", "https:", "file:", "data:"];
|
|
10
|
-
const DEFAULT_MAX_URI_LENGTH = 2048;
|
|
8
|
+
import { DEFAULT_PROTOCOLS, DEFAULT_MAX_URI_LENGTH } from "../../constants/moderation.constants";
|
|
11
9
|
|
|
12
10
|
class ImageModerator extends BaseModerator {
|
|
13
|
-
private allowedProtocols = DEFAULT_PROTOCOLS;
|
|
11
|
+
private allowedProtocols: readonly string[] = DEFAULT_PROTOCOLS;
|
|
14
12
|
private maxUriLength = DEFAULT_MAX_URI_LENGTH;
|
|
15
13
|
|
|
16
14
|
setAllowedProtocols(protocols: string[]): void {
|
|
@@ -7,90 +7,12 @@ 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 "
|
|
10
|
+
import { DEFAULT_MAX_TEXT_LENGTH } from "../../constants/moderation.constants";
|
|
11
|
+
import { containsMaliciousPatterns } from "../../utils/content-security.util";
|
|
12
|
+
import { containsPromptInjection } from "../../utils/prompt-injection.util";
|
|
11
13
|
|
|
12
14
|
declare const __DEV__: boolean;
|
|
13
15
|
|
|
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
|
-
}
|
|
93
|
-
|
|
94
16
|
class TextModerator extends BaseModerator {
|
|
95
17
|
private maxLength = DEFAULT_MAX_TEXT_LENGTH;
|
|
96
18
|
|
|
@@ -100,7 +22,6 @@ class TextModerator extends BaseModerator {
|
|
|
100
22
|
|
|
101
23
|
moderate(content: string): ModerationResult {
|
|
102
24
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
103
|
-
|
|
104
25
|
console.log("[TextModerator] moderate() called", {
|
|
105
26
|
contentLength: content?.length ?? 0,
|
|
106
27
|
});
|
|
@@ -109,7 +30,6 @@ class TextModerator extends BaseModerator {
|
|
|
109
30
|
const validationError = this.validate(content);
|
|
110
31
|
if (validationError) {
|
|
111
32
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
112
|
-
|
|
113
33
|
console.log("[TextModerator] validation failed", {
|
|
114
34
|
ruleId: validationError.ruleId,
|
|
115
35
|
violationType: validationError.violationType,
|
|
@@ -121,7 +41,6 @@ class TextModerator extends BaseModerator {
|
|
|
121
41
|
const violations = this.evaluateRules(content);
|
|
122
42
|
|
|
123
43
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
124
|
-
|
|
125
44
|
console.log("[TextModerator] moderate() completed", {
|
|
126
45
|
isAllowed: violations.length === 0,
|
|
127
46
|
violationsCount: violations.length,
|
|
@@ -149,7 +68,7 @@ class TextModerator extends BaseModerator {
|
|
|
149
68
|
);
|
|
150
69
|
}
|
|
151
70
|
|
|
152
|
-
if (
|
|
71
|
+
if (containsPromptInjection(content)) {
|
|
153
72
|
return this.createViolation(
|
|
154
73
|
"prompt-injection",
|
|
155
74
|
"Security",
|
|
@@ -165,10 +84,6 @@ class TextModerator extends BaseModerator {
|
|
|
165
84
|
return containsMaliciousPatterns(content);
|
|
166
85
|
}
|
|
167
86
|
|
|
168
|
-
private containsPromptInjection(content: string): boolean {
|
|
169
|
-
return containsPromptInjection(content);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
87
|
private evaluateRules(content: string): Violation[] {
|
|
173
88
|
const rules = rulesRegistry.getRulesByContentType("text");
|
|
174
89
|
const violations: Violation[] = [];
|
package/src/domains/content-moderation/infrastructure/services/moderators/video.moderator.ts
CHANGED
|
@@ -5,12 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Violation } from "../../../domain/entities/moderation.types";
|
|
7
7
|
import { BaseModerator, type ModerationResult } from "./base.moderator";
|
|
8
|
-
|
|
9
|
-
const DEFAULT_PROTOCOLS = ["http:", "https:", "file:"];
|
|
10
|
-
const DEFAULT_MAX_URI_LENGTH = 2048;
|
|
8
|
+
import { VIDEO_PROTOCOLS, DEFAULT_MAX_URI_LENGTH } from "../../constants/moderation.constants";
|
|
11
9
|
|
|
12
10
|
class VideoModerator extends BaseModerator {
|
|
13
|
-
private allowedProtocols =
|
|
11
|
+
private allowedProtocols: readonly string[] = VIDEO_PROTOCOLS;
|
|
14
12
|
private maxUriLength = DEFAULT_MAX_URI_LENGTH;
|
|
15
13
|
|
|
16
14
|
setAllowedProtocols(protocols: string[]): void {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Security Utilities
|
|
3
|
+
* Provides security validation for content including malicious code detection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* HTML entity encoding detection
|
|
8
|
+
* More reliable than regex for detecting encoded malicious content
|
|
9
|
+
*/
|
|
10
|
+
export function containsHTMLEntities(content: string): boolean {
|
|
11
|
+
const htmlEntities = [
|
|
12
|
+
/</gi, />/gi, /"/gi, /&/gi, /'/gi,
|
|
13
|
+
/&#\d+;/gi, /&#x[0-9a-fA-F]+;/gi,
|
|
14
|
+
];
|
|
15
|
+
return htmlEntities.some(entity => entity.test(content));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Safe string matching for malicious code detection
|
|
20
|
+
* Uses string operations instead of regex where possible
|
|
21
|
+
*/
|
|
22
|
+
export function containsMaliciousPatterns(content: string): boolean {
|
|
23
|
+
const lowerContent = content.toLowerCase();
|
|
24
|
+
|
|
25
|
+
// Check for script tags (case-insensitive)
|
|
26
|
+
const scriptPatterns = ["<script", "</script>", "javascript:", "onclick=", "onerror=", "onload="];
|
|
27
|
+
for (const pattern of scriptPatterns) {
|
|
28
|
+
if (lowerContent.includes(pattern)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check for HTML entities (potential evasion)
|
|
34
|
+
if (containsHTMLEntities(content)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Injection Detection Utilities
|
|
3
|
+
* Detects various forms of prompt injection attacks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Multi-layered prompt injection detection
|
|
8
|
+
* Combines regex with string matching for better security
|
|
9
|
+
*/
|
|
10
|
+
export function containsPromptInjection(content: string): boolean {
|
|
11
|
+
const lowerContent = content.toLowerCase();
|
|
12
|
+
|
|
13
|
+
// Critical injection patterns (string-based for safety)
|
|
14
|
+
const criticalPatterns = [
|
|
15
|
+
"ignore all instructions",
|
|
16
|
+
"ignore previous instructions",
|
|
17
|
+
"disregard all instructions",
|
|
18
|
+
"forget all instructions",
|
|
19
|
+
"you are now a",
|
|
20
|
+
"jailbreak",
|
|
21
|
+
"dan mode",
|
|
22
|
+
"developer mode",
|
|
23
|
+
"system:",
|
|
24
|
+
"[system]",
|
|
25
|
+
"<<system>>",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
for (const pattern of criticalPatterns) {
|
|
29
|
+
if (lowerContent.includes(pattern)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Additional regex patterns for more complex matching
|
|
35
|
+
const regexPatterns = [
|
|
36
|
+
/act\s+as\s+(if|though)\s+you/gi,
|
|
37
|
+
/pretend\s+(you\s+are|to\s+be)/gi,
|
|
38
|
+
/bypass\s+(your\s+)?(safety|content|moderation)/gi,
|
|
39
|
+
/override\s+(your\s+)?(restrictions?|limitations?|rules?)/gi,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
return regexPatterns.some(pattern => {
|
|
43
|
+
try {
|
|
44
|
+
return pattern.test(content);
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Moderation Validators
|
|
3
|
+
* Reusable validation functions for content moderation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validates that content is not empty and is a string
|
|
8
|
+
*/
|
|
9
|
+
export function validateContentPresence(content: unknown): boolean {
|
|
10
|
+
return Boolean(content && typeof content === "string");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validates content length against maximum
|
|
15
|
+
*/
|
|
16
|
+
export function validateContentLength(content: string, maxLength: number): boolean {
|
|
17
|
+
return content.length <= maxLength;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates URI protocol
|
|
22
|
+
*/
|
|
23
|
+
export function validateUriProtocol(uri: string, allowedProtocols: readonly string[]): boolean {
|
|
24
|
+
const lowerUri = uri.toLowerCase();
|
|
25
|
+
return allowedProtocols.some((protocol) => lowerUri.startsWith(protocol));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validates URI
|
|
30
|
+
*/
|
|
31
|
+
export function validateUri(
|
|
32
|
+
uri: string,
|
|
33
|
+
options: {
|
|
34
|
+
maxLength?: number;
|
|
35
|
+
allowedProtocols?: readonly string[];
|
|
36
|
+
} = {}
|
|
37
|
+
): { isValid: boolean; error?: { type: string; message: string } } {
|
|
38
|
+
if (!validateContentPresence(uri)) {
|
|
39
|
+
return { isValid: false, error: { type: "empty-uri", message: "empty URI" } };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (options.maxLength && !validateContentLength(uri, options.maxLength)) {
|
|
43
|
+
return { isValid: false, error: { type: "uri-too-long", message: "URI too long" } };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (options.allowedProtocols && !validateUriProtocol(uri, options.allowedProtocols)) {
|
|
47
|
+
return { isValid: false, error: { type: "invalid-protocol", message: "invalid protocol" } };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { isValid: true };
|
|
51
|
+
}
|
|
@@ -168,7 +168,7 @@ export {
|
|
|
168
168
|
getLocalizedTitle,
|
|
169
169
|
getFilterCategoriesFromConfig,
|
|
170
170
|
getTranslatedTypes,
|
|
171
|
-
} from "./presentation/utils/
|
|
171
|
+
} from "./presentation/utils/gallery-filters";
|
|
172
172
|
|
|
173
173
|
// =============================================================================
|
|
174
174
|
// PRESENTATION LAYER - Screens
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGalleryState Hook
|
|
3
|
+
* Manages the state for the gallery screen including selection and media URL handling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect, useRef, useMemo } from "react";
|
|
7
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
8
|
+
import { getPreviewUrl } from "../../domain/utils";
|
|
9
|
+
|
|
10
|
+
export interface GalleryStateOptions {
|
|
11
|
+
initialCreationId?: string;
|
|
12
|
+
creations: Creation[] | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface GalleryStateReturn {
|
|
16
|
+
selectedCreation: Creation | null;
|
|
17
|
+
showRatingPicker: boolean;
|
|
18
|
+
selectedImageUrl: string | undefined;
|
|
19
|
+
selectedVideoUrl: string | undefined;
|
|
20
|
+
hasMediaToShow: boolean;
|
|
21
|
+
showPreview: boolean;
|
|
22
|
+
setSelectedCreation: (creation: Creation | null) => void;
|
|
23
|
+
setShowRatingPicker: (show: boolean) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useGalleryState(options: GalleryStateOptions): GalleryStateReturn {
|
|
27
|
+
const { initialCreationId, creations } = options;
|
|
28
|
+
const [selectedCreation, setSelectedCreation] = useState<Creation | null>(null);
|
|
29
|
+
const [showRatingPicker, setShowRatingPicker] = useState(false);
|
|
30
|
+
const hasAutoSelectedRef = useRef(false);
|
|
31
|
+
|
|
32
|
+
// Auto-select creation when initialCreationId is provided
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (initialCreationId && creations && creations.length > 0 && !hasAutoSelectedRef.current) {
|
|
35
|
+
const creation = creations.find((c) => c.id === initialCreationId);
|
|
36
|
+
if (creation) {
|
|
37
|
+
hasAutoSelectedRef.current = true;
|
|
38
|
+
setSelectedCreation(creation);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}, [initialCreationId, creations]);
|
|
42
|
+
|
|
43
|
+
// Extract media URLs from selected creation
|
|
44
|
+
const selectedImageUrl = useMemo(
|
|
45
|
+
() => selectedCreation ? (getPreviewUrl(selectedCreation.output) || selectedCreation.uri) : undefined,
|
|
46
|
+
[selectedCreation]
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const selectedVideoUrl = useMemo(
|
|
50
|
+
() => selectedCreation?.output?.videoUrl,
|
|
51
|
+
[selectedCreation]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const hasMediaToShow = useMemo(
|
|
55
|
+
() => Boolean(selectedImageUrl || selectedVideoUrl),
|
|
56
|
+
[selectedImageUrl, selectedVideoUrl]
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const showPreview = useMemo(
|
|
60
|
+
() => Boolean(selectedCreation && hasMediaToShow),
|
|
61
|
+
[selectedCreation, hasMediaToShow]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
selectedCreation,
|
|
66
|
+
showRatingPicker,
|
|
67
|
+
selectedImageUrl,
|
|
68
|
+
selectedVideoUrl,
|
|
69
|
+
hasMediaToShow,
|
|
70
|
+
showPreview,
|
|
71
|
+
setSelectedCreation,
|
|
72
|
+
setShowRatingPicker,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useMemo, useCallback } from "react";
|
|
2
2
|
import { View, FlatList, RefreshControl } from "react-native";
|
|
3
3
|
import {
|
|
4
4
|
useAppDesignTokens,
|
|
@@ -11,11 +11,11 @@ import { useDeleteCreation } from "../hooks/useDeleteCreation";
|
|
|
11
11
|
import { useProcessingJobsPoller } from "../hooks/useProcessingJobsPoller";
|
|
12
12
|
import { useGalleryFilters } from "../hooks/useGalleryFilters";
|
|
13
13
|
import { useGalleryCallbacks } from "../hooks/useGalleryCallbacks";
|
|
14
|
+
import { useGalleryState } from "../hooks/useGalleryState";
|
|
14
15
|
import { GalleryHeader, CreationCard, GalleryEmptyStates } from "../components";
|
|
15
16
|
import { GalleryResultPreview } from "../components/GalleryResultPreview";
|
|
16
17
|
import { GalleryScreenHeader } from "../components/GalleryScreenHeader";
|
|
17
18
|
import { MEDIA_FILTER_OPTIONS, STATUS_FILTER_OPTIONS } from "../../domain/types/creation-filter";
|
|
18
|
-
import { getPreviewUrl } from "../../domain/utils";
|
|
19
19
|
import { createFilterButtons, createItemTitle } from "../utils/filter-buttons.util";
|
|
20
20
|
import type { Creation } from "../../domain/entities/Creation";
|
|
21
21
|
import type { CreationsGalleryScreenProps } from "./creations-gallery.types";
|
|
@@ -37,9 +37,6 @@ export function CreationsGalleryScreen({
|
|
|
37
37
|
getCreationTitle,
|
|
38
38
|
}: CreationsGalleryScreenProps) {
|
|
39
39
|
const tokens = useAppDesignTokens();
|
|
40
|
-
const [selectedCreation, setSelectedCreation] = useState<Creation | null>(null);
|
|
41
|
-
const [showRatingPicker, setShowRatingPicker] = useState(false);
|
|
42
|
-
const hasAutoSelectedRef = useRef(false);
|
|
43
40
|
|
|
44
41
|
const { data: creations, isLoading, refetch } = useCreations({ userId, repository });
|
|
45
42
|
const deleteMutation = useDeleteCreation({ userId, repository });
|
|
@@ -51,15 +48,8 @@ export function CreationsGalleryScreen({
|
|
|
51
48
|
enabled: !!userId && (creations?.length ?? 0) > 0,
|
|
52
49
|
});
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const creation = creations.find((c) => c.id === initialCreationId);
|
|
57
|
-
if (creation) {
|
|
58
|
-
hasAutoSelectedRef.current = true;
|
|
59
|
-
setSelectedCreation(creation);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}, [initialCreationId, creations]);
|
|
51
|
+
// Gallery state management
|
|
52
|
+
const galleryState = useGalleryState({ initialCreationId, creations });
|
|
63
53
|
|
|
64
54
|
const callbacks = useGalleryCallbacks({
|
|
65
55
|
userId,
|
|
@@ -68,9 +58,9 @@ export function CreationsGalleryScreen({
|
|
|
68
58
|
t,
|
|
69
59
|
deleteMutation,
|
|
70
60
|
refetch: async () => { await refetch(); },
|
|
71
|
-
setSelectedCreation,
|
|
72
|
-
setShowRatingPicker,
|
|
73
|
-
selectedCreation,
|
|
61
|
+
setSelectedCreation: galleryState.setSelectedCreation,
|
|
62
|
+
setShowRatingPicker: galleryState.setShowRatingPicker,
|
|
63
|
+
selectedCreation: galleryState.selectedCreation,
|
|
74
64
|
onTryAgain,
|
|
75
65
|
});
|
|
76
66
|
|
|
@@ -145,30 +135,25 @@ export function CreationsGalleryScreen({
|
|
|
145
135
|
/>
|
|
146
136
|
), [isLoading, creations, filters.isFiltered, tokens, t, config, emptyActionLabel, onEmptyAction, filters.clearAllFilters]);
|
|
147
137
|
|
|
148
|
-
const selectedImageUrl = selectedCreation ? (getPreviewUrl(selectedCreation.output) || selectedCreation.uri) : undefined;
|
|
149
|
-
const selectedVideoUrl = selectedCreation?.output?.videoUrl;
|
|
150
|
-
const hasMediaToShow = selectedImageUrl || selectedVideoUrl;
|
|
151
|
-
const showPreview = selectedCreation && hasMediaToShow;
|
|
152
|
-
|
|
153
138
|
const screenHeader = useMemo(() => {
|
|
154
139
|
if (!onBack) return undefined;
|
|
155
140
|
return <GalleryScreenHeader title={t(config.translations.title)} onBack={onBack} />;
|
|
156
141
|
}, [onBack, t, config.translations.title]);
|
|
157
142
|
|
|
158
|
-
if (showPreview) {
|
|
143
|
+
if (galleryState.showPreview && galleryState.selectedCreation) {
|
|
159
144
|
return (
|
|
160
145
|
<GalleryResultPreview
|
|
161
|
-
selectedCreation={selectedCreation}
|
|
162
|
-
imageUrl={selectedVideoUrl ? undefined : selectedImageUrl}
|
|
163
|
-
videoUrl={selectedVideoUrl}
|
|
164
|
-
showRatingPicker={showRatingPicker}
|
|
146
|
+
selectedCreation={galleryState.selectedCreation}
|
|
147
|
+
imageUrl={galleryState.selectedVideoUrl ? undefined : galleryState.selectedImageUrl}
|
|
148
|
+
videoUrl={galleryState.selectedVideoUrl}
|
|
149
|
+
showRatingPicker={galleryState.showRatingPicker}
|
|
165
150
|
config={config}
|
|
166
151
|
t={t}
|
|
167
152
|
onBack={callbacks.handleBack}
|
|
168
153
|
onTryAgain={callbacks.handleTryAgain}
|
|
169
154
|
onRate={callbacks.handleOpenRatingPicker}
|
|
170
155
|
onSubmitRating={callbacks.handleSubmitRating}
|
|
171
|
-
onCloseRating={() => setShowRatingPicker(false)}
|
|
156
|
+
onCloseRating={() => galleryState.setShowRatingPicker(false)}
|
|
172
157
|
/>
|
|
173
158
|
);
|
|
174
159
|
}
|
|
@@ -164,36 +164,5 @@ export class ImagePromptBuilder {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
* Kontext uses instruction-based editing that preserves character identity automatically
|
|
170
|
-
*/
|
|
171
|
-
export function createAnimeSelfiePrompt(customStyle?: string): AnimeSelfiePromptResult {
|
|
172
|
-
const stylePrefix = customStyle ? `${customStyle} anime style` : "anime style";
|
|
173
|
-
|
|
174
|
-
const prompt = [
|
|
175
|
-
`Transform this person into a ${stylePrefix} illustration.`,
|
|
176
|
-
"IMPORTANT: Preserve the exact same gender - if male keep male, if female keep female.",
|
|
177
|
-
"Keep the same face structure, hair color, eye color, skin tone, and facial expression.",
|
|
178
|
-
"Make it look like a high-quality Japanese anime character portrait.",
|
|
179
|
-
"Use vibrant anime colors, clean lineart, and cel-shaded rendering.",
|
|
180
|
-
"Large expressive anime eyes with detailed iris, smooth anime skin with subtle blush.",
|
|
181
|
-
].join(" ");
|
|
182
|
-
|
|
183
|
-
return {
|
|
184
|
-
prompt,
|
|
185
|
-
guidance_scale: 4.0,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Create a style transfer prompt with identity preservation
|
|
191
|
-
*/
|
|
192
|
-
export function createStyleTransferPrompt(style: string): ImagePromptResult {
|
|
193
|
-
return ImagePromptBuilder.create()
|
|
194
|
-
.withSegment(`${style} style`) // Style first
|
|
195
|
-
.withIdentityPreservation()
|
|
196
|
-
.withQuality()
|
|
197
|
-
.withAnatomySafety()
|
|
198
|
-
.build();
|
|
199
|
-
}
|
|
167
|
+
// Re-export prompt creation utilities for backward compatibility
|
|
168
|
+
export { createAnimeSelfiePrompt, createStyleTransferPrompt } from "../utils/prompt-creators.util";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Creation Utilities
|
|
3
|
+
* Provides helper functions for creating specific types of prompts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ImagePromptResult, AnimeSelfiePromptResult } from "../services/ImagePromptBuilder";
|
|
7
|
+
import { ImagePromptBuilder } from "../services/ImagePromptBuilder";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create anime selfie prompt for Kontext model
|
|
11
|
+
* Kontext uses instruction-based editing that preserves character identity automatically
|
|
12
|
+
*/
|
|
13
|
+
export function createAnimeSelfiePrompt(customStyle?: string): AnimeSelfiePromptResult {
|
|
14
|
+
const stylePrefix = customStyle ? `${customStyle} anime style` : "anime style";
|
|
15
|
+
|
|
16
|
+
const prompt = [
|
|
17
|
+
`Transform this person into a ${stylePrefix} illustration.`,
|
|
18
|
+
"IMPORTANT: Preserve the exact same gender - if male keep male, if female keep female.",
|
|
19
|
+
"Keep the same face structure, hair color, eye color, skin tone, and facial expression.",
|
|
20
|
+
"Make it look like a high-quality Japanese anime character portrait.",
|
|
21
|
+
"Use vibrant anime colors, clean lineart, and cel-shaded rendering.",
|
|
22
|
+
"Large expressive anime eyes with detailed iris, smooth anime skin with subtle blush.",
|
|
23
|
+
].join(" ");
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
prompt,
|
|
27
|
+
guidance_scale: 4.0,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a style transfer prompt with identity preservation
|
|
33
|
+
*/
|
|
34
|
+
export function createStyleTransferPrompt(style: string): ImagePromptResult {
|
|
35
|
+
return ImagePromptBuilder.create()
|
|
36
|
+
.withSegment(`${style} style`)
|
|
37
|
+
.withIdentityPreservation()
|
|
38
|
+
.withQuality()
|
|
39
|
+
.withAnatomySafety()
|
|
40
|
+
.build();
|
|
41
|
+
}
|
|
@@ -20,8 +20,8 @@ export {
|
|
|
20
20
|
filterScenariosByCategory,
|
|
21
21
|
getScenarioCategories,
|
|
22
22
|
findScenarioById,
|
|
23
|
-
} from "./infrastructure/scenario-
|
|
24
|
-
export type { AppScenarioConfig } from "./infrastructure/scenario-
|
|
23
|
+
} from "./infrastructure/scenario-utils";
|
|
24
|
+
export type { AppScenarioConfig } from "./infrastructure/scenario-utils";
|
|
25
25
|
|
|
26
26
|
// Scenario Registry - Singleton for app configuration
|
|
27
27
|
export {
|