@umituz/react-native-ai-generation-content 1.17.225 → 1.17.226

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.
Files changed (30) hide show
  1. package/package.json +1 -1
  2. package/src/features/ai-hug/presentation/components/AIHugFeature.tsx +2 -2
  3. package/src/features/ai-kiss/presentation/components/AIKissFeature.tsx +5 -2
  4. package/src/features/anime-selfie/presentation/components/AnimeSelfieFeature.tsx +7 -3
  5. package/src/features/face-swap/presentation/components/FaceSwapFeature.tsx +7 -3
  6. package/src/features/hd-touch-up/presentation/components/HDTouchUpFeature.tsx +7 -3
  7. package/src/features/image-to-image/domain/types/base.types.ts +41 -226
  8. package/src/features/image-to-image/domain/types/partials/config.types.ts +37 -0
  9. package/src/features/image-to-image/domain/types/partials/hook.types.ts +55 -0
  10. package/src/features/image-to-image/domain/types/partials/metadata.types.ts +32 -0
  11. package/src/features/image-to-image/domain/types/partials/result.types.ts +44 -0
  12. package/src/features/image-to-image/domain/types/partials/state.types.ts +34 -0
  13. package/src/features/image-to-image/domain/types/partials/translation.types.ts +57 -0
  14. package/src/features/photo-restoration/presentation/components/PhotoRestoreFeature.tsx +7 -3
  15. package/src/features/remove-background/presentation/components/RemoveBackgroundFeature.tsx +3 -3
  16. package/src/features/remove-object/presentation/components/RemoveObjectFeature.tsx +3 -3
  17. package/src/features/replace-background/presentation/components/ReplaceBackgroundFeature.tsx +7 -3
  18. package/src/features/upscaling/presentation/components/UpscaleFeature.tsx +7 -3
  19. package/src/infrastructure/utils/error-classifier.util.ts +8 -40
  20. package/src/infrastructure/utils/error-patterns.constants.ts +41 -0
  21. package/src/presentation/hooks/base/use-dual-image-feature.ts +68 -76
  22. package/src/presentation/hooks/base/use-image-with-prompt-feature.ts +70 -73
  23. package/src/presentation/hooks/base/utils/feature-state.factory.ts +133 -0
  24. package/src/presentation/layouts/index.ts +14 -11
  25. package/src/presentation/layouts/types/feature-states.ts +38 -0
  26. package/src/presentation/layouts/types/index.ts +35 -0
  27. package/src/presentation/layouts/types/input-props.ts +34 -0
  28. package/src/presentation/layouts/{types.ts → types/layout-props.ts} +22 -125
  29. package/src/presentation/layouts/types/result-props.ts +33 -0
  30. package/src/presentation/layouts/types/translations.ts +35 -0
@@ -7,7 +7,11 @@
7
7
  import React, { useMemo } from "react";
8
8
  import { PhotoUploadCard } from "../../../../presentation/components/PhotoUploadCard";
9
9
  import { SingleImageFeatureLayout } from "../../../../presentation/layouts";
10
- import type { ProcessingModalRenderProps } from "../../../../presentation/layouts";
10
+ import type {
11
+ ProcessingModalRenderProps,
12
+ SingleImageInputRenderProps,
13
+ CustomResultRenderProps,
14
+ } from "../../../../presentation/layouts";
11
15
  import { PhotoRestoreResultView } from "./PhotoRestoreResultView";
12
16
  import { usePhotoRestoreFeature } from "../hooks";
