@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.47.0",
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(
@@ -2,4 +2,8 @@
2
2
  * Domain Repository Interfaces
3
3
  */
4
4
 
5
- export type { ICreationsRepository } from "./ICreationsRepository";
5
+ export type {
6
+ ICreationsRepository,
7
+ CreationsSubscriptionCallback,
8
+ UnsubscribeFunction,
9
+ } from "./ICreationsRepository";
@@ -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 { ICreationsRepository } from "./domain/repositories";
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 { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
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
- * Eliminates boilerplate code in feature screens
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__) console.log("[useCreationPersistence] No userId, skipping");
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, queryClient, type],
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__) console.log("[useCreationPersistence] Missing userId or creationId");
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, queryClient, creditCost, onCreditDeduct],
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__) console.log("[useCreationPersistence] Missing userId or creationId");
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, queryClient],
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 with optimistic update
3
+ * Handles rating of creations
4
+ * Realtime listener handles UI updates automatically
4
5
  */
5
6
 
6
- import { useMutation, useQueryClient } from "@umituz/react-native-design-system";
7
+ import { useState, useCallback } from "react";
7
8
  import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
8
- import type { Creation } from "../../domain/entities/Creation";
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 queryClient = useQueryClient();
25
- const queryKey = ["creations", userId ?? ""];
32
+ }: UseCreationRatingProps): UseCreationRatingReturn {
33
+ const [isPending, setIsPending] = useState(false);
26
34
 
27
- return useMutation({
28
- mutationFn: async ({ id, rating }: RatingVariables) => {
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
- return { previousData };
45
- },
46
- onError: (_error, _variables, context) => {
47
- if (context?.previousData) {
48
- queryClient.setQueryData(queryKey, context.previousData);
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
- onSettled: () => {
52
- void queryClient.invalidateQueries({ queryKey });
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
- * Fetches user's creations from repository
3
+ * Realtime Firestore listener for user's creations
4
+ * Auto-updates UI when Firestore data changes
4
5
  */
5
6
 
6
- import { useQuery } from "@umituz/react-native-design-system";
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 CACHE_CONFIG = {
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
- return useQuery<Creation[]>({
27
- queryKey: ["creations", userId ?? ""],
28
- queryFn: async () => {
29
- if (!userId) {
30
- return [];
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
- return repository.getAll(userId);
33
- },
34
- enabled: !!userId && enabled,
35
- staleTime: CACHE_CONFIG.staleTime,
36
- gcTime: CACHE_CONFIG.gcTime,
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 with optimistic update
3
+ * Handles deletion of user creations
4
+ * Realtime listener handles UI updates automatically
4
5
  */
5
6
 
6
- import { useMutation, useQueryClient } from "@umituz/react-native-design-system";
7
+ import { useState, useCallback } from "react";
7
8
  import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
8
- import type { Creation } from "../../domain/entities/Creation";
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 queryClient = useQueryClient();
20
- const queryKey = ["creations", userId ?? ""];
26
+ }: UseDeleteCreationProps): UseDeleteCreationReturn {
27
+ const [isPending, setIsPending] = useState(false);
21
28
 
22
- return useMutation({
23
- mutationFn: async (creationId: string) => {
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
- return { previousData };
38
- },
39
- onError: (_error: Error, _variables: string, context: { previousData?: Creation[] } | undefined) => {
40
- if (context?.previousData) {
41
- queryClient.setQueryData(queryKey, context.previousData);
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
- onSettled: () => {
45
- void queryClient.invalidateQueries({ queryKey });
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
  }
@@ -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
- promptType: input.promptType,
38
+ skipIdentityPreservation,
36
39
  });
37
40
  }
38
41
 
@@ -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
- /** Prompt type - identity preservation or genetic blend */
23
- readonly promptType?: ScenarioPromptType;
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 identity: Uses createMultiPersonPrompt with @image1, @image2 references
29
- * - Photo-based genetic_blend: Uses createGeneticBlendPrompt for child prediction
30
- * - Text-only: Uses createPhotorealisticPrompt with identity preservation
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, promptType } = options;
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
- // Genetic blend for child prediction scenarios
45
- if (promptType === "genetic_blend") {
46
- return createGeneticBlendPrompt(basePrompt);
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
- * - genetic_blend: Create new face by blending features from multiple inputs (for child prediction)
19
+ * - custom: Use aiPrompt as-is without adding identity preservation (for app-specific scenarios)
20
20
  */
21
- export type ScenarioPromptType = "identity" | "genetic_blend";
21
+ export type ScenarioPromptType = "identity" | "custom";
22
22
 
23
23
  export interface GeneratingMessages {
24
24
  title?: string;