@umituz/react-native-ai-generation-content 1.47.0 → 1.48.1
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/creations/domain/repositories/ICreationsRepository.ts +9 -0
- package/src/domains/creations/domain/repositories/index.ts +5 -1
- package/src/domains/creations/index.ts +5 -1
- package/src/domains/creations/infrastructure/repositories/CreationsFetcher.ts +59 -2
- package/src/domains/creations/infrastructure/repositories/CreationsRepository.ts +13 -1
- package/src/domains/creations/presentation/hooks/useCreationPersistence.ts +19 -48
- package/src/domains/creations/presentation/hooks/useCreationRating.ts +43 -29
- package/src/domains/creations/presentation/hooks/useCreations.ts +63 -18
- package/src/domains/creations/presentation/hooks/useDeleteCreation.ts +42 -27
- package/src/domains/generation/wizard/infrastructure/strategies/image-generation.executor.ts +4 -1
- package/src/domains/generation/wizard/infrastructure/strategies/shared/unified-prompt-builder.ts +10 -14
- package/src/domains/prompts/domain/entities/MultiPersonPromptStructure.ts +0 -56
- package/src/domains/prompts/index.ts +0 -2
- package/src/domains/scenarios/domain/Scenario.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.48.1",
|
|
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",
|
|
@@ -5,8 +5,17 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Creation } from "../entities/Creation";
|
|
7
7
|
|
|
8
|
+
export type CreationsSubscriptionCallback = (creations: Creation[]) => void;
|
|
9
|
+
export type UnsubscribeFunction = () => void;
|
|
10
|
+
|
|
8
11
|
export interface ICreationsRepository {
|
|
9
12
|
getAll(userId: string): Promise<Creation[]>;
|
|
13
|
+
/** Realtime subscription to all creations */
|
|
14
|
+
subscribeToAll(
|
|
15
|
+
userId: string,
|
|
16
|
+
onData: CreationsSubscriptionCallback,
|
|
17
|
+
onError?: (error: Error) => void,
|
|
18
|
+
): UnsubscribeFunction;
|
|
10
19
|
getById(userId: string, id: string): Promise<Creation | null>;
|
|
11
20
|
create(userId: string, creation: Creation): Promise<void>;
|
|
12
21
|
update(
|
|
@@ -94,7 +94,11 @@ export { DEFAULT_TRANSLATIONS, DEFAULT_CONFIG } from "./domain/value-objects";
|
|
|
94
94
|
// DOMAIN LAYER - Repository Interface
|
|
95
95
|
// =============================================================================
|
|
96
96
|
|
|
97
|
-
export type {
|
|
97
|
+
export type {
|
|
98
|
+
ICreationsRepository,
|
|
99
|
+
CreationsSubscriptionCallback,
|
|
100
|
+
UnsubscribeFunction,
|
|
101
|
+
} from "./domain/repositories";
|
|
98
102
|
|
|
99
103
|
// =============================================================================
|
|
100
104
|
// INFRASTRUCTURE LAYER
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { getDocs, getDoc, query, orderBy } from "firebase/firestore";
|
|
1
|
+
import { getDocs, getDoc, query, orderBy, onSnapshot } from "firebase/firestore";
|
|
2
2
|
import { type FirestorePathResolver } from "@umituz/react-native-firebase";
|
|
3
3
|
import type { DocumentMapper } from "../../domain/value-objects/CreationsConfig";
|
|
4
4
|
import type { Creation, CreationDocument } from "../../domain/entities/Creation";
|
|
5
|
+
import type { CreationsSubscriptionCallback, UnsubscribeFunction } from "../../domain/repositories/ICreationsRepository";
|
|
5
6
|
|
|
6
7
|
declare const __DEV__: boolean;
|
|
7
8
|
|
|
@@ -85,10 +86,66 @@ export class CreationsFetcher {
|
|
|
85
86
|
return this.documentMapper(docSnap.id, data);
|
|
86
87
|
} catch (error) {
|
|
87
88
|
if (__DEV__) {
|
|
88
|
-
|
|
89
|
+
|
|
89
90
|
console.error("[CreationsRepository] getById() ERROR", error);
|
|
90
91
|
}
|
|
91
92
|
return null;
|
|
92
93
|
}
|
|
93
94
|
}
|
|
95
|
+
|
|
96
|
+
subscribeToAll(
|
|
97
|
+
userId: string,
|
|
98
|
+
onData: CreationsSubscriptionCallback,
|
|
99
|
+
onError?: (error: Error) => void,
|
|
100
|
+
): UnsubscribeFunction {
|
|
101
|
+
if (__DEV__) {
|
|
102
|
+
console.log("[CreationsFetcher] subscribeToAll()", { userId });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const userCollection = this.pathResolver.getUserCollection(userId);
|
|
106
|
+
if (!userCollection) {
|
|
107
|
+
onData([]);
|
|
108
|
+
return () => {};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const q = query(userCollection, orderBy("createdAt", "desc"));
|
|
112
|
+
|
|
113
|
+
return onSnapshot(
|
|
114
|
+
q,
|
|
115
|
+
(snapshot) => {
|
|
116
|
+
const allCreations = snapshot.docs.map((docSnap) => {
|
|
117
|
+
const data = docSnap.data() as CreationDocument;
|
|
118
|
+
const creation = this.documentMapper(docSnap.id, data);
|
|
119
|
+
|
|
120
|
+
if (creation.deletedAt === undefined && data.deletedAt) {
|
|
121
|
+
const deletedAt =
|
|
122
|
+
data.deletedAt instanceof Date
|
|
123
|
+
? data.deletedAt
|
|
124
|
+
: data.deletedAt &&
|
|
125
|
+
typeof data.deletedAt === "object" &&
|
|
126
|
+
"toDate" in data.deletedAt
|
|
127
|
+
? (data.deletedAt as { toDate: () => Date }).toDate()
|
|
128
|
+
: undefined;
|
|
129
|
+
return { ...creation, deletedAt };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return creation;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const filtered = allCreations.filter((c) => !c.deletedAt);
|
|
136
|
+
|
|
137
|
+
if (__DEV__) {
|
|
138
|
+
console.log("[CreationsFetcher] Realtime update:", filtered.length);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
onData(filtered);
|
|
142
|
+
},
|
|
143
|
+
(error) => {
|
|
144
|
+
if (__DEV__) {
|
|
145
|
+
console.error("[CreationsFetcher] subscribeToAll() ERROR", error);
|
|
146
|
+
}
|
|
147
|
+
onError?.(error);
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
}
|
|
94
151
|
}
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("📍 [LIFECYCLE] CreationsRepository.ts - Module loading");
|
|
3
3
|
|
|
4
4
|
import { BaseRepository, FirestorePathResolver } from "@umituz/react-native-firebase";
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
ICreationsRepository,
|
|
7
|
+
CreationsSubscriptionCallback,
|
|
8
|
+
UnsubscribeFunction,
|
|
9
|
+
} from "../../domain/repositories/ICreationsRepository";
|
|
6
10
|
import type { Creation } from "../../domain/entities/Creation";
|
|
7
11
|
import { mapDocumentToCreation } from "../../domain/entities/Creation";
|
|
8
12
|
import type { DocumentMapper } from "../../domain/value-objects/CreationsConfig";
|
|
@@ -66,6 +70,14 @@ export class CreationsRepository
|
|
|
66
70
|
return this.fetcher.getById(userId, id);
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
subscribeToAll(
|
|
74
|
+
userId: string,
|
|
75
|
+
onData: CreationsSubscriptionCallback,
|
|
76
|
+
onError?: (error: Error) => void,
|
|
77
|
+
): UnsubscribeFunction {
|
|
78
|
+
return this.fetcher.subscribeToAll(userId, onData, onError);
|
|
79
|
+
}
|
|
80
|
+
|
|
69
81
|
async create(userId: string, creation: Creation): Promise<void> {
|
|
70
82
|
return this.writer.create(userId, creation);
|
|
71
83
|
}
|
|
@@ -1,75 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useCreationPersistence Hook
|
|
3
3
|
* Encapsulates Firestore persistence logic for AI generation features
|
|
4
|
-
*
|
|
4
|
+
* Realtime listener handles UI updates automatically
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useCallback, useMemo } from "react";
|
|
8
|
-
import { useQueryClient } from "@umituz/react-native-design-system";
|
|
9
8
|
import { useAuth } from "@umituz/react-native-auth";
|
|
10
9
|
import { createCreationsRepository } from "../../infrastructure/adapters";
|
|
11
10
|
import type { Creation } from "../../domain/entities/Creation";
|
|
12
11
|
|
|
13
12
|
declare const __DEV__: boolean;
|
|
14
13
|
|
|
15
|
-
/**
|
|
16
|
-
* Configuration for creation persistence
|
|
17
|
-
*/
|
|
18
14
|
export interface UseCreationPersistenceConfig {
|
|
19
|
-
/** Creation type identifier (e.g., "anime-selfie", "ai-kiss") */
|
|
20
15
|
readonly type: string;
|
|
21
|
-
/** Collection name in Firestore (defaults to "creations") */
|
|
22
16
|
readonly collectionName?: string;
|
|
23
|
-
/** Credit cost for this feature (passed to onCreditDeduct) */
|
|
24
17
|
readonly creditCost?: number;
|
|
25
|
-
/** Callback to deduct credits on successful processing */
|
|
26
18
|
readonly onCreditDeduct?: (cost: number) => Promise<void | boolean>;
|
|
27
19
|
}
|
|
28
20
|
|
|
29
|
-
/**
|
|
30
|
-
* Base processing start data - all features must have creationId
|
|
31
|
-
*/
|
|
32
21
|
export interface BaseProcessingStartData {
|
|
33
22
|
readonly creationId: string;
|
|
34
23
|
}
|
|
35
24
|
|
|
36
|
-
/**
|
|
37
|
-
* Base processing result - all features should have creationId
|
|
38
|
-
*/
|
|
39
25
|
export interface BaseProcessingResult {
|
|
40
26
|
readonly creationId?: string;
|
|
41
27
|
readonly imageUrl?: string;
|
|
42
28
|
readonly videoUrl?: string;
|
|
43
29
|
}
|
|
44
30
|
|
|
45
|
-
/**
|
|
46
|
-
* Return type for useCreationPersistence - uses generic callbacks
|
|
47
|
-
*/
|
|
48
31
|
export interface UseCreationPersistenceReturn {
|
|
49
32
|
readonly onProcessingStart: <T extends BaseProcessingStartData>(data: T) => void;
|
|
50
33
|
readonly onProcessingComplete: <T extends BaseProcessingResult>(result: T) => void;
|
|
51
34
|
readonly onError: (error: string, creationId?: string) => void;
|
|
52
35
|
}
|
|
53
36
|
|
|
54
|
-
/**
|
|
55
|
-
* Hook that provides Firestore persistence callbacks for AI features
|
|
56
|
-
*
|
|
57
|
-
* @example
|
|
58
|
-
* const { deductCredit } = useDeductCredit({ userId, onCreditsExhausted: openPaywall });
|
|
59
|
-
* const persistence = useCreationPersistence({
|
|
60
|
-
* type: "anime-selfie",
|
|
61
|
-
* creditCost: AI_CREDIT_COST.ANIME_SELFIE,
|
|
62
|
-
* onCreditDeduct: async (cost) => {
|
|
63
|
-
* for (let i = 0; i < cost; i++) await deductCredit("image");
|
|
64
|
-
* },
|
|
65
|
-
* });
|
|
66
|
-
*/
|
|
67
37
|
export function useCreationPersistence(
|
|
68
38
|
config: UseCreationPersistenceConfig,
|
|
69
39
|
): UseCreationPersistenceReturn {
|
|
70
40
|
const { type, collectionName = "creations", creditCost, onCreditDeduct } = config;
|
|
71
41
|
const { userId } = useAuth();
|
|
72
|
-
const queryClient = useQueryClient();
|
|
73
42
|
|
|
74
43
|
const repository = useMemo(
|
|
75
44
|
() => createCreationsRepository(collectionName),
|
|
@@ -78,12 +47,14 @@ export function useCreationPersistence(
|
|
|
78
47
|
|
|
79
48
|
const onProcessingStart = useCallback(
|
|
80
49
|
<T extends BaseProcessingStartData>(data: T) => {
|
|
81
|
-
if (__DEV__) {
|
|
50
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
82
51
|
console.log("[useCreationPersistence] onProcessingStart", { type, userId });
|
|
83
52
|
}
|
|
84
53
|
|
|
85
54
|
if (!userId) {
|
|
86
|
-
if (__DEV__
|
|
55
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
56
|
+
console.log("[useCreationPersistence] No userId, skipping");
|
|
57
|
+
}
|
|
87
58
|
return;
|
|
88
59
|
}
|
|
89
60
|
|
|
@@ -103,19 +74,18 @@ export function useCreationPersistence(
|
|
|
103
74
|
metadata: cleanMetadata,
|
|
104
75
|
};
|
|
105
76
|
|
|
106
|
-
if (__DEV__) {
|
|
77
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
107
78
|
console.log("[useCreationPersistence] Creating document", { creationId, type });
|
|
108
79
|
}
|
|
109
80
|
|
|
110
81
|
repository.create(userId, creation);
|
|
111
|
-
queryClient.invalidateQueries({ queryKey: ["creations"] });
|
|
112
82
|
},
|
|
113
|
-
[userId, repository,
|
|
83
|
+
[userId, repository, type],
|
|
114
84
|
);
|
|
115
85
|
|
|
116
86
|
const onProcessingComplete = useCallback(
|
|
117
87
|
<T extends BaseProcessingResult>(result: T) => {
|
|
118
|
-
if (__DEV__) {
|
|
88
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
119
89
|
console.log("[useCreationPersistence] onProcessingComplete", {
|
|
120
90
|
creationId: result.creationId,
|
|
121
91
|
hasImageUrl: !!result.imageUrl,
|
|
@@ -124,7 +94,9 @@ export function useCreationPersistence(
|
|
|
124
94
|
}
|
|
125
95
|
|
|
126
96
|
if (!userId || !result.creationId) {
|
|
127
|
-
if (__DEV__
|
|
97
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
98
|
+
console.log("[useCreationPersistence] Missing userId or creationId");
|
|
99
|
+
}
|
|
128
100
|
return;
|
|
129
101
|
}
|
|
130
102
|
|
|
@@ -140,31 +112,31 @@ export function useCreationPersistence(
|
|
|
140
112
|
status: "completed",
|
|
141
113
|
output,
|
|
142
114
|
});
|
|
143
|
-
queryClient.invalidateQueries({ queryKey: ["creations"] });
|
|
144
115
|
|
|
145
|
-
// Deduct credits via callback (app provides implementation)
|
|
146
116
|
if (creditCost && creditCost > 0 && onCreditDeduct) {
|
|
147
|
-
if (__DEV__) {
|
|
117
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
148
118
|
console.log("[useCreationPersistence] Deducting credits", { cost: creditCost });
|
|
149
119
|
}
|
|
150
120
|
onCreditDeduct(creditCost).catch((err) => {
|
|
151
|
-
if (__DEV__) {
|
|
121
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
152
122
|
console.error("[useCreationPersistence] Credit deduction failed", err);
|
|
153
123
|
}
|
|
154
124
|
});
|
|
155
125
|
}
|
|
156
126
|
},
|
|
157
|
-
[userId, repository,
|
|
127
|
+
[userId, repository, creditCost, onCreditDeduct],
|
|
158
128
|
);
|
|
159
129
|
|
|
160
130
|
const onError = useCallback(
|
|
161
131
|
(error: string, creationId?: string) => {
|
|
162
|
-
if (__DEV__) {
|
|
132
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
163
133
|
console.log("[useCreationPersistence] onError", { error, creationId });
|
|
164
134
|
}
|
|
165
135
|
|
|
166
136
|
if (!userId || !creationId) {
|
|
167
|
-
if (__DEV__
|
|
137
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
138
|
+
console.log("[useCreationPersistence] Missing userId or creationId");
|
|
139
|
+
}
|
|
168
140
|
return;
|
|
169
141
|
}
|
|
170
142
|
|
|
@@ -172,9 +144,8 @@ export function useCreationPersistence(
|
|
|
172
144
|
status: "failed",
|
|
173
145
|
metadata: { error },
|
|
174
146
|
});
|
|
175
|
-
queryClient.invalidateQueries({ queryKey: ["creations"] });
|
|
176
147
|
},
|
|
177
|
-
[userId, repository
|
|
148
|
+
[userId, repository],
|
|
178
149
|
);
|
|
179
150
|
|
|
180
151
|
return useMemo(
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useCreationRating Hook
|
|
3
|
-
* Handles rating of creations
|
|
3
|
+
* Handles rating of creations
|
|
4
|
+
* Realtime listener handles UI updates automatically
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
+
import { useState, useCallback } from "react";
|
|
7
8
|
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
declare const __DEV__: boolean;
|
|
9
11
|
|
|
10
12
|
interface UseCreationRatingProps {
|
|
11
13
|
readonly userId: string | null;
|
|
@@ -15,41 +17,53 @@ interface UseCreationRatingProps {
|
|
|
15
17
|
interface RatingVariables {
|
|
16
18
|
readonly id: string;
|
|
17
19
|
readonly rating: number;
|
|
20
|
+
readonly description?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface UseCreationRatingReturn {
|
|
24
|
+
readonly mutate: (variables: RatingVariables) => void;
|
|
25
|
+
readonly mutateAsync: (variables: RatingVariables) => Promise<boolean>;
|
|
26
|
+
readonly isPending: boolean;
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
export function useCreationRating({
|
|
21
30
|
userId,
|
|
22
31
|
repository,
|
|
23
|
-
}: UseCreationRatingProps) {
|
|
24
|
-
const
|
|
25
|
-
const queryKey = ["creations", userId ?? ""];
|
|
32
|
+
}: UseCreationRatingProps): UseCreationRatingReturn {
|
|
33
|
+
const [isPending, setIsPending] = useState(false);
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
const mutateAsync = useCallback(
|
|
36
|
+
async ({ id, rating, description }: RatingVariables): Promise<boolean> => {
|
|
29
37
|
if (!userId) return false;
|
|
30
|
-
return repository.rate(userId, id, rating);
|
|
31
|
-
},
|
|
32
|
-
onMutate: async ({ id, rating }: RatingVariables) => {
|
|
33
|
-
await queryClient.cancelQueries({ queryKey });
|
|
34
|
-
const previousData = queryClient.getQueryData<Creation[]>(queryKey);
|
|
35
|
-
|
|
36
|
-
if (previousData) {
|
|
37
|
-
queryClient.setQueryData<Creation[]>(queryKey, (old) =>
|
|
38
|
-
old?.map((c) =>
|
|
39
|
-
c.id === id ? { ...c, rating, ratedAt: new Date() } : c
|
|
40
|
-
) ?? []
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
setIsPending(true);
|
|
40
|
+
try {
|
|
41
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
42
|
+
console.log("[useCreationRating] Rating:", { id, rating });
|
|
43
|
+
}
|
|
44
|
+
const result = await repository.rate(userId, id, rating, description);
|
|
45
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
46
|
+
console.log("[useCreationRating] Rate result:", result);
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
51
|
+
console.error("[useCreationRating] Error:", error);
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
} finally {
|
|
55
|
+
setIsPending(false);
|
|
49
56
|
}
|
|
50
57
|
},
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
[userId, repository],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const mutate = useCallback(
|
|
62
|
+
(variables: RatingVariables): void => {
|
|
63
|
+
void mutateAsync(variables);
|
|
53
64
|
},
|
|
54
|
-
|
|
65
|
+
[mutateAsync],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return { mutate, mutateAsync, isPending };
|
|
55
69
|
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useCreations Hook
|
|
3
|
-
*
|
|
3
|
+
* Realtime Firestore listener for user's creations
|
|
4
|
+
* Auto-updates UI when Firestore data changes
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
+
import { useState, useEffect, useCallback } from "react";
|
|
7
8
|
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
8
9
|
import type { Creation } from "../../domain/entities/Creation";
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
-
staleTime: 5 * 60 * 1000, // 5 minutes - use cache invalidation on mutations
|
|
12
|
-
gcTime: 30 * 60 * 1000,
|
|
13
|
-
};
|
|
11
|
+
declare const __DEV__: boolean;
|
|
14
12
|
|
|
15
13
|
interface UseCreationsProps {
|
|
16
14
|
readonly userId: string | null;
|
|
@@ -18,21 +16,68 @@ interface UseCreationsProps {
|
|
|
18
16
|
readonly enabled?: boolean;
|
|
19
17
|
}
|
|
20
18
|
|
|
19
|
+
interface UseCreationsReturn {
|
|
20
|
+
readonly data: Creation[] | undefined;
|
|
21
|
+
readonly isLoading: boolean;
|
|
22
|
+
readonly error: Error | null;
|
|
23
|
+
readonly refetch: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
export function useCreations({
|
|
22
27
|
userId,
|
|
23
28
|
repository,
|
|
24
29
|
enabled = true,
|
|
25
|
-
}: UseCreationsProps) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
}: UseCreationsProps): UseCreationsReturn {
|
|
31
|
+
const [data, setData] = useState<Creation[] | undefined>(undefined);
|
|
32
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
33
|
+
const [error, setError] = useState<Error | null>(null);
|
|
34
|
+
|
|
35
|
+
const refetch = useCallback(() => {
|
|
36
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
37
|
+
console.log("[useCreations] refetch() - realtime listener handles updates");
|
|
38
|
+
}
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!userId || !enabled) {
|
|
43
|
+
setData([]);
|
|
44
|
+
setIsLoading(false);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
49
|
+
console.log("[useCreations] Setting up realtime listener", { userId });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setIsLoading(true);
|
|
53
|
+
setError(null);
|
|
54
|
+
|
|
55
|
+
const unsubscribe = repository.subscribeToAll(
|
|
56
|
+
userId,
|
|
57
|
+
(creations) => {
|
|
58
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
59
|
+
console.log("[useCreations] Realtime update:", creations.length);
|
|
60
|
+
}
|
|
61
|
+
setData(creations);
|
|
62
|
+
setIsLoading(false);
|
|
63
|
+
setError(null);
|
|
64
|
+
},
|
|
65
|
+
(err) => {
|
|
66
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
67
|
+
console.error("[useCreations] Realtime listener error:", err);
|
|
68
|
+
}
|
|
69
|
+
setError(err);
|
|
70
|
+
setIsLoading(false);
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
76
|
+
console.log("[useCreations] Cleaning up realtime listener");
|
|
31
77
|
}
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
78
|
+
unsubscribe();
|
|
79
|
+
};
|
|
80
|
+
}, [userId, repository, enabled]);
|
|
81
|
+
|
|
82
|
+
return { data, isLoading, error, refetch };
|
|
38
83
|
}
|
|
@@ -1,48 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useDeleteCreation Hook
|
|
3
|
-
* Handles deletion of user creations
|
|
3
|
+
* Handles deletion of user creations
|
|
4
|
+
* Realtime listener handles UI updates automatically
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
+
import { useState, useCallback } from "react";
|
|
7
8
|
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
declare const __DEV__: boolean;
|
|
9
11
|
|
|
10
12
|
interface UseDeleteCreationProps {
|
|
11
13
|
readonly userId: string | null;
|
|
12
14
|
readonly repository: ICreationsRepository;
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
interface UseDeleteCreationReturn {
|
|
18
|
+
readonly mutate: (creationId: string) => void;
|
|
19
|
+
readonly mutateAsync: (creationId: string) => Promise<boolean>;
|
|
20
|
+
readonly isPending: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
export function useDeleteCreation({
|
|
16
24
|
userId,
|
|
17
25
|
repository,
|
|
18
|
-
}: UseDeleteCreationProps) {
|
|
19
|
-
const
|
|
20
|
-
const queryKey = ["creations", userId ?? ""];
|
|
26
|
+
}: UseDeleteCreationProps): UseDeleteCreationReturn {
|
|
27
|
+
const [isPending, setIsPending] = useState(false);
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
const mutateAsync = useCallback(
|
|
30
|
+
async (creationId: string): Promise<boolean> => {
|
|
24
31
|
if (!userId) return false;
|
|
25
|
-
return repository.delete(userId, creationId);
|
|
26
|
-
},
|
|
27
|
-
onMutate: async (creationId: string) => {
|
|
28
|
-
await queryClient.cancelQueries({ queryKey });
|
|
29
|
-
const previousData = queryClient.getQueryData<Creation[]>(queryKey);
|
|
30
|
-
|
|
31
|
-
if (previousData) {
|
|
32
|
-
queryClient.setQueryData<Creation[]>(queryKey, (old: Creation[] | undefined) =>
|
|
33
|
-
old?.filter((c: Creation) => c.id !== creationId) ?? []
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
setIsPending(true);
|
|
34
|
+
try {
|
|
35
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
36
|
+
console.log("[useDeleteCreation] Deleting:", creationId);
|
|
37
|
+
}
|
|
38
|
+
const result = await repository.delete(userId, creationId);
|
|
39
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
40
|
+
console.log("[useDeleteCreation] Delete result:", result);
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
45
|
+
console.error("[useDeleteCreation] Error:", error);
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
} finally {
|
|
49
|
+
setIsPending(false);
|
|
42
50
|
}
|
|
43
51
|
},
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
[userId, repository],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const mutate = useCallback(
|
|
56
|
+
(creationId: string): void => {
|
|
57
|
+
void mutateAsync(creationId);
|
|
46
58
|
},
|
|
47
|
-
|
|
59
|
+
[mutateAsync],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return { mutate, mutateAsync, isPending };
|
|
48
63
|
}
|
package/src/domains/generation/wizard/infrastructure/strategies/image-generation.executor.ts
CHANGED
|
@@ -28,11 +28,14 @@ function buildFinalPrompt(input: WizardImageInput, imageUrls: string[]): string
|
|
|
28
28
|
const hasPhotos = imageUrls.length > 0;
|
|
29
29
|
|
|
30
30
|
if (hasPhotos) {
|
|
31
|
+
// Custom prompt type means app provides complete prompt - skip identity preservation
|
|
32
|
+
const skipIdentityPreservation = input.promptType === "custom";
|
|
33
|
+
|
|
31
34
|
return buildUnifiedPrompt({
|
|
32
35
|
basePrompt: input.prompt,
|
|
33
36
|
photoCount: imageUrls.length,
|
|
34
37
|
interactionStyle: input.interactionStyle,
|
|
35
|
-
|
|
38
|
+
skipIdentityPreservation,
|
|
36
39
|
});
|
|
37
40
|
}
|
|
38
41
|
|
package/src/domains/generation/wizard/infrastructure/strategies/shared/unified-prompt-builder.ts
CHANGED
|
@@ -5,12 +5,8 @@
|
|
|
5
5
|
* Uses createPhotorealisticPrompt for text-only scenarios
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
createMultiPersonPrompt,
|
|
10
|
-
createGeneticBlendPrompt,
|
|
11
|
-
} from "../../../../../prompts/domain/entities/MultiPersonPromptStructure";
|
|
8
|
+
import { createMultiPersonPrompt } from "../../../../../prompts/domain/entities/MultiPersonPromptStructure";
|
|
12
9
|
import { createPhotorealisticPrompt } from "../../../../../prompts/domain/entities/BasePromptStructure";
|
|
13
|
-
import type { ScenarioPromptType } from "../../../../../scenarios/domain/Scenario";
|
|
14
10
|
|
|
15
11
|
export interface BuildPromptOptions {
|
|
16
12
|
/** Base scenario prompt (aiPrompt from scenario config) */
|
|
@@ -19,18 +15,18 @@ export interface BuildPromptOptions {
|
|
|
19
15
|
readonly photoCount: number;
|
|
20
16
|
/** Interaction style from scenario (optional - only if scenario specifies it) */
|
|
21
17
|
readonly interactionStyle?: string;
|
|
22
|
-
/**
|
|
23
|
-
readonly
|
|
18
|
+
/** Skip identity preservation (for custom prompts like genetic blend) */
|
|
19
|
+
readonly skipIdentityPreservation?: boolean;
|
|
24
20
|
}
|
|
25
21
|
|
|
26
22
|
/**
|
|
27
23
|
* Build unified prompt for any generation type
|
|
28
|
-
* - Photo-based
|
|
29
|
-
* -
|
|
30
|
-
* -
|
|
24
|
+
* - Photo-based: Uses createMultiPersonPrompt with @image1, @image2 references
|
|
25
|
+
* - Text-only: Uses createPhotorealisticPrompt
|
|
26
|
+
* - Custom: Uses basePrompt directly when skipIdentityPreservation is true
|
|
31
27
|
*/
|
|
32
28
|
export function buildUnifiedPrompt(options: BuildPromptOptions): string {
|
|
33
|
-
const { basePrompt, photoCount, interactionStyle,
|
|
29
|
+
const { basePrompt, photoCount, interactionStyle, skipIdentityPreservation } = options;
|
|
34
30
|
|
|
35
31
|
// Text-only generation (no photos)
|
|
36
32
|
if (photoCount === 0) {
|
|
@@ -41,9 +37,9 @@ export function buildUnifiedPrompt(options: BuildPromptOptions): string {
|
|
|
41
37
|
});
|
|
42
38
|
}
|
|
43
39
|
|
|
44
|
-
//
|
|
45
|
-
if (
|
|
46
|
-
return
|
|
40
|
+
// Custom prompt handling (app provides complete prompt)
|
|
41
|
+
if (skipIdentityPreservation) {
|
|
42
|
+
return basePrompt;
|
|
47
43
|
}
|
|
48
44
|
|
|
49
45
|
// Default: Photo-based generation with identity preservation
|
|
@@ -22,28 +22,6 @@ export const MULTI_PERSON_PRESERVATION_RULES: MultiPersonPreservationRules = {
|
|
|
22
22
|
positioning: "Natural positioning, all looking at camera with natural expressions",
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
/**
|
|
26
|
-
* Genetic blend rules for child prediction scenarios
|
|
27
|
-
* Creates a new face by blending features from parent photos
|
|
28
|
-
* Optimized for FAL AI / Nano Banana Edit semantic understanding
|
|
29
|
-
*/
|
|
30
|
-
export const GENETIC_BLEND_RULES = {
|
|
31
|
-
requirement: "Create a COMPLETELY NEW child face by intelligently blending genetic features from both parents",
|
|
32
|
-
blendingRules: [
|
|
33
|
-
"Extract and analyze facial genetics from parent 1 (eye color, face shape, skin tone, hair color)",
|
|
34
|
-
"Extract and analyze facial genetics from parent 2 (eye color, face shape, skin tone, hair color)",
|
|
35
|
-
"Generate a NEW child face that naturally combines inherited traits from BOTH parents",
|
|
36
|
-
"The child must look like a realistic biological offspring - not a copy of either parent",
|
|
37
|
-
"Apply realistic child facial proportions (larger eyes, rounder cheeks, smaller nose)",
|
|
38
|
-
],
|
|
39
|
-
forbidden: [
|
|
40
|
-
"NEVER show or copy either parent's face in the output",
|
|
41
|
-
"NEVER use parent photos directly - only extract genetic features for blending",
|
|
42
|
-
"Do NOT create an adult face - maintain child proportions",
|
|
43
|
-
"Do NOT favor one parent over the other - blend features equally",
|
|
44
|
-
],
|
|
45
|
-
};
|
|
46
|
-
|
|
47
25
|
/**
|
|
48
26
|
* Creates a multi-person prompt dynamically
|
|
49
27
|
*
|
|
@@ -78,37 +56,3 @@ ${NATURAL_POSE_GUIDELINES}
|
|
|
78
56
|
SCENARIO DESCRIPTION:
|
|
79
57
|
${scenarioPrompt}`;
|
|
80
58
|
};
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Creates a genetic blend prompt for child prediction scenarios
|
|
84
|
-
* Instead of preserving identities, it blends parent features to create a child
|
|
85
|
-
* Optimized for FAL AI Nano Banana Edit's semantic understanding
|
|
86
|
-
*
|
|
87
|
-
* @param scenarioPrompt - The scenario description
|
|
88
|
-
* @returns Complete prompt with genetic blending instructions
|
|
89
|
-
*/
|
|
90
|
-
export const createGeneticBlendPrompt = (scenarioPrompt: string): string => {
|
|
91
|
-
return `GENETIC CHILD PREDICTION - CRITICAL INSTRUCTIONS:
|
|
92
|
-
|
|
93
|
-
You are creating a PREDICTION of what a child would look like based on two parent reference images.
|
|
94
|
-
|
|
95
|
-
IMPORTANT: This is NOT a face swap or identity preservation task.
|
|
96
|
-
- The parent photos are ONLY for extracting genetic traits (eye color, face shape, skin tone, hair)
|
|
97
|
-
- You must CREATE a completely NEW child face that combines features from BOTH parents
|
|
98
|
-
- The output should show ONLY the child - never show or copy the parent faces
|
|
99
|
-
|
|
100
|
-
GENETIC EXTRACTION FROM REFERENCE IMAGES:
|
|
101
|
-
- From reference image 1: Extract eye color, face shape, skin tone, hair color/texture
|
|
102
|
-
- From reference image 2: Extract eye color, face shape, skin tone, hair color/texture
|
|
103
|
-
|
|
104
|
-
CHILD GENERATION RULES:
|
|
105
|
-
${GENETIC_BLEND_RULES.blendingRules.map(rule => `- ${rule}`).join("\n")}
|
|
106
|
-
|
|
107
|
-
STRICTLY FORBIDDEN:
|
|
108
|
-
${GENETIC_BLEND_RULES.forbidden.map(rule => `- ${rule}`).join("\n")}
|
|
109
|
-
|
|
110
|
-
${PHOTOREALISTIC_RENDERING}
|
|
111
|
-
|
|
112
|
-
SCENARIO TO GENERATE:
|
|
113
|
-
${scenarioPrompt}`;
|
|
114
|
-
};
|
|
@@ -52,9 +52,7 @@ export type { CreatePromptOptions } from './domain/entities/BasePromptStructure'
|
|
|
52
52
|
|
|
53
53
|
export {
|
|
54
54
|
MULTI_PERSON_PRESERVATION_RULES,
|
|
55
|
-
GENETIC_BLEND_RULES,
|
|
56
55
|
createMultiPersonPrompt,
|
|
57
|
-
createGeneticBlendPrompt,
|
|
58
56
|
} from './domain/entities/MultiPersonPromptStructure';
|
|
59
57
|
export type { MultiPersonPreservationRules } from './domain/entities/MultiPersonPromptStructure';
|
|
60
58
|
|
|
@@ -16,9 +16,9 @@ export type ScenarioInputType = "single" | "dual" | "text";
|
|
|
16
16
|
/**
|
|
17
17
|
* Prompt type determines how multi-person prompts are built
|
|
18
18
|
* - identity: Preserve exact facial features from input photos (default)
|
|
19
|
-
* -
|
|
19
|
+
* - custom: Use aiPrompt as-is without adding identity preservation (for app-specific scenarios)
|
|
20
20
|
*/
|
|
21
|
-
export type ScenarioPromptType = "identity" | "
|
|
21
|
+
export type ScenarioPromptType = "identity" | "custom";
|
|
22
22
|
|
|
23
23
|
export interface GeneratingMessages {
|
|
24
24
|
title?: string;
|