@umituz/react-native-ai-generation-content 1.35.5 → 1.35.7
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/index.ts +1 -0
- package/src/domains/creations/presentation/components/GalleryEmptyStates.tsx +3 -3
- package/src/domains/creations/presentation/components/GalleryHeader.tsx +36 -2
- package/src/domains/creations/presentation/components/PendingJobsSection.tsx +78 -0
- package/src/domains/creations/presentation/components/index.ts +1 -0
- package/src/domains/creations/presentation/hooks/useGalleryFilters.ts +24 -3
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +53 -12
- package/src/domains/generation/wizard/presentation/hooks/useWizardGeneration.ts +92 -17
- package/src/domains/prompts/domain/entities/BasePromptStructure.ts +7 -63
- package/src/domains/prompts/domain/entities/MultiPersonPromptStructure.ts +59 -0
- package/src/domains/scenarios/infrastructure/utils/scenario-utils.ts +3 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.35.
|
|
3
|
+
"version": "1.35.7",
|
|
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",
|
|
@@ -154,6 +154,7 @@ export {
|
|
|
154
154
|
// Gallery Components
|
|
155
155
|
export { CreationsHomeCard } from "./presentation/components/CreationsHomeCard";
|
|
156
156
|
export { EmptyState } from "./presentation/components/EmptyState";
|
|
157
|
+
export { PendingJobsSection, type PendingJobsSectionProps } from "./presentation/components/PendingJobsSection";
|
|
157
158
|
|
|
158
159
|
// Utilities
|
|
159
160
|
export {
|
|
@@ -102,9 +102,9 @@ export function GalleryEmptyStates({
|
|
|
102
102
|
return (
|
|
103
103
|
<View style={styles.centerContainer}>
|
|
104
104
|
<EmptyState
|
|
105
|
-
title={t("common.no_results")
|
|
106
|
-
description={t("common.no_results_description")
|
|
107
|
-
actionLabel={t("common.clear_all")
|
|
105
|
+
title={t("common.no_results")}
|
|
106
|
+
description={t("common.no_results_description")}
|
|
107
|
+
actionLabel={t("common.clear_all")}
|
|
108
108
|
onAction={onClearFilters}
|
|
109
109
|
/>
|
|
110
110
|
</View>
|
|
@@ -19,6 +19,10 @@ interface GalleryHeaderProps {
|
|
|
19
19
|
readonly filterButtons?: FilterButtonConfig[];
|
|
20
20
|
readonly showFilter?: boolean;
|
|
21
21
|
readonly style?: ViewStyle;
|
|
22
|
+
/** Number of pending/processing jobs to show as badge */
|
|
23
|
+
readonly pendingCount?: number;
|
|
24
|
+
/** Label for pending badge tooltip */
|
|
25
|
+
readonly pendingLabel?: string;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
|
|
@@ -28,6 +32,8 @@ export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
|
|
|
28
32
|
filterButtons = [],
|
|
29
33
|
showFilter = true,
|
|
30
34
|
style,
|
|
35
|
+
pendingCount = 0,
|
|
36
|
+
pendingLabel,
|
|
31
37
|
}) => {
|
|
32
38
|
const tokens = useAppDesignTokens();
|
|
33
39
|
const styles = useStyles(tokens);
|
|
@@ -35,9 +41,20 @@ export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
|
|
|
35
41
|
return (
|
|
36
42
|
<View style={[styles.headerArea, style]}>
|
|
37
43
|
<View>
|
|
38
|
-
<
|
|
44
|
+
<View style={styles.titleRow}>
|
|
45
|
+
<AtomicText style={styles.title}>{title}</AtomicText>
|
|
46
|
+
{pendingCount > 0 && (
|
|
47
|
+
<View style={[styles.pendingBadge, { backgroundColor: tokens.colors.primary }]}>
|
|
48
|
+
<AtomicIcon name="Loader" size="xs" color="onPrimary" />
|
|
49
|
+
<AtomicText style={[styles.pendingBadgeText, { color: tokens.colors.onPrimary }]}>
|
|
50
|
+
{pendingCount}
|
|
51
|
+
</AtomicText>
|
|
52
|
+
</View>
|
|
53
|
+
)}
|
|
54
|
+
</View>
|
|
39
55
|
<AtomicText style={styles.subtitle}>
|
|
40
56
|
{count} {countLabel}
|
|
57
|
+
{pendingCount > 0 && pendingLabel ? ` · ${pendingCount} ${pendingLabel}` : ""}
|
|
41
58
|
</AtomicText>
|
|
42
59
|
</View>
|
|
43
60
|
{showFilter && filterButtons.length > 0 && (
|
|
@@ -83,11 +100,28 @@ const useStyles = (tokens: DesignTokens) =>
|
|
|
83
100
|
paddingVertical: tokens.spacing.sm,
|
|
84
101
|
marginBottom: tokens.spacing.sm,
|
|
85
102
|
},
|
|
103
|
+
titleRow: {
|
|
104
|
+
flexDirection: "row",
|
|
105
|
+
alignItems: "center",
|
|
106
|
+
gap: tokens.spacing.sm,
|
|
107
|
+
marginBottom: 4,
|
|
108
|
+
},
|
|
86
109
|
title: {
|
|
87
110
|
fontSize: 20,
|
|
88
111
|
fontWeight: "700",
|
|
89
112
|
color: tokens.colors.textPrimary,
|
|
90
|
-
|
|
113
|
+
},
|
|
114
|
+
pendingBadge: {
|
|
115
|
+
flexDirection: "row",
|
|
116
|
+
alignItems: "center",
|
|
117
|
+
gap: 4,
|
|
118
|
+
paddingHorizontal: 8,
|
|
119
|
+
paddingVertical: 4,
|
|
120
|
+
borderRadius: 12,
|
|
121
|
+
},
|
|
122
|
+
pendingBadgeText: {
|
|
123
|
+
fontSize: 12,
|
|
124
|
+
fontWeight: "600",
|
|
91
125
|
},
|
|
92
126
|
subtitle: {
|
|
93
127
|
fontSize: 14,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PendingJobsSection Component
|
|
3
|
+
* Displays pending/processing AI generation jobs in CreationsGallery
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import { AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
+
import type { BackgroundJob } from "../../../../domain/entities/job.types";
|
|
10
|
+
import { PendingJobCard } from "../../../../presentation/components/PendingJobCard";
|
|
11
|
+
|
|
12
|
+
export interface PendingJobsSectionProps {
|
|
13
|
+
readonly jobs: BackgroundJob[];
|
|
14
|
+
readonly onCancel?: (id: string) => void;
|
|
15
|
+
readonly onRetry?: (id: string) => void;
|
|
16
|
+
readonly title?: string;
|
|
17
|
+
readonly statusLabels?: {
|
|
18
|
+
readonly queued?: string;
|
|
19
|
+
readonly processing?: string;
|
|
20
|
+
readonly uploading?: string;
|
|
21
|
+
readonly completed?: string;
|
|
22
|
+
readonly failed?: string;
|
|
23
|
+
};
|
|
24
|
+
readonly getTypeLabel?: (type: string) => string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function PendingJobsSection({
|
|
28
|
+
jobs,
|
|
29
|
+
onCancel,
|
|
30
|
+
onRetry,
|
|
31
|
+
title,
|
|
32
|
+
statusLabels,
|
|
33
|
+
getTypeLabel,
|
|
34
|
+
}: PendingJobsSectionProps): React.ReactElement | null {
|
|
35
|
+
const tokens = useAppDesignTokens();
|
|
36
|
+
|
|
37
|
+
// Only show processing/queued jobs
|
|
38
|
+
const activeJobs = jobs.filter(
|
|
39
|
+
(job) => job.status === "processing" || job.status === "queued" || job.status === "failed",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (activeJobs.length === 0) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const styles = StyleSheet.create({
|
|
47
|
+
container: {
|
|
48
|
+
marginBottom: 16,
|
|
49
|
+
},
|
|
50
|
+
title: {
|
|
51
|
+
fontSize: 14,
|
|
52
|
+
fontWeight: "600",
|
|
53
|
+
color: tokens.colors.textSecondary,
|
|
54
|
+
marginBottom: 12,
|
|
55
|
+
},
|
|
56
|
+
jobsContainer: {
|
|
57
|
+
gap: 12,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<View style={styles.container}>
|
|
63
|
+
{title && <AtomicText style={styles.title}>{title}</AtomicText>}
|
|
64
|
+
<View style={styles.jobsContainer}>
|
|
65
|
+
{activeJobs.map((job) => (
|
|
66
|
+
<PendingJobCard
|
|
67
|
+
key={job.id}
|
|
68
|
+
job={job}
|
|
69
|
+
onCancel={onCancel}
|
|
70
|
+
onRetry={onRetry}
|
|
71
|
+
typeLabel={getTypeLabel ? getTypeLabel(job.type) : job.type}
|
|
72
|
+
statusLabels={statusLabels}
|
|
73
|
+
/>
|
|
74
|
+
))}
|
|
75
|
+
</View>
|
|
76
|
+
</View>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -32,3 +32,4 @@ export { GalleryEmptyStates } from "./GalleryEmptyStates";
|
|
|
32
32
|
export { CreationsHomeCard } from "./CreationsHomeCard";
|
|
33
33
|
export { CreationRating } from "./CreationRating";
|
|
34
34
|
export { CreationsGrid } from "./CreationsGrid";
|
|
35
|
+
export { PendingJobsSection, type PendingJobsSectionProps } from "./PendingJobsSection";
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* SOLID: Coordinates filter hooks without business logic
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { useState, useCallback } from "react";
|
|
7
|
+
import { useState, useCallback, useMemo } from "react";
|
|
8
8
|
import type { Creation } from "../../domain/entities/Creation";
|
|
9
9
|
import type { FilterOption } from "../../domain/types/creation-filter";
|
|
10
|
+
import type { BackgroundJob } from "../../../../domain/entities/job.types";
|
|
10
11
|
import { useFilter } from "./useFilter";
|
|
11
12
|
import { useCreationsFilter } from "./useCreationsFilter";
|
|
12
13
|
|
|
@@ -15,6 +16,8 @@ interface UseGalleryFiltersProps {
|
|
|
15
16
|
readonly statusOptions: FilterOption[];
|
|
16
17
|
readonly mediaOptions: FilterOption[];
|
|
17
18
|
readonly t: (key: string) => string;
|
|
19
|
+
/** Pending background jobs to include in status counts */
|
|
20
|
+
readonly pendingJobs?: BackgroundJob[];
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
interface UseGalleryFiltersReturn {
|
|
@@ -36,12 +39,30 @@ export function useGalleryFilters({
|
|
|
36
39
|
creations,
|
|
37
40
|
statusOptions,
|
|
38
41
|
mediaOptions,
|
|
39
|
-
t
|
|
42
|
+
t,
|
|
43
|
+
pendingJobs = [],
|
|
40
44
|
}: UseGalleryFiltersProps): UseGalleryFiltersReturn {
|
|
41
45
|
const [statusFilterVisible, setStatusFilterVisible] = useState(false);
|
|
42
46
|
const [mediaFilterVisible, setMediaFilterVisible] = useState(false);
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
// Calculate pending jobs count for status filter
|
|
49
|
+
const processingJobsCount = useMemo(() => {
|
|
50
|
+
return pendingJobs.filter(
|
|
51
|
+
(job) => job.status === "processing" || job.status === "queued",
|
|
52
|
+
).length;
|
|
53
|
+
}, [pendingJobs]);
|
|
54
|
+
|
|
55
|
+
// Enrich status options with dynamic counts
|
|
56
|
+
const enrichedStatusOptions = useMemo(() => {
|
|
57
|
+
return statusOptions.map((option) => {
|
|
58
|
+
if (option.id === "processing" && processingJobsCount > 0) {
|
|
59
|
+
return { ...option, count: processingJobsCount };
|
|
60
|
+
}
|
|
61
|
+
return option;
|
|
62
|
+
});
|
|
63
|
+
}, [statusOptions, processingJobsCount]);
|
|
64
|
+
|
|
65
|
+
const statusFilter = useFilter({ options: enrichedStatusOptions, t });
|
|
45
66
|
const mediaFilter = useFilter({ options: mediaOptions, t });
|
|
46
67
|
|
|
47
68
|
const { filtered, isFiltered, activeFiltersCount } = useCreationsFilter({
|
|
@@ -13,10 +13,11 @@ import {
|
|
|
13
13
|
import { useCreations } from "../hooks/useCreations";
|
|
14
14
|
import { useDeleteCreation } from "../hooks/useDeleteCreation";
|
|
15
15
|
import { useGalleryFilters } from "../hooks/useGalleryFilters";
|
|
16
|
-
import { GalleryHeader, CreationCard, GalleryEmptyStates } from "../components";
|
|
16
|
+
import { GalleryHeader, CreationCard, GalleryEmptyStates, PendingJobsSection } from "../components";
|
|
17
17
|
import { ResultPreviewScreen } from "../../../result-preview/presentation/components/ResultPreviewScreen";
|
|
18
18
|
import { StarRatingPicker } from "../../../result-preview/presentation/components/StarRatingPicker";
|
|
19
19
|
import { useResultActions } from "../../../result-preview/presentation/hooks/useResultActions";
|
|
20
|
+
import { usePendingJobs } from "../../../../presentation/hooks/use-pending-jobs";
|
|
20
21
|
import { MEDIA_FILTER_OPTIONS, STATUS_FILTER_OPTIONS } from "../../domain/types/creation-filter";
|
|
21
22
|
import { getPreviewUrl } from "../../domain/utils";
|
|
22
23
|
import type { Creation } from "../../domain/entities/Creation";
|
|
@@ -32,6 +33,10 @@ interface CreationsGalleryScreenProps {
|
|
|
32
33
|
readonly onEmptyAction?: () => void;
|
|
33
34
|
readonly emptyActionLabel?: string;
|
|
34
35
|
readonly showFilter?: boolean;
|
|
36
|
+
/** Show pending generation jobs at the top */
|
|
37
|
+
readonly showPendingJobs?: boolean;
|
|
38
|
+
/** Title for the pending jobs section */
|
|
39
|
+
readonly pendingJobsTitle?: string;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
export function CreationsGalleryScreen({
|
|
@@ -43,6 +48,8 @@ export function CreationsGalleryScreen({
|
|
|
43
48
|
onEmptyAction,
|
|
44
49
|
emptyActionLabel,
|
|
45
50
|
showFilter = config.showFilter ?? true,
|
|
51
|
+
showPendingJobs = true,
|
|
52
|
+
pendingJobsTitle,
|
|
46
53
|
}: CreationsGalleryScreenProps) {
|
|
47
54
|
const tokens = useAppDesignTokens();
|
|
48
55
|
const { share } = useSharing();
|
|
@@ -53,6 +60,9 @@ export function CreationsGalleryScreen({
|
|
|
53
60
|
|
|
54
61
|
const { data: creations, isLoading, refetch } = useCreations({ userId, repository });
|
|
55
62
|
|
|
63
|
+
// Background jobs for pending generations
|
|
64
|
+
const { jobs: pendingJobs, removeJob } = usePendingJobs();
|
|
65
|
+
|
|
56
66
|
// Auto-select creation when initialCreationId is provided
|
|
57
67
|
useEffect(() => {
|
|
58
68
|
if (initialCreationId && creations && creations.length > 0 && !hasAutoSelectedRef.current) {
|
|
@@ -81,7 +91,7 @@ export function CreationsGalleryScreen({
|
|
|
81
91
|
const showStatusFilter = config.filterConfig?.showStatusFilter ?? true;
|
|
82
92
|
const showMediaFilter = config.filterConfig?.showMediaFilter ?? true;
|
|
83
93
|
|
|
84
|
-
const filters = useGalleryFilters({ creations, statusOptions, mediaOptions, t });
|
|
94
|
+
const filters = useGalleryFilters({ creations, statusOptions, mediaOptions, t, pendingJobs });
|
|
85
95
|
|
|
86
96
|
useAppFocusEffect(useCallback(() => { void refetch(); }, [refetch]));
|
|
87
97
|
|
|
@@ -180,20 +190,51 @@ export function CreationsGalleryScreen({
|
|
|
180
190
|
/>
|
|
181
191
|
), [handleShareCard, handleDelete, handleFavorite, handleCardPress, getScenarioTitle]);
|
|
182
192
|
|
|
193
|
+
// Status labels for pending jobs
|
|
194
|
+
const pendingJobStatusLabels = useMemo(() => ({
|
|
195
|
+
queued: t("generator.status.queued"),
|
|
196
|
+
processing: t("generator.status.processing"),
|
|
197
|
+
uploading: t("generator.status.uploading"),
|
|
198
|
+
completed: t("generator.status.completed"),
|
|
199
|
+
failed: t("generator.status.failed"),
|
|
200
|
+
}), [t]);
|
|
201
|
+
|
|
183
202
|
const renderHeader = useMemo(() => {
|
|
184
|
-
|
|
203
|
+
const hasPendingJobs = showPendingJobs && pendingJobs.length > 0;
|
|
204
|
+
const hasCreations = creations && creations.length > 0;
|
|
205
|
+
|
|
206
|
+
if (!hasPendingJobs && !hasCreations && !isLoading) return null;
|
|
207
|
+
|
|
185
208
|
return (
|
|
186
|
-
<View
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
209
|
+
<View>
|
|
210
|
+
{/* Pending Jobs Section */}
|
|
211
|
+
{showPendingJobs && (
|
|
212
|
+
<PendingJobsSection
|
|
213
|
+
jobs={pendingJobs}
|
|
214
|
+
onCancel={removeJob}
|
|
215
|
+
title={pendingJobsTitle || t("creations.pendingJobs")}
|
|
216
|
+
statusLabels={pendingJobStatusLabels}
|
|
217
|
+
getTypeLabel={getScenarioTitle}
|
|
218
|
+
/>
|
|
219
|
+
)}
|
|
220
|
+
|
|
221
|
+
{/* Gallery Header */}
|
|
222
|
+
{hasCreations && (
|
|
223
|
+
<View style={[styles.header, { backgroundColor: tokens.colors.surface, borderBottomColor: tokens.colors.border }]}>
|
|
224
|
+
<GalleryHeader
|
|
225
|
+
title={t(config.translations.title)}
|
|
226
|
+
count={filters.filtered.length}
|
|
227
|
+
countLabel={t(config.translations.photoCount)}
|
|
228
|
+
showFilter={showFilter}
|
|
229
|
+
filterButtons={filterButtons}
|
|
230
|
+
pendingCount={pendingJobs.filter((j) => j.status === "processing" || j.status === "queued").length}
|
|
231
|
+
pendingLabel={t("creations.processing")}
|
|
232
|
+
/>
|
|
233
|
+
</View>
|
|
234
|
+
)}
|
|
194
235
|
</View>
|
|
195
236
|
);
|
|
196
|
-
}, [creations, isLoading, filters.filtered.length, showFilter, filterButtons, t, config, tokens]);
|
|
237
|
+
}, [creations, isLoading, filters.filtered.length, showFilter, filterButtons, t, config, tokens, showPendingJobs, pendingJobs, removeJob, pendingJobsTitle, pendingJobStatusLabels, getScenarioTitle]);
|
|
197
238
|
|
|
198
239
|
const renderEmpty = useMemo(() => (
|
|
199
240
|
<GalleryEmptyStates
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useWizardGeneration Hook
|
|
3
3
|
* Wizard generation using orchestrator + strategy factory pattern
|
|
4
|
+
* Includes background job tracking for CreationsGallery display
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import { useEffect, useRef, useMemo } from "react";
|
|
7
|
+
import { useEffect, useRef, useMemo, useCallback } from "react";
|
|
7
8
|
import { useGenerationOrchestrator } from "../../../../../presentation/hooks/generation";
|
|
8
9
|
import type { AlertMessages } from "../../../../../presentation/hooks/generation/types";
|
|
10
|
+
import { usePendingJobs } from "../../../../../presentation/hooks/use-pending-jobs";
|
|
9
11
|
import { createWizardStrategy, buildWizardInput } from "../../infrastructure/strategies";
|
|
10
12
|
|
|
11
13
|
declare const __DEV__: boolean;
|
|
@@ -32,10 +34,14 @@ export interface UseWizardGenerationProps {
|
|
|
32
34
|
readonly onSuccess?: (result: unknown) => void;
|
|
33
35
|
readonly onError?: (error: string) => void;
|
|
34
36
|
readonly onCreditsExhausted?: () => void;
|
|
37
|
+
/** Enable background job tracking for CreationsGallery display */
|
|
38
|
+
readonly trackAsBackgroundJob?: boolean;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
export interface UseWizardGenerationReturn {
|
|
38
42
|
readonly isGenerating: boolean;
|
|
43
|
+
/** Current job ID if tracking is enabled */
|
|
44
|
+
readonly currentJobId: string | null;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
export const useWizardGeneration = (
|
|
@@ -50,18 +56,24 @@ export const useWizardGeneration = (
|
|
|
50
56
|
onSuccess,
|
|
51
57
|
onError,
|
|
52
58
|
onCreditsExhausted,
|
|
59
|
+
trackAsBackgroundJob = true,
|
|
53
60
|
} = props;
|
|
54
61
|
|
|
55
62
|
const hasStarted = useRef(false);
|
|
63
|
+
const currentJobIdRef = useRef<string | null>(null);
|
|
64
|
+
|
|
65
|
+
// Background job tracking
|
|
66
|
+
const { addJob, updateJob, removeJob } = usePendingJobs();
|
|
56
67
|
|
|
57
68
|
useEffect(() => {
|
|
58
69
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
59
70
|
console.log("[useWizardGeneration] Initialized", {
|
|
60
71
|
scenarioId: scenario.id,
|
|
61
72
|
outputType: scenario.outputType || "video",
|
|
73
|
+
trackAsBackgroundJob,
|
|
62
74
|
});
|
|
63
75
|
}
|
|
64
|
-
}, [scenario.id, scenario.outputType]);
|
|
76
|
+
}, [scenario.id, scenario.outputType, trackAsBackgroundJob]);
|
|
65
77
|
|
|
66
78
|
const strategy = useMemo(() => {
|
|
67
79
|
return createWizardStrategy({
|
|
@@ -71,6 +83,44 @@ export const useWizardGeneration = (
|
|
|
71
83
|
});
|
|
72
84
|
}, [scenario, wizardData]);
|
|
73
85
|
|
|
86
|
+
// Handle generation success - remove job from queue
|
|
87
|
+
const handleSuccess = useCallback(
|
|
88
|
+
(result: unknown) => {
|
|
89
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
90
|
+
console.log("[useWizardGeneration] Success");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Remove job from pending queue (creation is saved)
|
|
94
|
+
if (trackAsBackgroundJob && currentJobIdRef.current) {
|
|
95
|
+
removeJob(currentJobIdRef.current);
|
|
96
|
+
currentJobIdRef.current = null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
onSuccess?.(result);
|
|
100
|
+
},
|
|
101
|
+
[trackAsBackgroundJob, removeJob, onSuccess],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Handle generation error - update job status
|
|
105
|
+
const handleError = useCallback(
|
|
106
|
+
(err: { message: string }) => {
|
|
107
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
108
|
+
console.log("[useWizardGeneration] Error:", err.message);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Update job to failed status
|
|
112
|
+
if (trackAsBackgroundJob && currentJobIdRef.current) {
|
|
113
|
+
updateJob({
|
|
114
|
+
id: currentJobIdRef.current,
|
|
115
|
+
updates: { status: "failed", error: err.message, progress: 0 },
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
onError?.(err.message);
|
|
120
|
+
},
|
|
121
|
+
[trackAsBackgroundJob, updateJob, onError],
|
|
122
|
+
);
|
|
123
|
+
|
|
74
124
|
const { generate, isGenerating } = useGenerationOrchestrator(
|
|
75
125
|
strategy,
|
|
76
126
|
{
|
|
@@ -83,18 +133,8 @@ export const useWizardGeneration = (
|
|
|
83
133
|
unknown: "An error occurred",
|
|
84
134
|
},
|
|
85
135
|
onCreditsExhausted,
|
|
86
|
-
onSuccess:
|
|
87
|
-
|
|
88
|
-
console.log("[useWizardGeneration] Success");
|
|
89
|
-
}
|
|
90
|
-
onSuccess?.(result);
|
|
91
|
-
},
|
|
92
|
-
onError: (err) => {
|
|
93
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
94
|
-
console.log("[useWizardGeneration] Error:", err.message);
|
|
95
|
-
}
|
|
96
|
-
onError?.(err.message);
|
|
97
|
-
},
|
|
136
|
+
onSuccess: handleSuccess,
|
|
137
|
+
onError: handleError,
|
|
98
138
|
},
|
|
99
139
|
);
|
|
100
140
|
|
|
@@ -116,6 +156,29 @@ export const useWizardGeneration = (
|
|
|
116
156
|
onError?.("Failed to build generation input");
|
|
117
157
|
return;
|
|
118
158
|
}
|
|
159
|
+
|
|
160
|
+
// Create background job for tracking
|
|
161
|
+
if (trackAsBackgroundJob) {
|
|
162
|
+
const jobId = `wizard-${scenario.id}-${Date.now()}`;
|
|
163
|
+
currentJobIdRef.current = jobId;
|
|
164
|
+
|
|
165
|
+
addJob({
|
|
166
|
+
id: jobId,
|
|
167
|
+
input: {
|
|
168
|
+
scenarioId: scenario.id,
|
|
169
|
+
scenarioTitle: scenario.title || scenario.id,
|
|
170
|
+
outputType: scenario.outputType || "video",
|
|
171
|
+
},
|
|
172
|
+
type: scenario.outputType || "video",
|
|
173
|
+
status: "processing",
|
|
174
|
+
progress: 10,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
178
|
+
console.log("[useWizardGeneration] Created background job:", jobId);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
119
182
|
generate(input);
|
|
120
183
|
})
|
|
121
184
|
.catch((error) => {
|
|
@@ -130,7 +193,19 @@ export const useWizardGeneration = (
|
|
|
130
193
|
if (!isGeneratingStep && hasStarted.current) {
|
|
131
194
|
hasStarted.current = false;
|
|
132
195
|
}
|
|
133
|
-
}, [
|
|
134
|
-
|
|
135
|
-
|
|
196
|
+
}, [
|
|
197
|
+
isGeneratingStep,
|
|
198
|
+
scenario,
|
|
199
|
+
wizardData,
|
|
200
|
+
isGenerating,
|
|
201
|
+
generate,
|
|
202
|
+
onError,
|
|
203
|
+
trackAsBackgroundJob,
|
|
204
|
+
addJob,
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
isGenerating,
|
|
209
|
+
currentJobId: currentJobIdRef.current,
|
|
210
|
+
};
|
|
136
211
|
};
|
|
@@ -20,7 +20,6 @@ export const IDENTITY_PRESERVATION_CORE = `CRITICAL IDENTITY PRESERVATION (HIGHE
|
|
|
20
20
|
"mandatory_rules": [
|
|
21
21
|
"The face must be EXACTLY as it appears in the reference photo - 100% identical",
|
|
22
22
|
"Preserve every facial detail: bone structure, eye shape, eye color, nose shape, lip shape",
|
|
23
|
-
"Maintain natural skin texture with pores, marks, and realistic details",
|
|
24
23
|
"Keep the person instantly recognizable - any deviation is NOT acceptable"
|
|
25
24
|
],
|
|
26
25
|
"forbidden_modifications": [
|
|
@@ -28,7 +27,6 @@ export const IDENTITY_PRESERVATION_CORE = `CRITICAL IDENTITY PRESERVATION (HIGHE
|
|
|
28
27
|
"Do NOT alter eye color, eye shape, or eye spacing",
|
|
29
28
|
"Do NOT modify nose shape, size, or position",
|
|
30
29
|
"Do NOT change lip shape, thickness, or natural expression",
|
|
31
|
-
"Do NOT alter skin tone or smooth skin texture",
|
|
32
30
|
"Do NOT remove natural features like freckles, moles, or wrinkles"
|
|
33
31
|
],
|
|
34
32
|
"verification": "Before output: confirm face matches reference photo with 100% accuracy"
|
|
@@ -36,17 +34,14 @@ export const IDENTITY_PRESERVATION_CORE = `CRITICAL IDENTITY PRESERVATION (HIGHE
|
|
|
36
34
|
|
|
37
35
|
/**
|
|
38
36
|
* Photorealistic rendering instruction
|
|
39
|
-
* Ensures high-quality, professional photography output
|
|
40
37
|
*/
|
|
41
38
|
export const PHOTOREALISTIC_RENDERING = `PHOTOREALISTIC RENDERING REQUIREMENTS:
|
|
42
39
|
{
|
|
43
|
-
"style": "
|
|
44
|
-
"quality": "
|
|
45
|
-
"lighting": "
|
|
46
|
-
"camera": "Shot on professional DSLR camera (Canon EOS R5, Sony A7R V, or equivalent)",
|
|
47
|
-
"lens": "Professional lens with appropriate focal length (35mm, 50mm, 85mm)",
|
|
40
|
+
"style": "PHOTOREALISTIC PHOTOGRAPH",
|
|
41
|
+
"quality": "high quality, professional photography",
|
|
42
|
+
"lighting": "Natural lighting with realistic shadows and highlights",
|
|
48
43
|
"prohibited": "STRICTLY NO anime, cartoons, illustrations, sketches, 3D renders, or non-photorealistic styles",
|
|
49
|
-
"output": "Must look like a real photograph
|
|
44
|
+
"output": "Must look like a real photograph"
|
|
50
45
|
}`;
|
|
51
46
|
|
|
52
47
|
/**
|
|
@@ -150,14 +145,14 @@ TRANSFORMATION REQUEST:
|
|
|
150
145
|
"environment_update": "${background.replace(/\n/g, ' ').trim()}"
|
|
151
146
|
},
|
|
152
147
|
"visual_constraints": {
|
|
153
|
-
"style_matching": "Render as a premium
|
|
148
|
+
"style_matching": "Render as a premium photograph",
|
|
154
149
|
"face_preservation": "Maintain 100% identity of the person",
|
|
155
|
-
"lighting": "Realistic professional
|
|
150
|
+
"lighting": "Realistic professional recording lighting",
|
|
156
151
|
"pose": "Natural, contextually appropriate pose"
|
|
157
152
|
}
|
|
158
153
|
}
|
|
159
154
|
|
|
160
|
-
FINAL COMMAND: Transform the input person into a
|
|
155
|
+
FINAL COMMAND: Transform the input person into a photorealistic ${styleName}. The result MUST be a real-life looking person in high-quality ${styleName} attire, maintaining perfect facial identity.`;
|
|
161
156
|
|
|
162
157
|
/**
|
|
163
158
|
* Simplified prompt for scenarios that already include detailed instructions
|
|
@@ -171,54 +166,3 @@ export const enhanceExistingPrompt = (existingPrompt: string): string => {
|
|
|
171
166
|
|
|
172
167
|
${existingPrompt}`;
|
|
173
168
|
};
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Multi-person identity preservation rules
|
|
177
|
-
* Ensures all people maintain their identities with strict rules
|
|
178
|
-
* Supports any number of people (1, 2, 3, N)
|
|
179
|
-
*/
|
|
180
|
-
export const MULTI_PERSON_PRESERVATION_RULES = {
|
|
181
|
-
requirement: "ALL individuals must have 100% identical facial appearance to their reference photos",
|
|
182
|
-
perPersonRule: "Use EXACTLY the person from @imageN - preserve 100% identical facial features",
|
|
183
|
-
forbidden: [
|
|
184
|
-
"Do NOT swap, mix, or blend facial features between people",
|
|
185
|
-
"Do NOT idealize or beautify any face",
|
|
186
|
-
"Do NOT alter facial proportions or characteristics",
|
|
187
|
-
],
|
|
188
|
-
positioning: "Natural positioning, all looking at camera with natural expressions",
|
|
189
|
-
} as const;
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Creates a multi-person prompt dynamically
|
|
193
|
-
*
|
|
194
|
-
* @param scenarioPrompt - The scenario description
|
|
195
|
-
* @param personCount - Number of people (1, 2, 3, N)
|
|
196
|
-
* @returns Complete prompt with identity preservation for all people
|
|
197
|
-
*/
|
|
198
|
-
export const createMultiPersonPrompt = (
|
|
199
|
-
scenarioPrompt: string,
|
|
200
|
-
personCount: number,
|
|
201
|
-
): string => {
|
|
202
|
-
const personRefs = Array.from({ length: personCount }, (_, i) =>
|
|
203
|
-
`Person ${i + 1}: @image${i + 1} - preserve 100% facial identity`
|
|
204
|
-
).join("\n ");
|
|
205
|
-
|
|
206
|
-
return `${IDENTITY_PRESERVATION_CORE}
|
|
207
|
-
|
|
208
|
-
MULTI-PERSON IDENTITY PRESERVATION (${personCount} people):
|
|
209
|
-
{
|
|
210
|
-
"requirement": "${MULTI_PERSON_PRESERVATION_RULES.requirement}",
|
|
211
|
-
"references": [
|
|
212
|
-
${personRefs}
|
|
213
|
-
],
|
|
214
|
-
"forbidden": ${JSON.stringify(MULTI_PERSON_PRESERVATION_RULES.forbidden)},
|
|
215
|
-
"positioning": "${MULTI_PERSON_PRESERVATION_RULES.positioning}"
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
${PHOTOREALISTIC_RENDERING}
|
|
219
|
-
|
|
220
|
-
${NATURAL_POSE_GUIDELINES}
|
|
221
|
-
|
|
222
|
-
SCENARIO DESCRIPTION:
|
|
223
|
-
${scenarioPrompt}`;
|
|
224
|
-
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { IDENTITY_PRESERVATION_CORE, NATURAL_POSE_GUIDELINES, PHOTOREALISTIC_RENDERING } from "./BasePromptStructure";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Multi-person identity preservation rules
|
|
5
|
+
* Ensures all people maintain their identities with strict rules
|
|
6
|
+
* Supports any number of people (1, 2, 3, N)
|
|
7
|
+
*/
|
|
8
|
+
export interface MultiPersonPreservationRules {
|
|
9
|
+
requirement: string;
|
|
10
|
+
perPersonRule: string;
|
|
11
|
+
forbidden: string[];
|
|
12
|
+
positioning: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const MULTI_PERSON_PRESERVATION_RULES: MultiPersonPreservationRules = {
|
|
16
|
+
requirement: "ALL individuals must have 100% identical facial appearance to their reference photos",
|
|
17
|
+
perPersonRule: "Use EXACTLY the person from @imageN - preserve 100% identical facial features",
|
|
18
|
+
forbidden: [
|
|
19
|
+
"Do NOT swap, mix, or blend facial features between people",
|
|
20
|
+
"Do NOT idealize or beautify any face",
|
|
21
|
+
"Do NOT alter facial proportions or characteristics",
|
|
22
|
+
],
|
|
23
|
+
positioning: "Natural positioning, all looking at camera with natural expressions",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a multi-person prompt dynamically
|
|
28
|
+
*
|
|
29
|
+
* @param scenarioPrompt - The scenario description
|
|
30
|
+
* @param personCount - Number of people (1, 2, 3, N)
|
|
31
|
+
* @returns Complete prompt with identity preservation for all people
|
|
32
|
+
*/
|
|
33
|
+
export const createMultiPersonPrompt = (
|
|
34
|
+
scenarioPrompt: string,
|
|
35
|
+
personCount: number,
|
|
36
|
+
): string => {
|
|
37
|
+
const personRefs = Array.from({ length: personCount }, (_, i) =>
|
|
38
|
+
`Person ${i + 1}: @image${i + 1} - preserve 100% facial identity`
|
|
39
|
+
).join("\n ");
|
|
40
|
+
|
|
41
|
+
return `${IDENTITY_PRESERVATION_CORE}
|
|
42
|
+
|
|
43
|
+
MULTI-PERSON IDENTITY PRESERVATION (${personCount} people):
|
|
44
|
+
{
|
|
45
|
+
"requirement": "${MULTI_PERSON_PRESERVATION_RULES.requirement}",
|
|
46
|
+
"references": [
|
|
47
|
+
${personRefs}
|
|
48
|
+
],
|
|
49
|
+
"forbidden": ${JSON.stringify(MULTI_PERSON_PRESERVATION_RULES.forbidden)},
|
|
50
|
+
"positioning": "${MULTI_PERSON_PRESERVATION_RULES.positioning}"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
${PHOTOREALISTIC_RENDERING}
|
|
54
|
+
|
|
55
|
+
${NATURAL_POSE_GUIDELINES}
|
|
56
|
+
|
|
57
|
+
SCENARIO DESCRIPTION:
|
|
58
|
+
${scenarioPrompt}`;
|
|
59
|
+
};
|
|
@@ -8,10 +8,8 @@
|
|
|
8
8
|
* These ensure consistent, realistic output across all scenarios
|
|
9
9
|
*/
|
|
10
10
|
export const PHOTOREALISTIC_BASE = {
|
|
11
|
-
quality: "
|
|
12
|
-
|
|
13
|
-
camera: "shot on professional DSLR camera with 85mm f/1.4 lens, shallow depth of field, soft bokeh background",
|
|
14
|
-
lighting: "natural volumetric lighting, cinematic color grading",
|
|
11
|
+
quality: "photorealistic",
|
|
12
|
+
lighting: "cinematic lighting",
|
|
15
13
|
} as const;
|
|
16
14
|
|
|
17
15
|
/**
|
|
@@ -25,7 +23,7 @@ export const createPhotorealisticPrompt = (
|
|
|
25
23
|
lightingOverride?: string,
|
|
26
24
|
): string => {
|
|
27
25
|
const lighting = lightingOverride ?? PHOTOREALISTIC_BASE.lighting;
|
|
28
|
-
return `${PHOTOREALISTIC_BASE.quality}, ${scene}, ${
|
|
26
|
+
return `${PHOTOREALISTIC_BASE.quality}, ${scene}, ${lighting}`;
|
|
29
27
|
};
|
|
30
28
|
|
|
31
29
|
export const createStoryTemplate = (
|