13
17
  import type {
@@ -60,7 +64,7 @@ export const PhotoRestoreFeature: React.FC<PhotoRestoreFeatureProps> = ({
60
64
  translations={translations}
61
65
  modalTranslations={modalTranslations}
62
66
  renderProcessingModal={renderProcessingModal}
63
- renderInput={({ imageUri, onSelect, isDisabled, isProcessing }) => (
67
+ renderInput={({ imageUri, onSelect, isDisabled, isProcessing }: SingleImageInputRenderProps) => (
64
68
  <PhotoUploadCard
65
69
  imageUri={imageUri}
66
70
  onPress={onSelect}
@@ -80,7 +84,7 @@ export const PhotoRestoreFeature: React.FC<PhotoRestoreFeatureProps> = ({
80
84
  }}
81
85
  />
82
86
  )}
83
- renderCustomResult={({ processedUrl, originalImageUri, onSave, onReset }) => (
87
+ renderCustomResult={({ processedUrl, originalImageUri, onSave, onReset }: CustomResultRenderProps) => (
84
88
  <PhotoRestoreResultView
85
89
  originalUri={originalImageUri}
86
90
  processedUri={processedUrl}
@@ -8,7 +8,7 @@ import React, { useMemo } from "react";
8
8
  import { Image, StyleSheet } from "react-native";
9
9
  import { PhotoUploadCard } from "../../../../presentation/components/PhotoUploadCard";
10
10
  import { SingleImageFeatureLayout } from "../../../../presentation/layouts";
11
- import type { ProcessingModalRenderProps } from "../../../../presentation/layouts";
11
+ import type { ProcessingModalRenderProps, SingleImageInputRenderProps, ResultRenderProps } from "../../../../presentation/layouts";
12
12
  import { useRemoveBackgroundFeature } from "../hooks";
13
13
  import type {
14
14
  RemoveBackgroundTranslations,
@@ -60,7 +60,7 @@ export const RemoveBackgroundFeature: React.FC<RemoveBackgroundFeatureProps> = (
60
60
  translations={translations}
61
61
  modalTranslations={modalTranslations}
62
62
  renderProcessingModal={renderProcessingModal}
63
- renderInput={({ imageUri, onSelect, isDisabled, isProcessing }) => (
63
+ renderInput={({ imageUri, onSelect, isDisabled, isProcessing }: SingleImageInputRenderProps) => (
64
64
  <PhotoUploadCard
65
65
  imageUri={imageUri}
66
66
  onPress={onSelect}
@@ -80,7 +80,7 @@ export const RemoveBackgroundFeature: React.FC<RemoveBackgroundFeatureProps> = (
80
80
  }}
81
81
  />
82
82
  )}
83
- renderResult={({ imageUrl, imageSize }) => (
83
+ renderResult={({ imageUrl, imageSize }: ResultRenderProps) => (
84
84
  <Image
85
85
  source={{ uri: imageUrl }}
86
86
  style={[styles.resultImage, { width: imageSize, height: imageSize }]}
@@ -12,7 +12,7 @@ import {
12
12
  } from "@umituz/react-native-design-system";
13
13
  import { PhotoUploadCard } from "../../../../presentation/components/PhotoUploadCard";
14
14
  import { SingleImageWithPromptFeatureLayout } from "../../../../presentation/layouts";
15
- import type { ProcessingModalRenderProps } from "../../../../presentation/layouts";
15
+ import type { ProcessingModalRenderProps, SingleImageWithPromptInputRenderProps, ResultRenderProps } from "../../../../presentation/layouts";
16
16
  import { useRemoveObjectFeature } from "../hooks";
17
17
  import type {
18
18
  RemoveObjectTranslations,
@@ -66,7 +66,7 @@ export const RemoveObjectFeature: React.FC<RemoveObjectFeatureProps> = ({
66
66
  translations={translations}
67
67
  modalTranslations={modalTranslations}
68
68
  renderProcessingModal={renderProcessingModal}
69
- renderInput={({ imageUri, onSelect, isDisabled, isProcessing, prompt, onPromptChange }) => (
69
+ renderInput={({ imageUri, onSelect, isDisabled, isProcessing, prompt, onPromptChange }: SingleImageWithPromptInputRenderProps) => (
70
70
  <>
71
71
  <PhotoUploadCard
72
72
  imageUri={imageUri}
@@ -120,7 +120,7 @@ export const RemoveObjectFeature: React.FC<RemoveObjectFeatureProps> = ({
120
120
  </View>
121
121
  </>
122
122
  )}
123
- renderResult={({ imageUrl, imageSize }) => (
123
+ renderResult={({ imageUrl, imageSize }: ResultRenderProps) => (
124
124
  <Image
125
125
  source={{ uri: imageUrl }}
126
126
  style={[styles.resultImage, { width: imageSize, height: imageSize }]}
@@ -9,7 +9,11 @@ import { View, Image, StyleSheet } from "react-native";
9
9
  import { AtomicInput } from "@umituz/react-native-design-system";
10
10
  import { PhotoUploadCard } from "../../../../presentation/components/PhotoUploadCard";
11
11
  import { SingleImageWithPromptFeatureLayout } from "../../../../presentation/layouts";
12
- import type { ProcessingModalRenderProps } from "../../../../presentation/layouts";
12
+ import type {
13
+ ProcessingModalRenderProps,
14
+ SingleImageWithPromptInputRenderProps,
15
+ ResultRenderProps,
16
+ } from "../../../../presentation/layouts";
13
17
  import { useReplaceBackgroundFeature } from "../hooks";
14
18
  import type {
15
19
  ReplaceBackgroundTranslations,
@@ -61,7 +65,7 @@ export const ReplaceBackgroundFeature: React.FC<ReplaceBackgroundFeatureProps> =
61
65
  translations={translations}
62
66
  modalTranslations={modalTranslations}
63
67
  renderProcessingModal={renderProcessingModal}
64
- renderInput={({ imageUri, onSelect, isDisabled, isProcessing, prompt, onPromptChange }) => (
68
+ renderInput={({ imageUri, onSelect, isDisabled, isProcessing, prompt, onPromptChange }: SingleImageWithPromptInputRenderProps) => (
65
69
  <>
66
70
  <PhotoUploadCard
67
71
  imageUri={imageUri}
@@ -94,7 +98,7 @@ export const ReplaceBackgroundFeature: React.FC<ReplaceBackgroundFeatureProps> =
94
98
  </View>
95
99
  </>
96
100
  )}
97
- renderResult={({ imageUrl, imageSize }) => (
101
+ renderResult={({ imageUrl, imageSize }: ResultRenderProps) => (
98
102
  <Image
99
103
  source={{ uri: imageUrl }}
100
104
  style={[styles.resultImage, { width: imageSize, height: imageSize }]}
@@ -7,7 +7,11 @@
7
7
  import React, { useMemo } from "react";
8
8
  import { PhotoUploadCard } from "../../../../presentation/components/PhotoUploadCard";
9
9
  import { SingleImageFeatureLayout } from "../../../../presentation/layouts";
10
- import type { ProcessingModalRenderProps } from "../../../../presentation/layouts";
10
+ import type {
11
+ ProcessingModalRenderProps,
12
+ SingleImageInputRenderProps,
13
+ CustomResultRenderProps,
14
+ } from "../../../../presentation/layouts";
11
15
  import { UpscaleResultView } from "./UpscaleResultView";
12
16
  import { useUpscaleFeature } from "../hooks";
13
17
  import type {
@@ -61,7 +65,7 @@ export const UpscaleFeature: React.FC<UpscaleFeatureProps> = ({
61
65
  modalTranslations={modalTranslations}
62
66
  description={translations.description}
63
67
  renderProcessingModal={renderProcessingModal}
64
- renderInput={({ imageUri, onSelect, isDisabled, isProcessing }) => (
68
+ renderInput={({ imageUri, onSelect, isDisabled, isProcessing }: SingleImageInputRenderProps) => (
65
69
  <PhotoUploadCard
66
70
  imageUri={imageUri}
67
71
  onPress={onSelect}
@@ -81,7 +85,7 @@ export const UpscaleFeature: React.FC<UpscaleFeatureProps> = ({
81
85
  }}
82
86
  />
83
87
  )}
84
- renderCustomResult={({ processedUrl, originalImageUri, onSave, onReset }) => (
88
+ renderCustomResult={({ processedUrl, originalImageUri, onSave, onReset }: CustomResultRenderProps) => (
85
89
  <UpscaleResultView
86
90
  originalUri={originalImageUri}
87
91
  processedUri={processedUrl}
@@ -4,47 +4,17 @@
4
4
  */
5
5
 
6
6
  import { AIErrorType, type AIErrorInfo } from "../../domain/entities";
7
+ import {
8
+ NETWORK_ERROR_PATTERNS,
9
+ RATE_LIMIT_PATTERNS,
10
+ AUTH_ERROR_PATTERNS,
11
+ CONTENT_POLICY_PATTERNS,
12
+ SERVER_ERROR_PATTERNS,
13
+ } from "./error-patterns.constants";
7
14
 
8
15
  declare const __DEV__: boolean;
9
16
 
10
- const NETWORK_ERROR_PATTERNS = [
11
- "network",
12
- "timeout",
13
- "socket",
14
- "econnrefused",
15
- "enotfound",
16
- "fetch failed",
17
- "connection",
18
- ];
19
-
20
- const RATE_LIMIT_PATTERNS = ["rate limit", "too many requests", "429"];
21
-
22
- const AUTH_ERROR_PATTERNS = [
23
- "unauthorized",
24
- "authentication",
25
- "invalid api key",
26
- "401",
27
- "403",
28
- ];
29
-
30
- const CONTENT_POLICY_PATTERNS = [
31
- "content policy",
32
- "safety",
33
- "moderation",
34
- "inappropriate",
35
- "blocked",
36
- ];
37
-
38
- const SERVER_ERROR_PATTERNS = [
39
- "internal server error",
40
- "500",
41
- "502",
42
- "503",
43
- "504",
44
- "service unavailable",
45
- ];
46
-
47
- function matchesPatterns(message: string, patterns: string[]): boolean {
17
+ function matchesPatterns(message: string, patterns: readonly string[]): boolean {
48
18
  const lowerMessage = message.toLowerCase();
49
19
  return patterns.some((pattern) => lowerMessage.includes(pattern));
50
20
  }
@@ -64,7 +34,6 @@ function getStatusCode(error: unknown): number | undefined {
64
34
 
65
35
  function logClassification(info: AIErrorInfo): AIErrorInfo {
66
36
  if (typeof __DEV__ !== "undefined" && __DEV__) {
67
-
68
37
  console.log("[ErrorClassifier] Classified as:", {
69
38
  type: info.type,
70
39
  messageKey: info.messageKey,
@@ -79,7 +48,6 @@ export function classifyError(error: unknown): AIErrorInfo {
79
48
  const statusCode = getStatusCode(error);
80
49
 
81
50
  if (typeof __DEV__ !== "undefined" && __DEV__) {
82
-
83
51
  console.log("[ErrorClassifier] Classifying error:", {
84
52
  message: message.slice(0, 100),
85
53
  statusCode,
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Error Pattern Constants
3
+ * Error classification patterns for error classifier utility
4
+ */
5
+
6
+ export const NETWORK_ERROR_PATTERNS = [
7
+ "network",
8
+ "timeout",
9
+ "socket",
10
+ "econnrefused",
11
+ "enotfound",
12
+ "fetch failed",
13
+ "connection",
14
+ ] as const;
15
+
16
+ export const RATE_LIMIT_PATTERNS = ["rate limit", "too many requests", "429"] as const;
17
+
18
+ export const AUTH_ERROR_PATTERNS = [
19
+ "unauthorized",
20
+ "authentication",
21
+ "invalid api key",
22
+ "401",
23
+ "403",
24
+ ] as const;
25
+
26
+ export const CONTENT_POLICY_PATTERNS = [
27
+ "content policy",
28
+ "safety",
29
+ "moderation",
30
+ "inappropriate",
31
+ "blocked",
32
+ ] as const;
33
+
34
+ export const SERVER_ERROR_PATTERNS = [
35
+ "internal server error",
36
+ "500",
37
+ "502",
38
+ "503",
39
+ "504",
40
+ "service unavailable",
41
+ ] as const;
@@ -13,6 +13,7 @@ import type {
13
13
  OnSelectImageCallback,
14
14
  OnSaveCallback,
15
15
  } from "./types";
16
+ import { createFeatureStateHandlers, executeProcess, executeSave } from "./utils/feature-state.factory";
16
17
 
17
18
  /**
18
19
  * Request passed to processRequest callback
@@ -30,7 +31,7 @@ export interface UseDualImageFeatureConfig {
30
31
  readonly onSelectFirstImage: OnSelectImageCallback;
31
32
  readonly onSelectSecondImage: OnSelectImageCallback;
32
33
  readonly processRequest: (
33
- request: DualImageProcessRequest
34
+ request: DualImageProcessRequest,
34
35
  ) => Promise<FeatureProcessResult>;
35
36
  readonly onSave?: OnSaveCallback;
36
37
  readonly onError?: (error: string) => void;
@@ -57,27 +58,39 @@ export interface UseDualImageFeatureReturn
57
58
  readonly save: () => Promise<void>;
58
59
  }
59
60
 
61
+ const initialState: DualImageFeatureState = {
62
+ firstImageUri: null,
63
+ secondImageUri: null,
64
+ processedUrl: null,
65
+ isProcessing: false,
66
+ progress: 0,
67
+ error: null,
68
+ };
69
+
60
70
  export function useDualImageFeature(
61
- config: UseDualImageFeatureConfig
71
+ config: UseDualImageFeatureConfig,
62
72
  ): UseDualImageFeatureReturn {
63
- const [firstImageUri, setFirstImageUri] = useState<string | null>(null);
64
- const [secondImageUri, setSecondImageUri] = useState<string | null>(null);
65
- const [processedUrl, setProcessedUrl] = useState<string | null>(null);
66
- const [isProcessing, setIsProcessing] = useState(false);
67
- const [progress, setProgress] = useState(0);
68
- const [error, setError] = useState<string | null>(null);
73
+ const [state, setState] = useState<DualImageFeatureState>(initialState);
74
+
75
+ const { reset, clearError } = createFeatureStateHandlers({
76
+ setState,
77
+ initialState,
78
+ });
69
79
 
70
80
  const selectFirstImage = useCallback(async (): Promise<void> => {
71
81
  try {
72
82
  const uri = await config.onSelectFirstImage();
73
83
  if (uri) {
74
- setFirstImageUri(uri);
75
- setError(null);
76
- setProcessedUrl(null);
84
+ setState((prev) => ({
85
+ ...prev,
86
+ firstImageUri: uri,
87
+ error: null,
88
+ processedUrl: null,
89
+ }));
77
90
  }
78
91
  } catch (err) {
79
92
  const message = err instanceof Error ? err.message : "error.selectImage";
80
- setError(message);
93
+ setState((prev) => ({ ...prev, error: message }));
81
94
  config.onError?.(message);
82
95
  }
83
96
  }, [config]);
@@ -86,88 +99,67 @@ export function useDualImageFeature(
86
99
  try {
87
100
  const uri = await config.onSelectSecondImage();
88
101
  if (uri) {
89
- setSecondImageUri(uri);
90
- setError(null);
91
- setProcessedUrl(null);
102
+ setState((prev) => ({
103
+ ...prev,
104
+ secondImageUri: uri,
105
+ error: null,
106
+ processedUrl: null,
107
+ }));
92
108
  }
93
109
  } catch (err) {
94
110
  const message = err instanceof Error ? err.message : "error.selectImage";
95
- setError(message);
111
+ setState((prev) => ({ ...prev, error: message }));
96
112
  config.onError?.(message);
97
113
  }
98
114
  }, [config]);
99
115
 
100
116
  const process = useCallback(async (): Promise<void> => {
101
- if (!firstImageUri || !secondImageUri) {
117
+ if (!state.firstImageUri || !state.secondImageUri) {
102
118
  const message = "error.noImages";
103
- setError(message);
119
+ setState((prev) => ({ ...prev, error: message }));
104
120
  config.onError?.(message);
105
121
  return;
106
122
  }
107
123
 
108
- setIsProcessing(true);
109
- setProgress(0);
110
- setError(null);
111
-
112
- try {
113
- const result = await config.processRequest({
114
- firstImageUri,
115
- secondImageUri,
116
- onProgress: setProgress,
117
- });
118
-
119
- if (result.success && result.outputUrl) {
120
- setProcessedUrl(result.outputUrl);
121
- config.onSuccess?.(result.outputUrl);
122
- } else {
123
- const message = result.error || "error.processing";
124
- setError(message);
125
- config.onError?.(message);
126
- }
127
- } catch (err) {
128
- const message = err instanceof Error ? err.message : "error.processing";
129
- setError(message);
130
- config.onError?.(message);
131
- } finally {
132
- setIsProcessing(false);
133
- setProgress(0);
124
+ const result = await executeProcess({
125
+ canProcess: () => !!state.firstImageUri && !!state.secondImageUri,
126
+ setError: (error) => setState((prev) => ({ ...prev, error })),
127
+ setProcessing: (isProcessing) => setState((prev) => ({ ...prev, isProcessing })),
128
+ onError: config.onError,
129
+ processFn: () =>
130
+ config.processRequest({
131
+ firstImageUri: state.firstImageUri!,
132
+ secondImageUri: state.secondImageUri!,
133
+ onProgress: (progress) => setState((prev) => ({ ...prev, progress })),
134
+ }),
135
+ onSuccess: (result) => {
136
+ if (result.outputUrl) {
137
+ setState((prev) => ({ ...prev, processedUrl: result.outputUrl ?? null }));
138
+ config.onSuccess?.(result.outputUrl);
139
+ } else {
140
+ const message = result.error || "error.processing";
141
+ setState((prev) => ({ ...prev, error: message }));
142
+ config.onError?.(message);
143
+ }
144
+ },
145
+ });
146
+
147
+ if (!result) {
148
+ setState((prev) => ({ ...prev, progress: 0 }));
134
149
  }
135
- }, [firstImageUri, secondImageUri, config]);
150
+ }, [state.firstImageUri, state.secondImageUri, config]);
136
151
 
137
152
  const save = useCallback(async (): Promise<void> => {
138
- if (!processedUrl || !config.onSave) {
139
- return;
140
- }
141
-
142
- try {
143
- await config.onSave(processedUrl);
144
- } catch (err) {
145
- const message = err instanceof Error ? err.message : "error.save";
146
- setError(message);
147
- config.onError?.(message);
148
- }
149
- }, [processedUrl, config]);
150
-
151
- const reset = useCallback((): void => {
152
- setFirstImageUri(null);
153
- setSecondImageUri(null);
154
- setProcessedUrl(null);
155
- setIsProcessing(false);
156
- setProgress(0);
157
- setError(null);
158
- }, []);
159
-
160
- const clearError = useCallback((): void => {
161
- setError(null);
162
- }, []);
153
+ await executeSave({
154
+ processedUrl: state.processedUrl,
155
+ onSave: config.onSave,
156
+ setError: (error) => setState((prev) => ({ ...prev, error })),
157
+ onError: config.onError,
158
+ });
159
+ }, [state.processedUrl, config]);
163
160
 
164
161
  return {
165
- firstImageUri,
166
- secondImageUri,
167
- processedUrl,
168
- isProcessing,
169
- progress,
170
- error,
162
+ ...state,
171
163
  selectFirstImage,
172
164
  selectSecondImage,
173
165
  process,
@@ -13,6 +13,7 @@ import type {
13
13
  OnSelectImageCallback,
14
14
  OnSaveCallback,
15
15
  } from "./types";
16
+ import { createFeatureStateHandlers, executeProcess, executeSave } from "./utils/feature-state.factory";
16
17
 
17
18
  /**
18
19
  * Request passed to processRequest callback
@@ -57,109 +58,105 @@ export interface UseImageWithPromptFeatureReturn
57
58
  readonly save: () => Promise<void>;
58
59
  }
59
60
 
61
+ const initialState: ImageWithPromptFeatureState = {
62
+ imageUri: null,
63
+ prompt: "",
64
+ processedUrl: null,
65
+ isProcessing: false,
66
+ progress: 0,
67
+ error: null,
68
+ };
69
+
60
70
  export function useImageWithPromptFeature(
61
- config: UseImageWithPromptFeatureConfig
71
+ config: UseImageWithPromptFeatureConfig,
62
72
  ): UseImageWithPromptFeatureReturn {
63
- const [imageUri, setImageUri] = useState<string | null>(null);
64
- const [prompt, setPrompt] = useState<string>("");
65
- const [processedUrl, setProcessedUrl] = useState<string | null>(null);
66
- const [isProcessing, setIsProcessing] = useState(false);
67
- const [progress, setProgress] = useState(0);
68
- const [error, setError] = useState<string | null>(null);
73
+ const [state, setState] = useState<ImageWithPromptFeatureState>(initialState);
74
+
75
+ const { reset, clearError } = createFeatureStateHandlers({
76
+ setState,
77
+ initialState,
78
+ });
69
79
 
70
80
  const selectImage = useCallback(async (): Promise<void> => {
71
81
  try {
72
82
  const uri = await config.onSelectImage();
73
83
  if (uri) {
74
- setImageUri(uri);
75
- setError(null);
76
- setProcessedUrl(null);
84
+ setState((prev) => ({
85
+ ...prev,
86
+ imageUri: uri,
87
+ error: null,
88
+ processedUrl: null,
89
+ }));
77
90
  }
78
91
  } catch (err) {
79
92
  const message = err instanceof Error ? err.message : "error.selectImage";
80
- setError(message);
93
+ setState((prev) => ({ ...prev, error: message }));
81
94
  config.onError?.(message);
82
95
  }
83
96
  }, [config]);
84
97
 
98
+ const setPrompt = useCallback((prompt: string) => {
99
+ setState((prev) => ({ ...prev, prompt }));
100
+ }, []);
101
+
85
102
  const process = useCallback(async (): Promise<void> => {
86
- if (!imageUri) {
103
+ if (!state.imageUri) {
87
104
  const message = "error.noImage";
88
- setError(message);
105
+ setState((prev) => ({ ...prev, error: message }));
89
106
  config.onError?.(message);
90
107
  return;
91
108
  }
92
109
 
93
- if (config.requirePrompt && !prompt.trim()) {
110
+ if (config.requirePrompt && !state.prompt.trim()) {
94
111
  const message = "error.noPrompt";
95
- setError(message);
112
+ setState((prev) => ({ ...prev, error: message }));
96
113
  config.onError?.(message);
97
114
  return;
98
115
  }
99
116
 
100
- setIsProcessing(true);
101
- setProgress(0);
102
- setError(null);
103
-
104
- try {
105
- const result = await config.processRequest({
106
- imageUri,
107
- prompt: prompt.trim(),
108
- onProgress: setProgress,
109
- });
110
-
111
- if (result.success && result.outputUrl) {
112
- setProcessedUrl(result.outputUrl);
113
- config.onSuccess?.(result.outputUrl);
114
- } else {
115
- const message = result.error || "error.processing";
116
- setError(message);
117
- config.onError?.(message);
118
- }
119
- } catch (err) {
120
- const message = err instanceof Error ? err.message : "error.processing";
121
- setError(message);
122
- config.onError?.(message);
123
- } finally {
124
- setIsProcessing(false);
125
- setProgress(0);
117
+ const result = await executeProcess({
118
+ canProcess: () => {
119
+ if (!state.imageUri) return false;
120
+ if (config.requirePrompt) return !!state.prompt.trim();
121
+ return true;
122
+ },
123
+ setError: (error) => setState((prev) => ({ ...prev, error })),
124
+ setProcessing: (isProcessing) => setState((prev) => ({ ...prev, isProcessing })),
125
+ onError: config.onError,
126
+ processFn: () =>
127
+ config.processRequest({
128
+ imageUri: state.imageUri!,
129
+ prompt: state.prompt.trim(),
130
+ onProgress: (progress) => setState((prev) => ({ ...prev, progress })),
131
+ }),
132
+ onSuccess: (result) => {
133
+ if (result.outputUrl) {
134
+ setState((prev) => ({ ...prev, processedUrl: result.outputUrl ?? null }));
135
+ config.onSuccess?.(result.outputUrl);
136
+ } else {
137
+ const message = result.error || "error.processing";
138
+ setState((prev) => ({ ...prev, error: message }));
139
+ config.onError?.(message);
140
+ }
141
+ },
142
+ });
143
+
144
+ if (!result) {
145
+ setState((prev) => ({ ...prev, progress: 0 }));
126
146
  }
127
- }, [imageUri, prompt, config]);
147
+ }, [state.imageUri, state.prompt, config]);
128
148
 
129
149
  const save = useCallback(async (): Promise<void> => {
130
- if (!processedUrl || !config.onSave) {
131
- return;
132
- }
133
-
134
- try {
135
- await config.onSave(processedUrl);
136
- } catch (err) {
137
- const message = err instanceof Error ? err.message : "error.save";
138
- setError(message);
139
- config.onError?.(message);
140
- }
141
- }, [processedUrl, config]);
142
-
143
- const reset = useCallback((): void => {
144
- setImageUri(null);
145
- setPrompt("");
146
- setProcessedUrl(null);
147
- setIsProcessing(false);
148
- setProgress(0);
149
- setError(null);
150
- }, []);
151
-
152
- const clearError = useCallback((): void => {
153
- setError(null);
154
- }, []);
150
+ await executeSave({
151
+ processedUrl: state.processedUrl,
152
+ onSave: config.onSave,
153
+ setError: (error) => setState((prev) => ({ ...prev, error })),
154
+ onError: config.onError,
155
+ });
156
+ }, [state.processedUrl, config]);
155
157
 
156
158
  return {
157
- imageUri,
158
- prompt,
159
- processedUrl,
160
- isProcessing,
161
- progress,
162
- error,
159
+ ...state,
163
160
  selectImage,
164
161
  setPrompt,
165
162
  process,