@umituz/react-native-ai-generation-content 1.36.1 → 1.37.0
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 +124 -199
- package/src/domains/creations/presentation/components/GalleryResultPreview.tsx +88 -0
- package/src/domains/creations/presentation/hooks/useGalleryCallbacks.ts +127 -0
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +37 -123
- package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.factory.ts +0 -1
- package/src/domains/generation/wizard/presentation/components/GenericWizardFlow.tsx +5 -231
- package/src/domains/generation/wizard/presentation/components/WizardContinueButton.tsx +73 -0
- package/src/domains/generation/wizard/presentation/components/WizardFlowContent.tsx +181 -0
- package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.tsx +18 -134
- package/src/domains/generation/wizard/presentation/components/step-renderers/renderPhotoUploadStep.tsx +52 -0
- package/src/domains/generation/wizard/presentation/components/step-renderers/renderSelectionStep.tsx +59 -0
- package/src/domains/generation/wizard/presentation/components/step-renderers/renderTextInputStep.tsx +62 -0
- package/src/domains/generation/wizard/presentation/hooks/useWizardFlowHandlers.ts +133 -0
- package/src/domains/generation/wizard/presentation/hooks/useWizardGeneration.ts +1 -2
- package/src/domains/generation/wizard/presentation/screens/GenericPhotoUploadScreen.tsx +15 -83
- package/src/domains/generation/wizard/presentation/screens/SelectionScreen.tsx +55 -134
- package/src/domains/result-preview/presentation/components/GenerationErrorScreen.tsx +19 -122
- package/src/domains/result-preview/presentation/hooks/useResultActions.ts +16 -131
- package/src/domains/result-preview/presentation/utils/media-file-utils.ts +78 -0
- package/src/domains/scenarios/presentation/components/ScenarioContinueButton.tsx +76 -0
- package/src/domains/scenarios/presentation/hooks/useHierarchicalScenarios.ts +70 -0
- package/src/domains/scenarios/presentation/screens/HierarchicalScenarioListScreen.tsx +37 -170
- package/src/presentation/hooks/generation/moderation-handler.ts +77 -0
- package/src/presentation/hooks/generation/orchestrator.ts +33 -125
|
@@ -2,10 +2,6 @@ import React, { useState, useMemo, useCallback, useEffect, useRef } from "react"
|
|
|
2
2
|
import { View, FlatList, RefreshControl, StyleSheet } from "react-native";
|
|
3
3
|
import {
|
|
4
4
|
useAppDesignTokens,
|
|
5
|
-
useAlert,
|
|
6
|
-
AlertType,
|
|
7
|
-
AlertMode,
|
|
8
|
-
useSharing,
|
|
9
5
|
FilterSheet,
|
|
10
6
|
ScreenLayout,
|
|
11
7
|
useAppFocusEffect,
|
|
@@ -13,10 +9,9 @@ import {
|
|
|
13
9
|
import { useCreations } from "../hooks/useCreations";
|
|
14
10
|
import { useDeleteCreation } from "../hooks/useDeleteCreation";
|
|
15
11
|
import { useGalleryFilters } from "../hooks/useGalleryFilters";
|
|
12
|
+
import { useGalleryCallbacks } from "../hooks/useGalleryCallbacks";
|
|
16
13
|
import { GalleryHeader, CreationCard, GalleryEmptyStates } from "../components";
|
|
17
|
-
import {
|
|
18
|
-
import { StarRatingPicker } from "../../../result-preview/presentation/components/StarRatingPicker";
|
|
19
|
-
import { useResultActions } from "../../../result-preview/presentation/hooks/useResultActions";
|
|
14
|
+
import { GalleryResultPreview } from "../components/GalleryResultPreview";
|
|
20
15
|
import { usePendingJobs } from "../../../../presentation/hooks/use-pending-jobs";
|
|
21
16
|
import { MEDIA_FILTER_OPTIONS, STATUS_FILTER_OPTIONS } from "../../domain/types/creation-filter";
|
|
22
17
|
import { getPreviewUrl } from "../../domain/utils";
|
|
@@ -33,7 +28,6 @@ interface CreationsGalleryScreenProps {
|
|
|
33
28
|
readonly onEmptyAction?: () => void;
|
|
34
29
|
readonly emptyActionLabel?: string;
|
|
35
30
|
readonly showFilter?: boolean;
|
|
36
|
-
/** Show pending generation jobs badge in header */
|
|
37
31
|
readonly showPendingJobs?: boolean;
|
|
38
32
|
}
|
|
39
33
|
|
|
@@ -49,16 +43,13 @@ export function CreationsGalleryScreen({
|
|
|
49
43
|
showPendingJobs = true,
|
|
50
44
|
}: CreationsGalleryScreenProps) {
|
|
51
45
|
const tokens = useAppDesignTokens();
|
|
52
|
-
const { share } = useSharing();
|
|
53
|
-
const alert = useAlert();
|
|
54
46
|
const [selectedCreation, setSelectedCreation] = useState<Creation | null>(null);
|
|
55
47
|
const [showRatingPicker, setShowRatingPicker] = useState(false);
|
|
56
48
|
const hasAutoSelectedRef = useRef(false);
|
|
57
49
|
|
|
58
50
|
const { data: creations, isLoading, refetch } = useCreations({ userId, repository });
|
|
59
|
-
|
|
60
|
-
// Background jobs for pending generations
|
|
61
51
|
const { jobs: pendingJobs } = usePendingJobs();
|
|
52
|
+
const deleteMutation = useDeleteCreation({ userId, repository });
|
|
62
53
|
|
|
63
54
|
// Auto-select creation when initialCreationId is provided
|
|
64
55
|
useEffect(() => {
|
|
@@ -71,16 +62,16 @@ export function CreationsGalleryScreen({
|
|
|
71
62
|
}
|
|
72
63
|
}, [initialCreationId, creations]);
|
|
73
64
|
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
65
|
+
const callbacks = useGalleryCallbacks({
|
|
66
|
+
userId,
|
|
67
|
+
repository,
|
|
68
|
+
config,
|
|
69
|
+
t,
|
|
70
|
+
deleteMutation,
|
|
71
|
+
refetch: async () => { await refetch(); },
|
|
72
|
+
setSelectedCreation,
|
|
73
|
+
setShowRatingPicker,
|
|
74
|
+
selectedCreation,
|
|
84
75
|
});
|
|
85
76
|
|
|
86
77
|
const statusOptions = config.filterConfig?.statusOptions ?? STATUS_FILTER_OPTIONS;
|
|
@@ -92,57 +83,6 @@ export function CreationsGalleryScreen({
|
|
|
92
83
|
|
|
93
84
|
useAppFocusEffect(useCallback(() => { void refetch(); }, [refetch]));
|
|
94
85
|
|
|
95
|
-
const handleShareCard = useCallback((c: Creation) => {
|
|
96
|
-
void share(c.uri, { dialogTitle: t("common.share") });
|
|
97
|
-
}, [share, t]);
|
|
98
|
-
|
|
99
|
-
const handleDelete = useCallback((c: Creation) => {
|
|
100
|
-
alert.show(AlertType.WARNING, AlertMode.MODAL, t(config.translations.deleteTitle), t(config.translations.deleteMessage), {
|
|
101
|
-
actions: [
|
|
102
|
-
{ id: "cancel", label: t("common.cancel"), onPress: () => {} },
|
|
103
|
-
{ id: "delete", label: t("common.delete"), style: "destructive", onPress: async () => {
|
|
104
|
-
await deleteMutation.mutateAsync(c.id);
|
|
105
|
-
}}
|
|
106
|
-
]
|
|
107
|
-
});
|
|
108
|
-
}, [alert, config, deleteMutation, t]);
|
|
109
|
-
|
|
110
|
-
const handleFavorite = useCallback((c: Creation, isFavorite: boolean) => {
|
|
111
|
-
void (async () => {
|
|
112
|
-
if (!userId) return;
|
|
113
|
-
const success = await repository.updateFavorite(userId, c.id, isFavorite);
|
|
114
|
-
if (success) void refetch();
|
|
115
|
-
})();
|
|
116
|
-
}, [userId, repository, refetch]);
|
|
117
|
-
|
|
118
|
-
const handleCardPress = useCallback((item: Creation) => {
|
|
119
|
-
setSelectedCreation(item);
|
|
120
|
-
}, []);
|
|
121
|
-
|
|
122
|
-
const handleBack = useCallback(() => {
|
|
123
|
-
setSelectedCreation(null);
|
|
124
|
-
}, []);
|
|
125
|
-
|
|
126
|
-
const handleTryAgain = useCallback(() => {
|
|
127
|
-
setSelectedCreation(null);
|
|
128
|
-
}, []);
|
|
129
|
-
|
|
130
|
-
const handleOpenRatingPicker = useCallback(() => {
|
|
131
|
-
setShowRatingPicker(true);
|
|
132
|
-
}, []);
|
|
133
|
-
|
|
134
|
-
const handleSubmitRating = useCallback((rating: number, description: string) => {
|
|
135
|
-
if (!userId || !selectedCreation) return;
|
|
136
|
-
void (async () => {
|
|
137
|
-
const success = await repository.rate(userId, selectedCreation.id, rating, description);
|
|
138
|
-
if (success) {
|
|
139
|
-
setSelectedCreation({ ...selectedCreation, rating, ratedAt: new Date() });
|
|
140
|
-
alert.show(AlertType.SUCCESS, AlertMode.TOAST, t("result.rateSuccessTitle"), t("result.rateSuccessMessage"));
|
|
141
|
-
void refetch();
|
|
142
|
-
}
|
|
143
|
-
})();
|
|
144
|
-
}, [userId, selectedCreation, repository, alert, t, refetch]);
|
|
145
|
-
|
|
146
86
|
const filterButtons = useMemo(() => {
|
|
147
87
|
const buttons = [];
|
|
148
88
|
if (showStatusFilter) {
|
|
@@ -168,10 +108,7 @@ export function CreationsGalleryScreen({
|
|
|
168
108
|
|
|
169
109
|
const getScenarioTitle = useCallback((type: string): string => {
|
|
170
110
|
const typeConfig = config.types?.find((tc) => tc.id === type);
|
|
171
|
-
|
|
172
|
-
return t(typeConfig.labelKey);
|
|
173
|
-
}
|
|
174
|
-
return type.split("_").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
111
|
+
return typeConfig?.labelKey ? t(typeConfig.labelKey) : type.split("_").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
175
112
|
}, [config.types, t]);
|
|
176
113
|
|
|
177
114
|
const renderItem = useCallback(({ item }: { item: Creation }) => (
|
|
@@ -179,24 +116,21 @@ export function CreationsGalleryScreen({
|
|
|
179
116
|
creation={item}
|
|
180
117
|
titleText={getScenarioTitle(item.type)}
|
|
181
118
|
callbacks={{
|
|
182
|
-
onPress: () => handleCardPress(item),
|
|
183
|
-
onShare: async () => handleShareCard(item),
|
|
184
|
-
onDelete: () => handleDelete(item),
|
|
185
|
-
onFavorite: () => handleFavorite(item, !item.isFavorite),
|
|
119
|
+
onPress: () => callbacks.handleCardPress(item),
|
|
120
|
+
onShare: async () => callbacks.handleShareCard(item),
|
|
121
|
+
onDelete: () => callbacks.handleDelete(item),
|
|
122
|
+
onFavorite: () => callbacks.handleFavorite(item, !item.isFavorite),
|
|
186
123
|
}}
|
|
187
124
|
/>
|
|
188
|
-
), [
|
|
125
|
+
), [callbacks, getScenarioTitle]);
|
|
189
126
|
|
|
190
|
-
// Calculate active pending jobs count once
|
|
191
127
|
const activePendingCount = useMemo(() => {
|
|
192
128
|
if (!showPendingJobs) return 0;
|
|
193
129
|
return pendingJobs.filter((j) => j.status === "processing" || j.status === "queued").length;
|
|
194
130
|
}, [showPendingJobs, pendingJobs]);
|
|
195
131
|
|
|
196
132
|
const renderHeader = useMemo(() => {
|
|
197
|
-
|
|
198
|
-
if (!hasCreations && !isLoading) return null;
|
|
199
|
-
|
|
133
|
+
if (!creations?.length && !isLoading) return null;
|
|
200
134
|
return (
|
|
201
135
|
<View style={[styles.header, { backgroundColor: tokens.colors.surface, borderBottomColor: tokens.colors.border }]}>
|
|
202
136
|
<GalleryHeader
|
|
@@ -227,45 +161,25 @@ export function CreationsGalleryScreen({
|
|
|
227
161
|
/>
|
|
228
162
|
), [isLoading, creations, filters.isFiltered, tokens, t, config, emptyActionLabel, onEmptyAction, filters.clearAllFilters]);
|
|
229
163
|
|
|
230
|
-
|
|
164
|
+
const selectedImageUrl = selectedCreation ? (getPreviewUrl(selectedCreation.output) || selectedCreation.uri) : undefined;
|
|
165
|
+
const selectedVideoUrl = selectedCreation?.output?.videoUrl;
|
|
231
166
|
const hasMediaToShow = selectedImageUrl || selectedVideoUrl;
|
|
167
|
+
|
|
232
168
|
if (selectedCreation && hasMediaToShow) {
|
|
233
|
-
const hasRating = selectedCreation.rating !== undefined && selectedCreation.rating !== null;
|
|
234
169
|
return (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
showTryAgain
|
|
249
|
-
showRating={!hasRating}
|
|
250
|
-
translations={{
|
|
251
|
-
title: t(config.translations.title),
|
|
252
|
-
saveButton: t("result.saveButton"),
|
|
253
|
-
saving: t("result.saving"),
|
|
254
|
-
shareButton: t("result.shareButton"),
|
|
255
|
-
sharing: t("result.sharing"),
|
|
256
|
-
tryAnother: t("result.tryAnother"),
|
|
257
|
-
}}
|
|
258
|
-
/>
|
|
259
|
-
<StarRatingPicker
|
|
260
|
-
visible={showRatingPicker}
|
|
261
|
-
onClose={() => setShowRatingPicker(false)}
|
|
262
|
-
onRate={handleSubmitRating}
|
|
263
|
-
title={t("result.rateTitle")}
|
|
264
|
-
submitLabel={t("common.submit")}
|
|
265
|
-
cancelLabel={t("common.cancel")}
|
|
266
|
-
descriptionPlaceholder={t("result.feedbackPlaceholder")}
|
|
267
|
-
/>
|
|
268
|
-
</>
|
|
170
|
+
<GalleryResultPreview
|
|
171
|
+
selectedCreation={selectedCreation}
|
|
172
|
+
imageUrl={selectedVideoUrl ? undefined : selectedImageUrl}
|
|
173
|
+
videoUrl={selectedVideoUrl}
|
|
174
|
+
showRatingPicker={showRatingPicker}
|
|
175
|
+
config={config}
|
|
176
|
+
t={t}
|
|
177
|
+
onBack={callbacks.handleBack}
|
|
178
|
+
onTryAgain={callbacks.handleTryAgain}
|
|
179
|
+
onRate={callbacks.handleOpenRatingPicker}
|
|
180
|
+
onSubmitRating={callbacks.handleSubmitRating}
|
|
181
|
+
onCloseRating={() => setShowRatingPicker(false)}
|
|
182
|
+
/>
|
|
269
183
|
);
|
|
270
184
|
}
|
|
271
185
|
|
|
@@ -277,7 +191,7 @@ export function CreationsGalleryScreen({
|
|
|
277
191
|
keyExtractor={(item) => item.id}
|
|
278
192
|
ListHeaderComponent={renderHeader}
|
|
279
193
|
ListEmptyComponent={renderEmpty}
|
|
280
|
-
contentContainerStyle={[styles.listContent, (!filters.filtered
|
|
194
|
+
contentContainerStyle={[styles.listContent, (!filters.filtered?.length) && styles.emptyContent]}
|
|
281
195
|
showsVerticalScrollIndicator={false}
|
|
282
196
|
refreshControl={<RefreshControl refreshing={isLoading} onRefresh={() => void refetch()} tintColor={tokens.colors.primary} />}
|
|
283
197
|
/>
|
|
@@ -4,33 +4,13 @@
|
|
|
4
4
|
* Supports both scenario object and scenarioId (resolved from registry)
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import React, { useMemo
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
useAppDesignTokens,
|
|
11
|
-
useAlert,
|
|
12
|
-
AlertType,
|
|
13
|
-
AlertMode,
|
|
14
|
-
} from "@umituz/react-native-design-system";
|
|
15
|
-
import { useFlow } from "../../../infrastructure/flow/useFlow";
|
|
16
|
-
import {
|
|
17
|
-
StepType,
|
|
18
|
-
type StepDefinition,
|
|
19
|
-
} from "../../../../../domain/entities/flow-config.types";
|
|
7
|
+
import React, { useMemo } from "react";
|
|
8
|
+
import type { StepType } from "../../../../../domain/entities/flow-config.types";
|
|
20
9
|
import type { WizardFeatureConfig } from "../../domain/entities/wizard-config.types";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
useWizardGeneration,
|
|
24
|
-
type WizardScenarioData,
|
|
25
|
-
} from "../hooks/useWizardGeneration";
|
|
10
|
+
import type { WizardScenarioData } from "../hooks/useWizardGeneration";
|
|
26
11
|
import type { AlertMessages } from "../../../../../presentation/hooks/generation/types";
|
|
27
|
-
import type { UploadedImage } from "../../../../../presentation/hooks/generation/useAIGenerateState";
|
|
28
|
-
import type { Creation } from "../../../../creations/domain/entities/Creation";
|
|
29
|
-
import { createCreationsRepository } from "../../../../creations";
|
|
30
|
-
import { useResultActions } from "../../../../result-preview/presentation/hooks/useResultActions";
|
|
31
12
|
import { validateScenario } from "../utilities/validateScenario";
|
|
32
|
-
import {
|
|
33
|
-
import { StarRatingPicker } from "../../../../result-preview/presentation/components/StarRatingPicker";
|
|
13
|
+
import { WizardFlowContent } from "./WizardFlowContent";
|
|
34
14
|
import {
|
|
35
15
|
getConfiguredScenario,
|
|
36
16
|
getDefaultOutputType,
|
|
@@ -83,9 +63,6 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = (props) => {
|
|
|
83
63
|
renderResult,
|
|
84
64
|
} = props;
|
|
85
65
|
|
|
86
|
-
const tokens = useAppDesignTokens();
|
|
87
|
-
const alert = useAlert();
|
|
88
|
-
|
|
89
66
|
// Resolve scenario from prop or registry
|
|
90
67
|
const scenario = useMemo<WizardScenarioData | undefined>(() => {
|
|
91
68
|
if (scenarioProp) {
|
|
@@ -118,10 +95,7 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = (props) => {
|
|
|
118
95
|
return undefined;
|
|
119
96
|
}, [scenarioProp, scenarioId]);
|
|
120
97
|
|
|
121
|
-
const validatedScenario = useMemo(
|
|
122
|
-
() => validateScenario(scenario),
|
|
123
|
-
[scenario],
|
|
124
|
-
);
|
|
98
|
+
const validatedScenario = useMemo(() => validateScenario(scenario), [scenario]);
|
|
125
99
|
|
|
126
100
|
return (
|
|
127
101
|
<WizardFlowContent
|
|
@@ -139,209 +113,9 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = (props) => {
|
|
|
139
113
|
onBack={onBack}
|
|
140
114
|
onTryAgain={onTryAgain}
|
|
141
115
|
t={t}
|
|
142
|
-
tokens={tokens}
|
|
143
|
-
alert={alert}
|
|
144
116
|
renderPreview={renderPreview}
|
|
145
117
|
renderGenerating={renderGenerating}
|
|
146
118
|
renderResult={renderResult}
|
|
147
119
|
/>
|
|
148
120
|
);
|
|
149
121
|
};
|
|
150
|
-
|
|
151
|
-
interface WizardFlowContentProps
|
|
152
|
-
extends Omit<GenericWizardFlowProps, "scenarioId" | "translations"> {
|
|
153
|
-
readonly validatedScenario: WizardScenarioData;
|
|
154
|
-
readonly tokens: ReturnType<typeof useAppDesignTokens>;
|
|
155
|
-
readonly alert: ReturnType<typeof useAlert>;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const WizardFlowContent: React.FC<WizardFlowContentProps> = (props) => {
|
|
159
|
-
const {
|
|
160
|
-
featureConfig,
|
|
161
|
-
scenario,
|
|
162
|
-
validatedScenario,
|
|
163
|
-
userId,
|
|
164
|
-
alertMessages,
|
|
165
|
-
skipResultStep = false,
|
|
166
|
-
onStepChange,
|
|
167
|
-
onGenerationStart,
|
|
168
|
-
onGenerationComplete,
|
|
169
|
-
onGenerationError,
|
|
170
|
-
onCreditsExhausted,
|
|
171
|
-
onBack,
|
|
172
|
-
onTryAgain,
|
|
173
|
-
t,
|
|
174
|
-
tokens,
|
|
175
|
-
alert,
|
|
176
|
-
renderPreview,
|
|
177
|
-
renderGenerating,
|
|
178
|
-
renderResult,
|
|
179
|
-
} = props;
|
|
180
|
-
|
|
181
|
-
const [currentCreation, setCurrentCreation] = useState<Creation | null>(null);
|
|
182
|
-
const [showRatingPicker, setShowRatingPicker] = useState(false);
|
|
183
|
-
const [hasRated, setHasRated] = useState(false);
|
|
184
|
-
const prevStepIdRef = useRef<string | undefined>(undefined);
|
|
185
|
-
const repository = useMemo(() => createCreationsRepository("creations"), []);
|
|
186
|
-
|
|
187
|
-
const flowSteps = useMemo<StepDefinition[]>(
|
|
188
|
-
() =>
|
|
189
|
-
buildFlowStepsFromWizard(featureConfig, {
|
|
190
|
-
includePreview: true,
|
|
191
|
-
includeGenerating: true,
|
|
192
|
-
includeResult: !skipResultStep,
|
|
193
|
-
}),
|
|
194
|
-
[featureConfig, skipResultStep],
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
const flow = useFlow({ steps: flowSteps, initialStepIndex: 0 });
|
|
198
|
-
const {
|
|
199
|
-
currentStep,
|
|
200
|
-
currentStepIndex,
|
|
201
|
-
customData,
|
|
202
|
-
generationProgress,
|
|
203
|
-
generationResult,
|
|
204
|
-
nextStep,
|
|
205
|
-
previousStep,
|
|
206
|
-
setCustomData,
|
|
207
|
-
setResult,
|
|
208
|
-
} = flow;
|
|
209
|
-
|
|
210
|
-
const resultImageUrl =
|
|
211
|
-
currentCreation?.output?.imageUrl || currentCreation?.uri || "";
|
|
212
|
-
const resultVideoUrl = currentCreation?.output?.videoUrl || "";
|
|
213
|
-
const { isSaving, isSharing, handleDownload, handleShare } = useResultActions(
|
|
214
|
-
{ imageUrl: resultImageUrl, videoUrl: resultVideoUrl },
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
const handleGenerationComplete = useCallback(
|
|
218
|
-
(result: unknown) => {
|
|
219
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
220
|
-
console.log("[WizardFlowContent] Generation completed");
|
|
221
|
-
}
|
|
222
|
-
setResult(result);
|
|
223
|
-
setCurrentCreation(result as Creation);
|
|
224
|
-
onGenerationComplete?.(result);
|
|
225
|
-
if (!skipResultStep) nextStep();
|
|
226
|
-
},
|
|
227
|
-
[setResult, nextStep, onGenerationComplete, skipResultStep],
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
useWizardGeneration({
|
|
231
|
-
scenario: validatedScenario,
|
|
232
|
-
wizardData: customData,
|
|
233
|
-
userId,
|
|
234
|
-
isGeneratingStep: currentStep?.type === StepType.GENERATING,
|
|
235
|
-
alertMessages,
|
|
236
|
-
onSuccess: handleGenerationComplete,
|
|
237
|
-
onError: onGenerationError,
|
|
238
|
-
onCreditsExhausted,
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
useEffect(() => {
|
|
242
|
-
if (currentStep && onStepChange && prevStepIdRef.current !== currentStep.id) {
|
|
243
|
-
prevStepIdRef.current = currentStep.id;
|
|
244
|
-
onStepChange(currentStep.id, currentStep.type);
|
|
245
|
-
}
|
|
246
|
-
}, [currentStep, onStepChange]);
|
|
247
|
-
|
|
248
|
-
const handleDismissGenerating = useCallback(() => {
|
|
249
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
250
|
-
console.log("[WizardFlowContent] Dismissing - generation continues");
|
|
251
|
-
}
|
|
252
|
-
alert.show(
|
|
253
|
-
AlertType.INFO,
|
|
254
|
-
AlertMode.TOAST,
|
|
255
|
-
t("generator.backgroundTitle"),
|
|
256
|
-
t("generator.backgroundMessage"),
|
|
257
|
-
);
|
|
258
|
-
onBack?.();
|
|
259
|
-
}, [alert, t, onBack]);
|
|
260
|
-
|
|
261
|
-
const handleBack = useCallback(() => {
|
|
262
|
-
if (currentStepIndex === 0) onBack?.();
|
|
263
|
-
else previousStep();
|
|
264
|
-
}, [currentStepIndex, previousStep, onBack]);
|
|
265
|
-
|
|
266
|
-
const handleNextStep = useCallback(() => {
|
|
267
|
-
const nextStepDef = flowSteps[currentStepIndex + 1];
|
|
268
|
-
if (nextStepDef?.type === StepType.GENERATING && onGenerationStart) {
|
|
269
|
-
onGenerationStart(customData, nextStep);
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
nextStep();
|
|
273
|
-
}, [currentStepIndex, flowSteps, customData, onGenerationStart, nextStep]);
|
|
274
|
-
|
|
275
|
-
const handlePhotoContinue = useCallback(
|
|
276
|
-
(stepId: string, image: UploadedImage) => {
|
|
277
|
-
setCustomData(stepId, image);
|
|
278
|
-
handleNextStep();
|
|
279
|
-
},
|
|
280
|
-
[setCustomData, handleNextStep],
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
const handleSubmitRating = useCallback(
|
|
284
|
-
async (rating: number, description: string) => {
|
|
285
|
-
if (!currentCreation?.id || !userId) return;
|
|
286
|
-
const success = await repository.rate(
|
|
287
|
-
userId,
|
|
288
|
-
currentCreation.id,
|
|
289
|
-
rating,
|
|
290
|
-
description,
|
|
291
|
-
);
|
|
292
|
-
if (success) {
|
|
293
|
-
setHasRated(true);
|
|
294
|
-
alert.show(
|
|
295
|
-
AlertType.SUCCESS,
|
|
296
|
-
AlertMode.TOAST,
|
|
297
|
-
t("result.rateSuccessTitle"),
|
|
298
|
-
t("result.rateSuccessMessage"),
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
setShowRatingPicker(false);
|
|
302
|
-
},
|
|
303
|
-
[currentCreation, userId, repository, alert, t],
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
return (
|
|
307
|
-
<View
|
|
308
|
-
style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
|
|
309
|
-
>
|
|
310
|
-
<WizardStepRenderer
|
|
311
|
-
step={currentStep}
|
|
312
|
-
scenario={scenario}
|
|
313
|
-
customData={customData}
|
|
314
|
-
generationProgress={generationProgress}
|
|
315
|
-
generationResult={generationResult}
|
|
316
|
-
isSaving={isSaving}
|
|
317
|
-
isSharing={isSharing}
|
|
318
|
-
showRating={Boolean(userId) && !hasRated}
|
|
319
|
-
onNext={handleNextStep}
|
|
320
|
-
onBack={handleBack}
|
|
321
|
-
onPhotoContinue={handlePhotoContinue}
|
|
322
|
-
onDownload={handleDownload}
|
|
323
|
-
onShare={handleShare}
|
|
324
|
-
onRate={() => setShowRatingPicker(true)}
|
|
325
|
-
onTryAgain={onTryAgain}
|
|
326
|
-
onDismissGenerating={handleDismissGenerating}
|
|
327
|
-
t={t}
|
|
328
|
-
renderPreview={renderPreview}
|
|
329
|
-
renderGenerating={renderGenerating}
|
|
330
|
-
renderResult={renderResult}
|
|
331
|
-
/>
|
|
332
|
-
<StarRatingPicker
|
|
333
|
-
visible={showRatingPicker}
|
|
334
|
-
onClose={() => setShowRatingPicker(false)}
|
|
335
|
-
onRate={handleSubmitRating}
|
|
336
|
-
title={t("result.rateTitle")}
|
|
337
|
-
submitLabel={t("common.submit")}
|
|
338
|
-
cancelLabel={t("common.cancel")}
|
|
339
|
-
descriptionPlaceholder={t("result.feedbackPlaceholder")}
|
|
340
|
-
/>
|
|
341
|
-
</View>
|
|
342
|
-
);
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
const styles = StyleSheet.create({
|
|
346
|
-
container: { flex: 1 },
|
|
347
|
-
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wizard Continue Button Component
|
|
3
|
+
* Reusable continue button for wizard screens
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
AtomicIcon,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
type IconName,
|
|
13
|
+
} from "@umituz/react-native-design-system";
|
|
14
|
+
|
|
15
|
+
export interface WizardContinueButtonProps {
|
|
16
|
+
readonly canContinue: boolean;
|
|
17
|
+
readonly onPress: () => void;
|
|
18
|
+
readonly label: string;
|
|
19
|
+
readonly icon?: IconName;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function WizardContinueButton({
|
|
23
|
+
canContinue,
|
|
24
|
+
onPress,
|
|
25
|
+
label,
|
|
26
|
+
icon = "chevron-forward-outline",
|
|
27
|
+
}: WizardContinueButtonProps) {
|
|
28
|
+
const tokens = useAppDesignTokens();
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<TouchableOpacity
|
|
32
|
+
onPress={onPress}
|
|
33
|
+
disabled={!canContinue}
|
|
34
|
+
activeOpacity={0.7}
|
|
35
|
+
style={[
|
|
36
|
+
styles.button,
|
|
37
|
+
{
|
|
38
|
+
backgroundColor: canContinue ? tokens.colors.primary : tokens.colors.surfaceVariant,
|
|
39
|
+
opacity: canContinue ? 1 : 0.5,
|
|
40
|
+
paddingHorizontal: tokens.spacing.md,
|
|
41
|
+
paddingVertical: tokens.spacing.xs,
|
|
42
|
+
borderRadius: tokens.borders.radius.full,
|
|
43
|
+
},
|
|
44
|
+
]}
|
|
45
|
+
>
|
|
46
|
+
<AtomicText
|
|
47
|
+
type="bodyMedium"
|
|
48
|
+
style={[
|
|
49
|
+
styles.text,
|
|
50
|
+
{ color: canContinue ? tokens.colors.onPrimary : tokens.colors.textSecondary },
|
|
51
|
+
]}
|
|
52
|
+
>
|
|
53
|
+
{label}
|
|
54
|
+
</AtomicText>
|
|
55
|
+
<AtomicIcon
|
|
56
|
+
name={icon}
|
|
57
|
+
size="sm"
|
|
58
|
+
color={canContinue ? "onPrimary" : "textSecondary"}
|
|
59
|
+
/>
|
|
60
|
+
</TouchableOpacity>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const styles = StyleSheet.create({
|
|
65
|
+
button: {
|
|
66
|
+
flexDirection: "row",
|
|
67
|
+
alignItems: "center",
|
|
68
|
+
},
|
|
69
|
+
text: {
|
|
70
|
+
fontWeight: "800",
|
|
71
|
+
marginRight: 4,
|
|
72
|
+
},
|
|
73
|
+
});
|