@umituz/react-native-ai-generation-content 1.88.3 → 1.88.5
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/presentation/screens/CreationsGalleryScreen.tsx +52 -40
- package/src/domains/creations/presentation/screens/creations-gallery.styles.ts +8 -0
- package/src/domains/generation/wizard/presentation/screens/GeneratingScreen.tsx +51 -11
- package/src/infrastructure/utils/couple-input.util.ts +51 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.88.
|
|
3
|
+
"version": "1.88.5",
|
|
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",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useMemo, useCallback, useState } from "react";
|
|
2
|
-
import { View } from "react-native";
|
|
2
|
+
import { View, FlatList } from "react-native";
|
|
3
3
|
import { ScreenLayout } from "@umituz/react-native-design-system/layouts";
|
|
4
4
|
import { FilterSheet, useAppFocusEffect } from "@umituz/react-native-design-system/molecules";
|
|
5
5
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
@@ -101,37 +101,42 @@ export function CreationsGalleryScreen({
|
|
|
101
101
|
onPostToFeed: onShareToFeed ? () => onShareToFeed(item) : undefined,
|
|
102
102
|
}), [callbacks, onCreationPress, onShareToFeed]);
|
|
103
103
|
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
canPostToFeed={!!onShareToFeed && item.status === "completed"}
|
|
110
|
-
/>
|
|
111
|
-
), [getItemTitle, getItemCallbacks, onShareToFeed]);
|
|
104
|
+
const [pageLimit, setPageLimit] = useState(6);
|
|
105
|
+
|
|
106
|
+
const paginatedCreations = useMemo(() => {
|
|
107
|
+
return filters.filtered.slice(0, pageLimit);
|
|
108
|
+
}, [filters.filtered, pageLimit]);
|
|
112
109
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
110
|
+
const handleLoadMore = useCallback(() => {
|
|
111
|
+
if (pageLimit < filters.filtered.length) {
|
|
112
|
+
if (__DEV__) console.log("[CreationsGallery] Loading more...", { current: pageLimit, total: filters.filtered.length });
|
|
113
|
+
setPageLimit(prev => prev + 3);
|
|
117
114
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
{row.right ? (
|
|
115
|
+
}, [pageLimit, filters.filtered.length]);
|
|
116
|
+
|
|
117
|
+
const renderItem = useCallback(({ item }: { item: Creation }) => {
|
|
118
|
+
if (viewMode === "grid") {
|
|
119
|
+
return (
|
|
120
|
+
<View style={styles.gridItemWrapper}>
|
|
125
121
|
<CreationCardCompact
|
|
126
|
-
creation={
|
|
127
|
-
callbacks={{ onPress: getItemCallbacks(
|
|
122
|
+
creation={item}
|
|
123
|
+
callbacks={{ onPress: getItemCallbacks(item).onPress }}
|
|
128
124
|
/>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
125
|
+
</View>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<View style={styles.listItemWrapper}>
|
|
131
|
+
<CreationCard
|
|
132
|
+
creation={item}
|
|
133
|
+
titleText={getItemTitle(item)}
|
|
134
|
+
callbacks={getItemCallbacks(item)}
|
|
135
|
+
canPostToFeed={!!onShareToFeed && item.status === "completed"}
|
|
136
|
+
/>
|
|
132
137
|
</View>
|
|
133
|
-
)
|
|
134
|
-
}, [getItemCallbacks]);
|
|
138
|
+
);
|
|
139
|
+
}, [viewMode, getItemTitle, getItemCallbacks, onShareToFeed]);
|
|
135
140
|
|
|
136
141
|
const hasScreenHeader = Boolean(onBack);
|
|
137
142
|
|
|
@@ -147,7 +152,10 @@ export function CreationsGalleryScreen({
|
|
|
147
152
|
showFilter={showFilter}
|
|
148
153
|
filterButtons={filterButtons}
|
|
149
154
|
viewMode={viewMode}
|
|
150
|
-
onViewModeChange={
|
|
155
|
+
onViewModeChange={(mode) => {
|
|
156
|
+
setPageLimit(6); // Reset pagination on mode change
|
|
157
|
+
setViewMode(mode);
|
|
158
|
+
}}
|
|
151
159
|
/>
|
|
152
160
|
</View>
|
|
153
161
|
);
|
|
@@ -195,24 +203,28 @@ export function CreationsGalleryScreen({
|
|
|
195
203
|
}
|
|
196
204
|
|
|
197
205
|
return (
|
|
198
|
-
<ScreenLayout header={screenHeader}>
|
|
206
|
+
<ScreenLayout header={screenHeader} scrollable={false}>
|
|
199
207
|
{renderHeader}
|
|
200
208
|
{filters.filtered.length === 0 ? (
|
|
201
209
|
<View style={[styles.listContent, styles.emptyContent]}>
|
|
202
210
|
{renderEmpty}
|
|
203
211
|
</View>
|
|
204
|
-
) : viewMode === "grid" ? (
|
|
205
|
-
<View style={styles.gridContent}>
|
|
206
|
-
{renderGridItems(filters.filtered)}
|
|
207
|
-
</View>
|
|
208
212
|
) : (
|
|
209
|
-
<
|
|
210
|
-
{
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
<FlatList
|
|
214
|
+
key={viewMode}
|
|
215
|
+
data={paginatedCreations}
|
|
216
|
+
renderItem={renderItem}
|
|
217
|
+
keyExtractor={(item) => item.id}
|
|
218
|
+
numColumns={viewMode === "grid" ? 2 : 1}
|
|
219
|
+
contentContainerStyle={viewMode === "grid" ? styles.gridContent : styles.listContent}
|
|
220
|
+
onEndReached={handleLoadMore}
|
|
221
|
+
onEndReachedThreshold={0.5}
|
|
222
|
+
initialNumToRender={3}
|
|
223
|
+
maxToRenderPerBatch={3}
|
|
224
|
+
windowSize={5}
|
|
225
|
+
removeClippedSubviews={true}
|
|
226
|
+
showsVerticalScrollIndicator={false}
|
|
227
|
+
/>
|
|
216
228
|
)}
|
|
217
229
|
<FilterSheet visible={filters.statusFilterVisible} onClose={filters.closeStatusFilter} options={filters.statusFilter.filterOptions} selectedIds={[filters.statusFilter.selectedId]} onFilterPress={filters.statusFilter.selectFilter} onClearFilters={filters.statusFilter.clearFilter} title={t(config.translations.statusFilterTitle ?? "creations.filter.status")} clearLabel={t(config.translations.clearFilter ?? "common.clear")} />
|
|
218
230
|
<FilterSheet visible={filters.mediaFilterVisible} onClose={filters.closeMediaFilter} options={filters.mediaFilter.filterOptions} selectedIds={[filters.mediaFilter.selectedId]} onFilterPress={filters.mediaFilter.selectFilter} onClearFilters={filters.mediaFilter.clearFilter} title={t(config.translations.mediaFilterTitle ?? "creations.filter.media")} clearLabel={t(config.translations.clearFilter ?? "common.clear")} />
|
|
@@ -17,6 +17,14 @@ export const creationsGalleryStyles = StyleSheet.create({
|
|
|
17
17
|
gridPlaceholder: {
|
|
18
18
|
flex: 1,
|
|
19
19
|
},
|
|
20
|
+
gridItemWrapper: {
|
|
21
|
+
flex: 1 / 2,
|
|
22
|
+
padding: 4,
|
|
23
|
+
},
|
|
24
|
+
listItemWrapper: {
|
|
25
|
+
width: "100%",
|
|
26
|
+
marginBottom: 16,
|
|
27
|
+
},
|
|
20
28
|
screenHeader: {
|
|
21
29
|
flexDirection: "row",
|
|
22
30
|
alignItems: "center",
|
|
@@ -52,16 +52,23 @@ export const GeneratingScreen: React.FC<GeneratingScreenProps> = ({
|
|
|
52
52
|
const phase = useGenerationPhase();
|
|
53
53
|
const pulseAnim = useRef(new Animated.Value(0.6)).current;
|
|
54
54
|
|
|
55
|
+
const scanAnim = useRef(new Animated.Value(0)).current;
|
|
56
|
+
|
|
55
57
|
useEffect(() => {
|
|
56
58
|
const animation = Animated.loop(
|
|
57
59
|
Animated.sequence([
|
|
58
|
-
Animated.timing(
|
|
59
|
-
Animated.timing(
|
|
60
|
+
Animated.timing(scanAnim, { toValue: 1, duration: 2500, useNativeDriver: true }),
|
|
61
|
+
Animated.timing(scanAnim, { toValue: 0, duration: 2500, useNativeDriver: true }),
|
|
60
62
|
]),
|
|
61
63
|
);
|
|
62
64
|
animation.start();
|
|
63
65
|
return () => animation.stop();
|
|
64
|
-
}, [
|
|
66
|
+
}, [scanAnim]);
|
|
67
|
+
|
|
68
|
+
const scanTranslateY = scanAnim.interpolate({
|
|
69
|
+
inputRange: [0, 1],
|
|
70
|
+
outputRange: [-100, 100],
|
|
71
|
+
});
|
|
65
72
|
|
|
66
73
|
const messages = useMemo(() => {
|
|
67
74
|
const custom = scenario?.generatingMessages;
|
|
@@ -92,10 +99,26 @@ export const GeneratingScreen: React.FC<GeneratingScreenProps> = ({
|
|
|
92
99
|
<ScreenLayout backgroundColor={tokens.colors.backgroundPrimary}>
|
|
93
100
|
<View style={styles.container}>
|
|
94
101
|
<View style={styles.content}>
|
|
95
|
-
{/*
|
|
96
|
-
<
|
|
97
|
-
<
|
|
98
|
-
|
|
102
|
+
{/* Scanning Animation Container */}
|
|
103
|
+
<View style={[styles.scanFrame, { borderColor: tokens.colors.primary + "30" }]}>
|
|
104
|
+
<View style={[styles.scanOverlay, { backgroundColor: tokens.colors.primary + "10" }]}>
|
|
105
|
+
<Animated.View
|
|
106
|
+
style={[
|
|
107
|
+
styles.scanLine,
|
|
108
|
+
{
|
|
109
|
+
backgroundColor: tokens.colors.primary,
|
|
110
|
+
transform: [{ translateY: scanTranslateY }],
|
|
111
|
+
shadowColor: tokens.colors.primary,
|
|
112
|
+
shadowOpacity: 0.8,
|
|
113
|
+
shadowRadius: 10,
|
|
114
|
+
}
|
|
115
|
+
]}
|
|
116
|
+
/>
|
|
117
|
+
</View>
|
|
118
|
+
<Animated.View style={[styles.spinnerOverlay, { opacity: pulseAnim }]}>
|
|
119
|
+
<ActivityIndicator size="large" color={tokens.colors.primary} />
|
|
120
|
+
</Animated.View>
|
|
121
|
+
</View>
|
|
99
122
|
|
|
100
123
|
<AtomicText type="headlineMedium" style={styles.title}>
|
|
101
124
|
{messages.title}
|
|
@@ -197,14 +220,31 @@ const styles = StyleSheet.create({
|
|
|
197
220
|
alignItems: "center",
|
|
198
221
|
gap: 16,
|
|
199
222
|
},
|
|
200
|
-
|
|
201
|
-
width:
|
|
202
|
-
height:
|
|
203
|
-
borderRadius:
|
|
223
|
+
scanFrame: {
|
|
224
|
+
width: 140,
|
|
225
|
+
height: 180,
|
|
226
|
+
borderRadius: 24,
|
|
227
|
+
borderWidth: 2,
|
|
228
|
+
overflow: "hidden",
|
|
204
229
|
justifyContent: "center",
|
|
205
230
|
alignItems: "center",
|
|
206
231
|
marginBottom: 8,
|
|
207
232
|
},
|
|
233
|
+
scanOverlay: {
|
|
234
|
+
...StyleSheet.absoluteFillObject,
|
|
235
|
+
justifyContent: "center",
|
|
236
|
+
alignItems: "center",
|
|
237
|
+
},
|
|
238
|
+
scanLine: {
|
|
239
|
+
width: "120%",
|
|
240
|
+
height: 3,
|
|
241
|
+
position: "absolute",
|
|
242
|
+
left: "-10%",
|
|
243
|
+
elevation: 4,
|
|
244
|
+
},
|
|
245
|
+
spinnerOverlay: {
|
|
246
|
+
zIndex: 10,
|
|
247
|
+
},
|
|
208
248
|
title: {
|
|
209
249
|
textAlign: "center",
|
|
210
250
|
},
|
|
@@ -43,19 +43,59 @@ export function prependContext(
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* Refines a prompt for couple mode by
|
|
47
|
-
* Primarily used to fix "baked" prompts that were generated with
|
|
46
|
+
* Refines a prompt for couple or solo mode by adjusting plurals and keywords.
|
|
47
|
+
* Primarily used to fix "baked" prompts that were generated with a specific mode in mind.
|
|
48
48
|
*/
|
|
49
49
|
export function refinePromptForCouple(prompt: string, isCouple: boolean): string {
|
|
50
|
-
if (
|
|
50
|
+
if (isCouple) {
|
|
51
|
+
// Ensure couple context is present
|
|
52
|
+
let refined = prompt
|
|
53
|
+
.replace(/\bthe person must be\b/gi, "both people must be")
|
|
54
|
+
.replace(/\bthe person is\b/gi, "both people are")
|
|
55
|
+
.replace(/\bkeep every facial feature\b/gi, "keep every facial feature for both people")
|
|
56
|
+
.replace(/\bthe person\b/gi, "both people")
|
|
57
|
+
.replace(/\bfacial feature\b/gi, "facial features")
|
|
58
|
+
.replace(/\bidentical\b/gi, "identical for both individuals")
|
|
59
|
+
.replace(/\binstantly recognizable\b/gi, "instantly recognizable as themselves");
|
|
51
60
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
// If it doesn't already have couple-hints, add them at the start
|
|
62
|
+
if (!/\b(couple|both|matching|dual|two people)\b/i.test(refined)) {
|
|
63
|
+
refined = `A photo of a couple, ${refined}`;
|
|
64
|
+
}
|
|
65
|
+
return refined;
|
|
66
|
+
} else {
|
|
67
|
+
// Solo mode: AGGRESSIVELY remove plural references to prevent "ghost" people
|
|
68
|
+
return prompt
|
|
69
|
+
// Instead of removing the clothing descriptions, just remove the gender labels
|
|
70
|
+
// so the AI can apply the relevant clothing to the single person it sees.
|
|
71
|
+
.replace(/\sfor women\b/gi, "")
|
|
72
|
+
.replace(/\sfor men\b/gi, "")
|
|
73
|
+
// Remove collective adjectives
|
|
74
|
+
.replace(/\bmatching\b/gi, "")
|
|
75
|
+
.replace(/\bcoordinated\b/gi, "")
|
|
76
|
+
.replace(/\bcomplementary\b/gi, "")
|
|
77
|
+
.replace(/\bcoordinating\b/gi, "")
|
|
78
|
+
.replace(/\bcouple\b/gi, "person")
|
|
79
|
+
.replace(/\bduo\b/gi, "person")
|
|
80
|
+
.replace(/\bpair\b/gi, "person")
|
|
81
|
+
.replace(/\bdoubles\b/gi, "")
|
|
82
|
+
.replace(/\bboth\b/gi, "the person")
|
|
83
|
+
.replace(/\bthey are\b/gi, "the person is")
|
|
84
|
+
.replace(/\btheir\b/gi, "the person's")
|
|
85
|
+
// Force singular clothing
|
|
86
|
+
.replace(/\bjackets\b/gi, "jacket")
|
|
87
|
+
.replace(/\boutfits\b/gi, "outfit")
|
|
88
|
+
.replace(/\bsuits\b/gi, "suit")
|
|
89
|
+
.replace(/\btuxedos\b/gi, "tuxedo")
|
|
90
|
+
.replace(/\bgowns\b/gi, "gown")
|
|
91
|
+
.replace(/\bdresses\b/gi, "dress")
|
|
92
|
+
.replace(/\bsweaters\b/gi, "sweater")
|
|
93
|
+
.replace(/\bhoodies\b/gi, "hoodie")
|
|
94
|
+
.replace(/\bcoats\b/gi, "coat")
|
|
95
|
+
.replace(/\bshirts\b/gi, "shirt")
|
|
96
|
+
.replace(/\bhats\b/gi, "hat")
|
|
97
|
+
.replace(/\baccessories\b/gi, "accessory")
|
|
98
|
+
.replace(/\s+/g, ' ').trim();
|
|
99
|
+
}
|
|
60
100
|
}
|
|
61
101
|
|