@umituz/react-native-ai-generation-content 1.27.24 → 1.27.26

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.27.24",
3
+ "version": "1.27.26",
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",
@@ -12,7 +12,8 @@ export const IMAGE_TO_VIDEO_WIZARD_CONFIG: WizardFeatureConfig = {
12
12
  {
13
13
  id: "photo_1",
14
14
  type: "photo_upload",
15
- label: "Your Photo",
15
+ titleKey: "imageToVideo.selectPhoto",
16
+ subtitleKey: "imageToVideo.selectPhotoHint",
16
17
  showFaceDetection: false,
17
18
  showPhotoTips: true,
18
19
  required: true,
@@ -20,13 +21,15 @@ export const IMAGE_TO_VIDEO_WIZARD_CONFIG: WizardFeatureConfig = {
20
21
  {
21
22
  id: "motion_prompt",
22
23
  type: "text_input",
23
- required: false,
24
+ titleKey: "imageToVideo.motionPrompt",
24
25
  placeholderKey: "imageToVideo.motionPromptPlaceholder",
26
+ required: false,
25
27
  maxLength: 200,
26
28
  },
27
29
  {
28
30
  id: "duration",
29
31
  type: "selection",
32
+ titleKey: "generation.duration.title",
30
33
  selectionType: "duration",
31
34
  options: [
32
35
  { id: "5s", label: "5 seconds", value: 5 },
@@ -9,9 +9,9 @@ const promptStep: TextInputStepConfig = {
9
9
  id: "prompt",
10
10
  type: "text_input",
11
11
  required: true,
12
- titleKey: "text2image.wizard.prompt.title",
13
- subtitleKey: "text2image.wizard.prompt.subtitle",
14
- placeholderKey: "text2image.wizard.prompt.placeholder",
12
+ titleKey: "text2image.hero.title",
13
+ subtitleKey: "text2image.hero.subtitle",
14
+ placeholderKey: "text2image.prompt.placeholder",
15
15
  minLength: 3,
16
16
  maxLength: 1000,
17
17
  multiline: true,
@@ -13,7 +13,9 @@ export const TEXT_TO_VIDEO_WIZARD_CONFIG: WizardFeatureConfig = {
13
13
  id: "prompt",
14
14
  type: "text_input",
15
15
  required: true,
16
- placeholderKey: "textToVideo.promptPlaceholder",
16
+ titleKey: "textToVideo.hero.title",
17
+ subtitleKey: "textToVideo.hero.subtitle",
18
+ placeholderKey: "textToVideo.prompt.placeholder",
17
19
  minLength: 3,
18
20
  maxLength: 500,
19
21
  multiline: true,
@@ -21,6 +23,7 @@ export const TEXT_TO_VIDEO_WIZARD_CONFIG: WizardFeatureConfig = {
21
23
  {
22
24
  id: "duration",
23
25
  type: "selection",
26
+ titleKey: "generation.duration.title",
24
27
  selectionType: "duration",
25
28
  options: [
26
29
  { id: "5s", label: "5 seconds", value: 5 },
@@ -16,6 +16,7 @@ import {
16
16
  } from "./wizard-strategy.constants";
17
17
  import { buildFacePreservationPrompt } from "../../../../prompts/infrastructure/builders/face-preservation-builder";
18
18
  import { buildInteractionStylePrompt, type InteractionStyle } from "../../../../prompts/infrastructure/builders/interaction-style-builder";
19
+ import { extractPrompt, extractSelection } from "../utils";
19
20
 
20
21
  declare const __DEV__: boolean;
21
22
 
@@ -165,9 +166,8 @@ export async function buildImageInput(
165
166
  ): Promise<ImageGenerationInput | null> {
166
167
  const photos = await extractPhotosFromWizardData(wizardData);
167
168
 
168
- // Get prompt from wizardData (text_input step) OR scenario.aiPrompt
169
- const userPrompt = wizardData.prompt as string | undefined;
170
- const prompt = userPrompt?.trim() || scenario.aiPrompt?.trim();
169
+ // Extract prompt using type-safe extractor with fallback
170
+ const prompt = extractPrompt(wizardData, scenario.aiPrompt);
171
171
 
172
172
  if (!prompt) {
173
173
  throw new Error("Prompt is required for image generation");
@@ -178,18 +178,19 @@ export async function buildImageInput(
178
178
  if (photos.length > 0) {
179
179
  const styleEnhancements: string[] = [];
180
180
 
181
- const romanticMoods = wizardData.selection_romantic_mood as string[] | undefined;
182
- if (romanticMoods?.length) {
181
+ // Extract selections using type-safe extractor
182
+ const romanticMoods = extractSelection(wizardData.selection_romantic_mood);
183
+ if (Array.isArray(romanticMoods) && romanticMoods.length > 0) {
183
184
  styleEnhancements.push(`Mood: ${romanticMoods.join(", ")}`);
184
185
  }
185
186
 
186
- const artStyle = wizardData.selection_art_style as string | undefined;
187
- if (artStyle && artStyle !== DEFAULT_STYLE_VALUE) {
187
+ const artStyle = extractSelection(wizardData.selection_art_style);
188
+ if (typeof artStyle === "string" && artStyle !== DEFAULT_STYLE_VALUE) {
188
189
  styleEnhancements.push(`Art style: ${artStyle}`);
189
190
  }
190
191
 
191
- const artist = wizardData.selection_artist_style as string | undefined;
192
- if (artist && artist !== DEFAULT_STYLE_VALUE) {
192
+ const artist = extractSelection(wizardData.selection_artist_style);
193
+ if (typeof artist === "string" && artist !== DEFAULT_STYLE_VALUE) {
193
194
  styleEnhancements.push(`Artist style: ${artist}`);
194
195
  }
195
196
 
@@ -198,8 +199,9 @@ export async function buildImageInput(
198
199
  }
199
200
  }
200
201
 
201
- // Get style from wizard selection (for text-to-image)
202
- const style = wizardData.style as string | undefined;
202
+ // Extract style using type-safe extractor (for text-to-image)
203
+ const styleValue = extractSelection(wizardData.style);
204
+ const style = typeof styleValue === "string" ? styleValue : undefined;
203
205
 
204
206
  // Get interaction style from scenario (default: romantic for couple apps)
205
207
  const interactionStyle = (scenario.interactionStyle as InteractionStyle) ?? "romantic";
@@ -10,6 +10,7 @@ import { createCreationsRepository } from "../../../../creations/infrastructure/
10
10
  import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
11
11
  import type { WizardStrategy } from "./wizard-strategy.types";
12
12
  import { PHOTO_KEY_PREFIX, VIDEO_FEATURE_PATTERNS } from "./wizard-strategy.constants";
13
+ import { extractPrompt, extractDuration } from "../utils";
13
14
 
14
15
  // ============================================================================
15
16
  // Types
@@ -86,17 +87,15 @@ export async function buildVideoInput(
86
87
  ): Promise<VideoGenerationInput | null> {
87
88
  const photos = await extractPhotosFromWizardData(wizardData);
88
89
 
89
- // Get prompt from wizardData or scenario
90
- const userPrompt = wizardData.prompt as string | undefined;
91
- const motionPrompt = wizardData.motion_prompt as string | undefined;
92
- const prompt = userPrompt?.trim() || motionPrompt?.trim() || scenario.aiPrompt?.trim();
90
+ // Extract prompt using type-safe extractor with fallback
91
+ const prompt = extractPrompt(wizardData, scenario.aiPrompt);
93
92
 
94
93
  if (!prompt) {
95
94
  throw new Error("Prompt is required for video generation");
96
95
  }
97
96
 
98
- // Get duration from wizardData
99
- const duration = wizardData.duration as number | undefined;
97
+ // Extract duration using type-safe extractor (default: 5 seconds)
98
+ const duration = extractDuration(wizardData, 5);
100
99
 
101
100
  return {
102
101
  sourceImageBase64: photos[0],
@@ -0,0 +1,8 @@
1
+ export {
2
+ extractString,
3
+ extractTrimmedString,
4
+ extractNumber,
5
+ extractSelection,
6
+ extractPrompt,
7
+ extractDuration,
8
+ } from "./wizard-data-extractors";
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Wizard Data Extractors
3
+ * Type-safe utilities for extracting values from wizard data
4
+ *
5
+ * Pattern: Type Guards + Normalizers
6
+ * @see https://www.typescriptlang.org/docs/handbook/2/narrowing.html
7
+ * @see https://betterstack.com/community/guides/scaling-nodejs/typescript-type-guards/
8
+ */
9
+
10
+ // ============================================================================
11
+ // Type Guards
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Check if value is a non-null object
16
+ */
17
+ function isObject(value: unknown): value is Record<string, unknown> {
18
+ return typeof value === "object" && value !== null && !Array.isArray(value);
19
+ }
20
+
21
+ /**
22
+ * Check if object has a specific property
23
+ * Uses 'in' operator for safe property checking
24
+ */
25
+ function hasProperty<K extends string>(
26
+ obj: Record<string, unknown>,
27
+ key: K,
28
+ ): obj is Record<K, unknown> {
29
+ return key in obj;
30
+ }
31
+
32
+ // ============================================================================
33
+ // String Extractors
34
+ // ============================================================================
35
+
36
+ /**
37
+ * Extracts string from various input formats:
38
+ * - Direct string: "my text"
39
+ * - Object with text field: { text: "my text" }
40
+ * - Object with uri field (fallback): { uri: "my text" }
41
+ *
42
+ * @param value - The value to extract string from
43
+ * @returns The extracted string or undefined
44
+ */
45
+ export function extractString(value: unknown): string | undefined {
46
+ // Direct string
47
+ if (typeof value === "string") {
48
+ return value;
49
+ }
50
+
51
+ // Object with text or uri field
52
+ if (isObject(value)) {
53
+ if (hasProperty(value, "text") && typeof value.text === "string") {
54
+ return value.text;
55
+ }
56
+ if (hasProperty(value, "uri") && typeof value.uri === "string") {
57
+ return value.uri;
58
+ }
59
+ }
60
+
61
+ return undefined;
62
+ }
63
+
64
+ /**
65
+ * Extracts and trims string, returning undefined if empty
66
+ */
67
+ export function extractTrimmedString(value: unknown): string | undefined {
68
+ const str = extractString(value);
69
+ const trimmed = str?.trim();
70
+ return trimmed && trimmed.length > 0 ? trimmed : undefined;
71
+ }
72
+
73
+ // ============================================================================
74
+ // Number Extractors
75
+ // ============================================================================
76
+
77
+ /**
78
+ * Extracts number from various input formats:
79
+ * - Direct number: 5
80
+ * - Object with value field: { value: 5 }
81
+ * - Object with selection field: { selection: 5 }
82
+ *
83
+ * @param value - The value to extract number from
84
+ * @returns The extracted number or undefined
85
+ */
86
+ export function extractNumber(value: unknown): number | undefined {
87
+ // Direct number
88
+ if (typeof value === "number" && !Number.isNaN(value)) {
89
+ return value;
90
+ }
91
+
92
+ // Object with value or selection field
93
+ if (isObject(value)) {
94
+ if (hasProperty(value, "value") && typeof value.value === "number") {
95
+ return value.value;
96
+ }
97
+ if (hasProperty(value, "selection") && typeof value.selection === "number") {
98
+ return value.selection;
99
+ }
100
+ }
101
+
102
+ return undefined;
103
+ }
104
+
105
+ // ============================================================================
106
+ // Selection Extractors
107
+ // ============================================================================
108
+
109
+ /**
110
+ * Extracts selection value (string or string array) from wizard data
111
+ *
112
+ * @param value - The value to extract selection from
113
+ * @returns The extracted selection or undefined
114
+ */
115
+ export function extractSelection(value: unknown): string | string[] | undefined {
116
+ // Direct string
117
+ if (typeof value === "string") {
118
+ return value;
119
+ }
120
+
121
+ // Direct string array
122
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
123
+ return value as string[];
124
+ }
125
+
126
+ // Object with selection field
127
+ if (isObject(value)) {
128
+ if (hasProperty(value, "selection")) {
129
+ const selection = value.selection;
130
+ if (typeof selection === "string") {
131
+ return selection;
132
+ }
133
+ if (Array.isArray(selection) && selection.every((v) => typeof v === "string")) {
134
+ return selection as string[];
135
+ }
136
+ }
137
+ }
138
+
139
+ return undefined;
140
+ }
141
+
142
+ // ============================================================================
143
+ // Prompt Extractor (Specialized)
144
+ // ============================================================================
145
+
146
+ /**
147
+ * Extracts prompt from wizard data with fallback chain
148
+ * Checks multiple keys in order: prompt, motion_prompt, text
149
+ *
150
+ * @param wizardData - The wizard data object
151
+ * @param fallback - Optional fallback value (e.g., scenario.aiPrompt)
152
+ * @returns The extracted and trimmed prompt or undefined
153
+ */
154
+ export function extractPrompt(
155
+ wizardData: Record<string, unknown>,
156
+ fallback?: string,
157
+ ): string | undefined {
158
+ // Priority chain for prompt keys
159
+ const promptKeys = ["prompt", "motion_prompt", "text", "userPrompt"];
160
+
161
+ for (const key of promptKeys) {
162
+ if (key in wizardData) {
163
+ const extracted = extractTrimmedString(wizardData[key]);
164
+ if (extracted) {
165
+ return extracted;
166
+ }
167
+ }
168
+ }
169
+
170
+ // Use fallback if provided
171
+ return fallback?.trim() || undefined;
172
+ }
173
+
174
+ // ============================================================================
175
+ // Duration Extractor (Specialized)
176
+ // ============================================================================
177
+
178
+ /**
179
+ * Extracts duration from wizard data
180
+ * Handles both direct number and object with value field
181
+ *
182
+ * @param wizardData - The wizard data object
183
+ * @param defaultValue - Default duration if not found
184
+ * @returns The extracted duration in seconds
185
+ */
186
+ export function extractDuration(
187
+ wizardData: Record<string, unknown>,
188
+ defaultValue = 5,
189
+ ): number {
190
+ const durationData = wizardData.duration;
191
+
192
+ const extracted = extractNumber(durationData);
193
+ if (extracted !== undefined && extracted > 0) {
194
+ return extracted;
195
+ }
196
+
197
+ return defaultValue;
198
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * WizardHeader Component
3
+ * Header with back button on left, action button on right
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TouchableOpacity, StyleSheet } from "react-native";
8
+ import {
9
+ AtomicText,
10
+ AtomicIcon,
11
+ useAppDesignTokens,
12
+ } from "@umituz/react-native-design-system";
13
+
14
+ export interface WizardHeaderProps {
15
+ readonly onBack: () => void;
16
+ readonly onAction?: () => void;
17
+ readonly backLabel?: string;
18
+ readonly actionLabel?: string;
19
+ readonly isActionDisabled?: boolean;
20
+ readonly showAction?: boolean;
21
+ }
22
+
23
+ export const WizardHeader: React.FC<WizardHeaderProps> = ({
24
+ onBack,
25
+ onAction,
26
+ backLabel,
27
+ actionLabel,
28
+ isActionDisabled = false,
29
+ showAction = true,
30
+ }) => {
31
+ const tokens = useAppDesignTokens();
32
+
33
+ return (
34
+ <View style={[styles.container, { paddingHorizontal: tokens.spacing.md }]}>
35
+ <TouchableOpacity onPress={onBack} style={styles.backButton}>
36
+ <AtomicIcon name="chevron-left" size="md" color="textPrimary" />
37
+ {backLabel ? (
38
+ <AtomicText type="bodyMedium" color="textPrimary">
39
+ {backLabel}
40
+ </AtomicText>
41
+ ) : null}
42
+ </TouchableOpacity>
43
+
44
+ {showAction && actionLabel ? (
45
+ <TouchableOpacity
46
+ onPress={onAction}
47
+ disabled={isActionDisabled}
48
+ style={[
49
+ styles.actionButton,
50
+ {
51
+ backgroundColor: isActionDisabled
52
+ ? tokens.colors.surfaceSecondary
53
+ : tokens.colors.primary,
54
+ borderRadius: tokens.radius.md,
55
+ },
56
+ ]}
57
+ >
58
+ <AtomicText
59
+ type="labelLarge"
60
+ style={{
61
+ color: isActionDisabled
62
+ ? tokens.colors.textSecondary
63
+ : tokens.colors.textInverse,
64
+ fontWeight: "600",
65
+ }}
66
+ >
67
+ {actionLabel}
68
+ </AtomicText>
69
+ </TouchableOpacity>
70
+ ) : (
71
+ <View style={styles.placeholder} />
72
+ )}
73
+ </View>
74
+ );
75
+ };
76
+
77
+ const styles = StyleSheet.create({
78
+ container: {
79
+ flexDirection: "row",
80
+ justifyContent: "space-between",
81
+ alignItems: "center",
82
+ paddingVertical: 12,
83
+ },
84
+ backButton: {
85
+ flexDirection: "row",
86
+ alignItems: "center",
87
+ gap: 4,
88
+ },
89
+ actionButton: {
90
+ paddingHorizontal: 16,
91
+ paddingVertical: 8,
92
+ },
93
+ placeholder: {
94
+ width: 80,
95
+ },
96
+ });
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * TextInputScreen
3
3
  * Generic text input step for wizard flows
4
+ * Header: Back on left, Continue on right
4
5
  */
5
6
 
6
7
  import React, { useState, useCallback } from "react";
7
- import { View, ScrollView, TextInput } from "react-native";
8
+ import { View, ScrollView, TextInput, TouchableOpacity, StyleSheet } from "react-native";
8
9
  import {
9
10
  AtomicText,
10
11
  AtomicButton,
11
12
  AtomicIcon,
12
13
  useAppDesignTokens,
13
14
  } from "@umituz/react-native-design-system";
14
- import { styles } from "./TextInputScreen.styles";
15
15
  import type { TextInputScreenProps } from "./TextInputScreen.types";
16
16
 
17
17
  export type {
@@ -48,17 +48,33 @@ export const TextInputScreen: React.FC<TextInputScreenProps> = ({
48
48
 
49
49
  return (
50
50
  <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
51
+ {/* Header with Back on left, Continue on right */}
51
52
  <View style={[styles.header, { paddingHorizontal: tokens.spacing.md }]}>
52
- <AtomicButton variant="text" size="sm" onPress={onBack}>
53
- <View style={styles.backButtonContent}>
54
- <AtomicIcon name="arrow-back" size="sm" color="textPrimary" />
55
- {translations.backButton ? (
56
- <AtomicText type="labelMedium" color="textPrimary" style={styles.backButtonText}>
57
- {translations.backButton}
58
- </AtomicText>
59
- ) : null}
60
- </View>
61
- </AtomicButton>
53
+ <TouchableOpacity onPress={onBack} style={styles.backButton}>
54
+ <AtomicIcon name="chevron-left" size="md" color="textPrimary" />
55
+ </TouchableOpacity>
56
+
57
+ <TouchableOpacity
58
+ onPress={handleContinue}
59
+ disabled={!canContinue}
60
+ style={[
61
+ styles.continueButton,
62
+ {
63
+ backgroundColor: canContinue ? tokens.colors.primary : tokens.colors.surfaceSecondary,
64
+ borderRadius: tokens.radius.md,
65
+ },
66
+ ]}
67
+ >
68
+ <AtomicText
69
+ type="labelLarge"
70
+ style={{
71
+ color: canContinue ? tokens.colors.textInverse : tokens.colors.textSecondary,
72
+ fontWeight: "600",
73
+ }}
74
+ >
75
+ {translations.continueButton}
76
+ </AtomicText>
77
+ </TouchableOpacity>
62
78
  </View>
63
79
 
64
80
  <ScrollView
@@ -120,12 +136,45 @@ export const TextInputScreen: React.FC<TextInputScreenProps> = ({
120
136
  </View>
121
137
  ) : null}
122
138
  </ScrollView>
123
-
124
- <View style={[styles.footer, { padding: tokens.spacing.md }]}>
125
- <AtomicButton variant="primary" size="lg" onPress={handleContinue} disabled={!canContinue} style={styles.continueButton}>
126
- {translations.continueButton}
127
- </AtomicButton>
128
- </View>
129
139
  </View>
130
140
  );
131
141
  };
142
+
143
+ const styles = StyleSheet.create({
144
+ container: {
145
+ flex: 1,
146
+ },
147
+ header: {
148
+ flexDirection: "row",
149
+ justifyContent: "space-between",
150
+ alignItems: "center",
151
+ paddingVertical: 12,
152
+ },
153
+ backButton: {
154
+ flexDirection: "row",
155
+ alignItems: "center",
156
+ gap: 4,
157
+ },
158
+ continueButton: {
159
+ paddingHorizontal: 16,
160
+ paddingVertical: 8,
161
+ },
162
+ scrollView: {
163
+ flex: 1,
164
+ },
165
+ title: {
166
+ marginBottom: 8,
167
+ },
168
+ inputContainer: {
169
+ borderWidth: 1,
170
+ padding: 12,
171
+ },
172
+ textInput: {
173
+ fontSize: 16,
174
+ lineHeight: 22,
175
+ },
176
+ charCount: {
177
+ textAlign: "right",
178
+ marginTop: 8,
179
+ },
180
+ });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * TextToImageWizardFlow Types
3
+ */
4
+
5
+ import type { AlertMessages } from "../../../../presentation/hooks/generation/types";
6
+
7
+ export interface TextToImageWizardFlowProps {
8
+ readonly model: string;
9
+ readonly userId?: string;
10
+ readonly isAuthenticated: boolean;
11
+ readonly hasPremium: boolean;
12
+ readonly creditBalance: number;
13
+ readonly isCreditsLoaded: boolean;
14
+ readonly onShowAuthModal: (callback: () => void) => void;
15
+ readonly onShowPaywall: () => void;
16
+ readonly onGenerationComplete?: () => void;
17
+ readonly onGenerationError?: (error: string) => void;
18
+ readonly onBack: () => void;
19
+ readonly t: (key: string) => string;
20
+ readonly alertMessages?: AlertMessages;
21
+ }
22
+
23
+ export interface TextToImageFormState {
24
+ prompt: string;
25
+ selectedStyle: string;
26
+ }
@@ -1,48 +0,0 @@
1
- /**
2
- * TextInputScreen Styles
3
- */
4
-
5
- import { StyleSheet } from "react-native";
6
-
7
- export const styles = StyleSheet.create({
8
- container: {
9
- flex: 1,
10
- },
11
- header: {
12
- flexDirection: "row",
13
- alignItems: "center",
14
- paddingVertical: 8,
15
- },
16
- backButtonContent: {
17
- flexDirection: "row",
18
- alignItems: "center",
19
- },
20
- backButtonText: {
21
- marginLeft: 4,
22
- },
23
- scrollView: {
24
- flex: 1,
25
- },
26
- title: {
27
- marginBottom: 8,
28
- },
29
- inputContainer: {
30
- borderWidth: 1,
31
- padding: 12,
32
- },
33
- textInput: {
34
- fontSize: 16,
35
- lineHeight: 24,
36
- },
37
- charCount: {
38
- textAlign: "right",
39
- marginTop: 4,
40
- },
41
- footer: {
42
- borderTopWidth: 1,
43
- borderTopColor: "rgba(0,0,0,0.1)",
44
- },
45
- continueButton: {
46
- width: "100%",
47
- },
48
- });