@umituz/react-native-ai-generation-content 1.17.110 → 1.17.113
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/infrastructure/repositories/CreationsWriter.ts +26 -1
- package/src/domains/creations/presentation/components/CreationVideoPreview.tsx +1 -1
- package/src/features/text-to-video/domain/types/callback.types.ts +11 -1
- package/src/features/text-to-video/domain/types/index.ts +1 -0
- package/src/features/text-to-video/index.ts +1 -0
- package/src/features/text-to-video/infrastructure/services/text-to-video-executor.ts +34 -4
- package/src/features/text-to-video/presentation/hooks/useTextToVideoFeature.ts +80 -41
- package/src/presentation/components/AIGenerationForm.tsx +3 -1
- package/src/presentation/components/AIGenerationForm.types.ts +4 -0
- package/src/presentation/components/GenerationProgressContent.tsx +27 -3
- package/src/presentation/components/GenerationProgressModal.tsx +2 -2
package/package.json
CHANGED
|
@@ -12,6 +12,11 @@ export class CreationsWriter {
|
|
|
12
12
|
constructor(private readonly pathResolver: FirestorePathResolver) { }
|
|
13
13
|
|
|
14
14
|
async create(userId: string, creation: Creation): Promise<void> {
|
|
15
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
16
|
+
// eslint-disable-next-line no-console
|
|
17
|
+
console.log("[CreationsWriter] create() start", { userId, creationId: creation.id });
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
const docRef = this.pathResolver.getDocRef(userId, creation.id);
|
|
16
21
|
if (!docRef) throw new Error("Firestore not initialized");
|
|
17
22
|
|
|
@@ -23,9 +28,23 @@ export class CreationsWriter {
|
|
|
23
28
|
metadata: creation.metadata || {},
|
|
24
29
|
isShared: creation.isShared || false,
|
|
25
30
|
isFavorite: creation.isFavorite || false,
|
|
31
|
+
status: creation.status,
|
|
32
|
+
output: creation.output,
|
|
26
33
|
};
|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
try {
|
|
36
|
+
await setDoc(docRef, data);
|
|
37
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
38
|
+
// eslint-disable-next-line no-console
|
|
39
|
+
console.log("[CreationsWriter] create() success", { creationId: creation.id });
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
43
|
+
// eslint-disable-next-line no-console
|
|
44
|
+
console.error("[CreationsWriter] create() error", error);
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
29
48
|
}
|
|
30
49
|
|
|
31
50
|
async update(
|
|
@@ -59,6 +78,12 @@ export class CreationsWriter {
|
|
|
59
78
|
if (updates.prompt !== undefined) {
|
|
60
79
|
updateData.prompt = updates.prompt;
|
|
61
80
|
}
|
|
81
|
+
if (updates.status !== undefined) {
|
|
82
|
+
updateData.status = updates.status;
|
|
83
|
+
}
|
|
84
|
+
if (updates.output !== undefined) {
|
|
85
|
+
updateData.output = updates.output;
|
|
86
|
+
}
|
|
62
87
|
|
|
63
88
|
await updateDoc(docRef, updateData);
|
|
64
89
|
return true;
|
|
@@ -27,12 +27,22 @@ export interface CreationData {
|
|
|
27
27
|
metadata?: Record<string, unknown>;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export interface GenerationStartData {
|
|
31
|
+
creationId: string;
|
|
32
|
+
type: "text-to-video";
|
|
33
|
+
prompt: string;
|
|
34
|
+
metadata?: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
export interface TextToVideoCallbacks {
|
|
31
38
|
onCreditCheck?: (cost: number) => Promise<boolean>;
|
|
32
39
|
onCreditDeduct?: (cost: number) => Promise<void>;
|
|
33
40
|
onAuthCheck?: () => boolean;
|
|
34
41
|
onModeration?: (prompt: string) => Promise<VideoModerationResult>;
|
|
35
|
-
|
|
42
|
+
/** Called when generation starts - save a "processing" creation */
|
|
43
|
+
onGenerationStart?: (data: GenerationStartData) => Promise<void>;
|
|
44
|
+
/** Called when generation completes - update creation to "completed" */
|
|
45
|
+
onCreationSave?: (data: CreationData & { creationId: string }) => Promise<void>;
|
|
36
46
|
onGenerate?: (result: TextToVideoResult) => void;
|
|
37
47
|
onError?: (error: string) => void;
|
|
38
48
|
onProgress?: (progress: number) => void;
|
|
@@ -49,13 +49,26 @@ export async function executeTextToVideo(
|
|
|
49
49
|
request: TextToVideoRequest,
|
|
50
50
|
options: ExecuteTextToVideoOptions,
|
|
51
51
|
): Promise<TextToVideoResult> {
|
|
52
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
53
|
+
// eslint-disable-next-line no-console
|
|
54
|
+
console.log("[TextToVideoExecutor] executeTextToVideo() called");
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
const provider = providerRegistry.getActiveProvider();
|
|
53
58
|
|
|
54
59
|
if (!provider) {
|
|
60
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
61
|
+
// eslint-disable-next-line no-console
|
|
62
|
+
console.error("[TextToVideoExecutor] No AI provider configured");
|
|
63
|
+
}
|
|
55
64
|
return { success: false, error: "No AI provider configured" };
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
if (!provider.isInitialized()) {
|
|
68
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
69
|
+
// eslint-disable-next-line no-console
|
|
70
|
+
console.error("[TextToVideoExecutor] AI provider not initialized");
|
|
71
|
+
}
|
|
59
72
|
return { success: false, error: "AI provider not initialized" };
|
|
60
73
|
}
|
|
61
74
|
|
|
@@ -65,27 +78,44 @@ export async function executeTextToVideo(
|
|
|
65
78
|
|
|
66
79
|
const { model, buildInput, extractResult, onProgress } = options;
|
|
67
80
|
|
|
68
|
-
if (__DEV__) {
|
|
81
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
69
82
|
// eslint-disable-next-line no-console
|
|
70
|
-
console.log(`[
|
|
83
|
+
console.log(`[TextToVideoExecutor] Provider: ${provider.providerId}, Model: ${model}`);
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
try {
|
|
74
87
|
onProgress?.(5);
|
|
88
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
89
|
+
// eslint-disable-next-line no-console
|
|
90
|
+
console.log("[TextToVideoExecutor] Starting provider.run()...");
|
|
91
|
+
}
|
|
75
92
|
|
|
76
93
|
const input = buildInput(request.prompt, request.options);
|
|
77
94
|
|
|
78
95
|
const result = await provider.run(model, input, {
|
|
79
96
|
onProgress: (progress) => {
|
|
97
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
98
|
+
// eslint-disable-next-line no-console
|
|
99
|
+
console.log("[TextToVideoExecutor] Progress:", progress);
|
|
100
|
+
}
|
|
80
101
|
onProgress?.(progress);
|
|
81
102
|
},
|
|
82
103
|
});
|
|
83
104
|
|
|
105
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
106
|
+
// eslint-disable-next-line no-console
|
|
107
|
+
console.log("[TextToVideoExecutor] provider.run() completed", result);
|
|
108
|
+
}
|
|
109
|
+
|
|
84
110
|
const extractor = extractResult || defaultExtractResult;
|
|
85
111
|
const extracted = extractor(result);
|
|
86
112
|
onProgress?.(100);
|
|
87
113
|
|
|
88
114
|
if (!extracted?.videoUrl) {
|
|
115
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
116
|
+
// eslint-disable-next-line no-console
|
|
117
|
+
console.error("[TextToVideoExecutor] No video URL in response");
|
|
118
|
+
}
|
|
89
119
|
return { success: false, error: "No video in response" };
|
|
90
120
|
}
|
|
91
121
|
|
|
@@ -96,9 +126,9 @@ export async function executeTextToVideo(
|
|
|
96
126
|
};
|
|
97
127
|
} catch (error) {
|
|
98
128
|
const message = error instanceof Error ? error.message : String(error);
|
|
99
|
-
if (__DEV__) {
|
|
129
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
100
130
|
// eslint-disable-next-line no-console
|
|
101
|
-
console.error("[
|
|
131
|
+
console.error("[TextToVideoExecutor] Error:", message);
|
|
102
132
|
}
|
|
103
133
|
return { success: false, error: message };
|
|
104
134
|
}
|
|
@@ -124,6 +124,9 @@ export function useTextToVideoFeature(
|
|
|
124
124
|
|
|
125
125
|
const executeGeneration = useCallback(
|
|
126
126
|
async (prompt: string, options?: TextToVideoOptions): Promise<TextToVideoResult> => {
|
|
127
|
+
// Generate unique creation ID for tracking
|
|
128
|
+
const creationId = `text-to-video_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
129
|
+
|
|
127
130
|
setState((prev) => ({
|
|
128
131
|
...prev,
|
|
129
132
|
isProcessing: true,
|
|
@@ -133,59 +136,95 @@ export function useTextToVideoFeature(
|
|
|
133
136
|
|
|
134
137
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
135
138
|
// eslint-disable-next-line no-console
|
|
136
|
-
console.log("[TextToVideoFeature] Starting generation with prompt:", prompt);
|
|
139
|
+
console.log("[TextToVideoFeature] Starting generation with prompt:", prompt, "creationId:", creationId);
|
|
137
140
|
}
|
|
138
141
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
{
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
142
|
+
// Create "processing" creation at start (fire-and-forget to not block generation)
|
|
143
|
+
if (callbacks.onGenerationStart) {
|
|
144
|
+
callbacks.onGenerationStart({
|
|
145
|
+
creationId,
|
|
146
|
+
type: "text-to-video",
|
|
147
|
+
prompt,
|
|
148
|
+
metadata: options as Record<string, unknown> | undefined,
|
|
149
|
+
}).catch((err) => {
|
|
150
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
151
|
+
// eslint-disable-next-line no-console
|
|
152
|
+
console.warn("[TextToVideoFeature] onGenerationStart failed:", err);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
151
156
|
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
thumbnailUrl: result.thumbnailUrl ?? null,
|
|
157
|
-
isProcessing: false,
|
|
158
|
-
progress: 100,
|
|
159
|
-
}));
|
|
157
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
158
|
+
// eslint-disable-next-line no-console
|
|
159
|
+
console.log("[TextToVideoFeature] Starting executeTextToVideo...");
|
|
160
|
+
}
|
|
160
161
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
try {
|
|
163
|
+
const result = await executeTextToVideo(
|
|
164
|
+
{ prompt, userId, options },
|
|
165
|
+
{
|
|
166
|
+
model: config.model,
|
|
167
|
+
buildInput,
|
|
168
|
+
extractResult,
|
|
169
|
+
onProgress: (progress) => {
|
|
170
|
+
setState((prev) => ({ ...prev, progress }));
|
|
171
|
+
callbacks.onProgress?.(progress);
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (result.success && result.videoUrl) {
|
|
177
|
+
setState((prev) => ({
|
|
178
|
+
...prev,
|
|
179
|
+
videoUrl: result.videoUrl ?? null,
|
|
180
|
+
thumbnailUrl: result.thumbnailUrl ?? null,
|
|
181
|
+
isProcessing: false,
|
|
182
|
+
progress: 100,
|
|
183
|
+
}));
|
|
184
|
+
|
|
185
|
+
// Deduct credits after successful generation
|
|
186
|
+
if (callbacks.onCreditDeduct) {
|
|
187
|
+
await callbacks.onCreditDeduct(config.creditCost);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Update creation to completed after successful generation
|
|
191
|
+
if (callbacks.onCreationSave) {
|
|
192
|
+
await callbacks.onCreationSave({
|
|
193
|
+
creationId,
|
|
194
|
+
type: "text-to-video",
|
|
195
|
+
videoUrl: result.videoUrl,
|
|
196
|
+
thumbnailUrl: result.thumbnailUrl,
|
|
197
|
+
prompt,
|
|
198
|
+
metadata: options as Record<string, unknown> | undefined,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
callbacks.onGenerate?.(result);
|
|
203
|
+
} else {
|
|
204
|
+
const error = result.error || "Generation failed";
|
|
205
|
+
setState((prev) => ({
|
|
206
|
+
...prev,
|
|
207
|
+
isProcessing: false,
|
|
208
|
+
error,
|
|
209
|
+
}));
|
|
210
|
+
callbacks.onError?.(error);
|
|
164
211
|
}
|
|
165
212
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
prompt,
|
|
173
|
-
metadata: options as Record<string, unknown> | undefined,
|
|
174
|
-
});
|
|
213
|
+
return result;
|
|
214
|
+
} catch (err) {
|
|
215
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
216
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
217
|
+
// eslint-disable-next-line no-console
|
|
218
|
+
console.error("[TextToVideoFeature] Generation error:", errorMessage);
|
|
175
219
|
}
|
|
176
|
-
|
|
177
|
-
callbacks.onGenerate?.(result);
|
|
178
|
-
} else {
|
|
179
|
-
const error = result.error || "Generation failed";
|
|
180
220
|
setState((prev) => ({
|
|
181
221
|
...prev,
|
|
182
222
|
isProcessing: false,
|
|
183
|
-
error,
|
|
223
|
+
error: errorMessage,
|
|
184
224
|
}));
|
|
185
|
-
callbacks.onError?.(
|
|
225
|
+
callbacks.onError?.(errorMessage);
|
|
226
|
+
return { success: false, error: errorMessage };
|
|
186
227
|
}
|
|
187
|
-
|
|
188
|
-
return result;
|
|
189
228
|
},
|
|
190
229
|
[userId, config.model, buildInput, extractResult, callbacks],
|
|
191
230
|
);
|
|
@@ -37,6 +37,7 @@ export const AIGenerationForm: React.FC<AIGenerationFormProps> = ({
|
|
|
37
37
|
hideGenerateButton,
|
|
38
38
|
progress,
|
|
39
39
|
progressIcon,
|
|
40
|
+
isProgressModalVisible,
|
|
40
41
|
onCloseProgressModal,
|
|
41
42
|
generateButtonProps,
|
|
42
43
|
showAdvanced,
|
|
@@ -159,12 +160,13 @@ export const AIGenerationForm: React.FC<AIGenerationFormProps> = ({
|
|
|
159
160
|
|
|
160
161
|
{/* MANDATORY: Progress Modal shows automatically when isGenerating */}
|
|
161
162
|
<GenerationProgressModal
|
|
162
|
-
visible={isGenerating}
|
|
163
|
+
visible={isProgressModalVisible ?? isGenerating}
|
|
163
164
|
progress={progress ?? 0}
|
|
164
165
|
icon={progressIcon || "sparkles-outline"}
|
|
165
166
|
title={translations.progressTitle || translations.generatingButton}
|
|
166
167
|
message={translations.progressMessage || translations.progressHint}
|
|
167
168
|
onClose={onCloseProgressModal}
|
|
169
|
+
backgroundHint={onCloseProgressModal ? translations.progressBackgroundHint : undefined}
|
|
168
170
|
/>
|
|
169
171
|
</>
|
|
170
172
|
);
|
|
@@ -18,6 +18,8 @@ export interface AIGenerationFormTranslations {
|
|
|
18
18
|
progressTitle?: string;
|
|
19
19
|
progressMessage?: string;
|
|
20
20
|
progressHint?: string;
|
|
21
|
+
/** Hint for background generation (e.g., "Continue in background") */
|
|
22
|
+
progressBackgroundHint?: string;
|
|
21
23
|
presetsTitle?: string;
|
|
22
24
|
showAdvancedLabel?: string;
|
|
23
25
|
hideAdvancedLabel?: string;
|
|
@@ -57,6 +59,8 @@ export interface AIGenerationFormProps extends PropsWithChildren {
|
|
|
57
59
|
// Optional: Generation Progress
|
|
58
60
|
progress?: number;
|
|
59
61
|
progressIcon?: string;
|
|
62
|
+
/** Override modal visibility (defaults to isGenerating) */
|
|
63
|
+
isProgressModalVisible?: boolean;
|
|
60
64
|
/** Callback when user closes the progress modal (for background generation) */
|
|
61
65
|
onCloseProgressModal?: () => void;
|
|
62
66
|
|
|
@@ -23,8 +23,8 @@ export interface GenerationProgressContentProps {
|
|
|
23
23
|
readonly onDismiss?: () => void;
|
|
24
24
|
/** Close button in top-right corner for background generation */
|
|
25
25
|
readonly onClose?: () => void;
|
|
26
|
-
/**
|
|
27
|
-
readonly
|
|
26
|
+
/** Hint text shown near close button (e.g., "Continue in background") */
|
|
27
|
+
readonly backgroundHint?: string;
|
|
28
28
|
readonly backgroundColor?: string;
|
|
29
29
|
readonly textColor?: string;
|
|
30
30
|
readonly hintColor?: string;
|
|
@@ -44,7 +44,7 @@ export const GenerationProgressContent: React.FC<
|
|
|
44
44
|
dismissLabel,
|
|
45
45
|
onDismiss,
|
|
46
46
|
onClose,
|
|
47
|
-
|
|
47
|
+
backgroundHint,
|
|
48
48
|
backgroundColor,
|
|
49
49
|
textColor,
|
|
50
50
|
hintColor,
|
|
@@ -119,6 +119,21 @@ export const GenerationProgressContent: React.FC<
|
|
|
119
119
|
</AtomicText>
|
|
120
120
|
)}
|
|
121
121
|
|
|
122
|
+
{/* Background hint - clickable to close and continue in background */}
|
|
123
|
+
{onClose && backgroundHint && (
|
|
124
|
+
<TouchableOpacity
|
|
125
|
+
style={styles.backgroundHintButton}
|
|
126
|
+
onPress={onClose}
|
|
127
|
+
>
|
|
128
|
+
<AtomicText
|
|
129
|
+
type="bodySmall"
|
|
130
|
+
style={[styles.backgroundHintText, { color: tokens.colors.primary }]}
|
|
131
|
+
>
|
|
132
|
+
{backgroundHint}
|
|
133
|
+
</AtomicText>
|
|
134
|
+
</TouchableOpacity>
|
|
135
|
+
)}
|
|
136
|
+
|
|
122
137
|
{onDismiss && (
|
|
123
138
|
<TouchableOpacity
|
|
124
139
|
style={[
|
|
@@ -178,6 +193,15 @@ const styles = StyleSheet.create({
|
|
|
178
193
|
lineHeight: 18,
|
|
179
194
|
paddingHorizontal: 8,
|
|
180
195
|
},
|
|
196
|
+
backgroundHintButton: {
|
|
197
|
+
marginTop: 16,
|
|
198
|
+
paddingVertical: 8,
|
|
199
|
+
paddingHorizontal: 16,
|
|
200
|
+
},
|
|
201
|
+
backgroundHintText: {
|
|
202
|
+
textAlign: "center",
|
|
203
|
+
textDecorationLine: "underline",
|
|
204
|
+
},
|
|
181
205
|
dismissButton: {
|
|
182
206
|
marginTop: 16,
|
|
183
207
|
paddingVertical: 14,
|
|
@@ -46,7 +46,7 @@ export const GenerationProgressModal: React.FC<
|
|
|
46
46
|
dismissLabel,
|
|
47
47
|
onDismiss,
|
|
48
48
|
onClose,
|
|
49
|
-
|
|
49
|
+
backgroundHint,
|
|
50
50
|
modalBackgroundColor,
|
|
51
51
|
textColor,
|
|
52
52
|
hintColor,
|
|
@@ -82,7 +82,7 @@ export const GenerationProgressModal: React.FC<
|
|
|
82
82
|
dismissLabel={dismissLabel}
|
|
83
83
|
onDismiss={onDismiss}
|
|
84
84
|
onClose={onClose}
|
|
85
|
-
|
|
85
|
+
backgroundHint={backgroundHint}
|
|
86
86
|
backgroundColor={modalBackgroundColor || tokens.colors.surface}
|
|
87
87
|
textColor={textColor || tokens.colors.textPrimary}
|
|
88
88
|
hintColor={hintColor || tokens.colors.textTertiary}
|