@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.88.3",
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 renderItem = useCallback(({ item }: { item: Creation }) => (
105
- <CreationCard
106
- creation={item}
107
- titleText={getItemTitle(item)}
108
- callbacks={getItemCallbacks(item)}
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 renderGridItems = useCallback((items: Creation[]) => {
114
- const rows: Array<{ left: Creation; right: Creation | null }> = [];
115
- for (let i = 0; i < items.length; i += 2) {
116
- rows.push({ left: items[i], right: items[i + 1] ?? null });
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
- return rows.map((row, index) => (
119
- <View key={`grid-row-${index}`} style={styles.gridRow}>
120
- <CreationCardCompact
121
- creation={row.left}
122
- callbacks={{ onPress: getItemCallbacks(row.left).onPress }}
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={row.right}
127
- callbacks={{ onPress: getItemCallbacks(row.right).onPress }}
122
+ creation={item}
123
+ callbacks={{ onPress: getItemCallbacks(item).onPress }}
128
124
  />
129
- ) : (
130
- <View style={styles.gridPlaceholder} />
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={setViewMode}
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
- <View style={styles.listContent}>
210
- {filters.filtered.map((item) => (
211
- <React.Fragment key={item.id}>
212
- {renderItem({ item })}
213
- </React.Fragment>
214
- ))}
215
- </View>
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(pulseAnim, { toValue: 1, duration: 900, useNativeDriver: true }),
59
- Animated.timing(pulseAnim, { toValue: 0.6, duration: 900, useNativeDriver: true }),
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
- }, [pulseAnim]);
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
- {/* Pulsing spinner */}
96
- <Animated.View style={[styles.spinnerContainer, { opacity: pulseAnim, backgroundColor: tokens.colors.primary + "15" }]}>
97
- <ActivityIndicator size="large" color={tokens.colors.primary} />
98
- </Animated.View>
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
- spinnerContainer: {
201
- width: 80,
202
- height: 80,
203
- borderRadius: 40,
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 replacing singular terms with plural ones.
47
- * Primarily used to fix "baked" prompts that were generated with singular defaults.
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 (!isCouple) return prompt;
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
- return prompt
53
- .replace(/The person must be/g, "Both people must be")
54
- .replace(/the person is/gi, "both people are")
55
- .replace(/Keep every facial feature/g, "Keep every facial feature for both people")
56
- .replace(/the person/gi, "both people")
57
- .replace(/facial feature/gi, "facial features")
58
- .replace(/identical/g, "identical for both individuals")
59
- .replace(/instantly recognizable/g, "instantly recognizable as themselves");
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