@umituz/react-native-ai-generation-content 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.7.0",
3
+ "version": "1.8.1",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -31,17 +31,20 @@
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@tanstack/react-query": ">=5.0.0",
34
+ "@umituz/react-native-design-system-theme": "latest",
34
35
  "react": ">=18.0.0",
35
36
  "react-native": ">=0.74.0"
36
37
  },
37
38
  "devDependencies": {
38
39
  "@tanstack/react-query": "^5.0.0",
39
- "@types/react": "^19.0.0",
40
+ "@umituz/react-native-design-system-theme": "latest",
41
+ "@types/react": "~19.1.10",
40
42
  "@types/react-native": "^0.73.0",
41
43
  "@typescript-eslint/eslint-plugin": "^7.0.0",
42
44
  "@typescript-eslint/parser": "^7.0.0",
43
45
  "eslint": "^8.57.0",
44
- "react-native": "^0.76.0",
46
+ "react": "19.1.0",
47
+ "react-native": "0.81.5",
45
48
  "typescript": "^5.3.0"
46
49
  },
47
50
  "publishConfig": {
@@ -42,7 +42,10 @@ export interface JobExecutorConfig<TInput = unknown, TResult = unknown> {
42
42
  readonly timeoutMs?: number;
43
43
  }
44
44
 
45
+ export type GenerationMode = "direct" | "queued";
46
+
45
47
  export interface BackgroundQueueConfig {
48
+ readonly mode?: GenerationMode;
46
49
  readonly maxConcurrent?: number;
47
50
  readonly retryCount?: number;
48
51
  readonly retryDelayMs?: number;
@@ -50,6 +53,7 @@ export interface BackgroundQueueConfig {
50
53
  }
51
54
 
52
55
  export const DEFAULT_QUEUE_CONFIG: Required<BackgroundQueueConfig> = {
56
+ mode: "queued",
53
57
  maxConcurrent: 1,
54
58
  retryCount: 2,
55
59
  retryDelayMs: 2000,
package/src/index.ts CHANGED
@@ -55,6 +55,7 @@ export type {
55
55
  UpdateJobInput,
56
56
  JobExecutorConfig,
57
57
  BackgroundQueueConfig,
58
+ GenerationMode,
58
59
  } from "./domain/entities";
59
60
 
60
61
  export { DEFAULT_POLLING_CONFIG, DEFAULT_PROGRESS_STAGES, DEFAULT_QUEUE_CONFIG } from "./domain/entities";
@@ -146,6 +147,7 @@ export type {
146
147
  UsePendingJobsReturn,
147
148
  UseBackgroundGenerationOptions,
148
149
  UseBackgroundGenerationReturn,
150
+ DirectExecutionResult,
149
151
  } from "./presentation/hooks";
150
152
 
151
153
  // =============================================================================
@@ -154,14 +156,20 @@ export type {
154
156
 
155
157
  export {
156
158
  GenerationProgressModal,
159
+ GenerationProgressContent,
160
+ GenerationProgressBar,
157
161
  PendingJobCard,
162
+ PendingJobProgressBar,
163
+ PendingJobCardActions,
158
164
  } from "./presentation/components";
159
165
 
160
166
  export type {
161
167
  GenerationProgressModalProps,
162
168
  GenerationProgressRenderProps,
169
+ GenerationProgressContentProps,
170
+ GenerationProgressBarProps,
163
171
  PendingJobCardProps,
164
- PendingJobCardStyles,
165
- PendingJobCardColors,
166
172
  StatusLabels,
173
+ PendingJobProgressBarProps,
174
+ PendingJobCardActionsProps,
167
175
  } from "./presentation/components";
@@ -0,0 +1,77 @@
1
+ /**
2
+ * GenerationProgressBar
3
+ * Individual progress bar component for AI generation
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet, Text } from "react-native";
8
+ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
9
+
10
+ export interface GenerationProgressBarProps {
11
+ progress: number;
12
+ textColor?: string;
13
+ progressColor?: string;
14
+ backgroundColor?: string;
15
+ }
16
+
17
+ export const GenerationProgressBar: React.FC<GenerationProgressBarProps> = ({
18
+ progress,
19
+ textColor,
20
+ progressColor,
21
+ backgroundColor,
22
+ }) => {
23
+ const tokens = useAppDesignTokens();
24
+ const clampedProgress = Math.max(0, Math.min(100, progress));
25
+
26
+ return (
27
+ <View style={styles.container}>
28
+ <View
29
+ style={[
30
+ styles.background,
31
+ { backgroundColor: backgroundColor || tokens.colors.borderLight },
32
+ ]}
33
+ >
34
+ <View
35
+ style={[
36
+ styles.fill,
37
+ {
38
+ backgroundColor: progressColor || tokens.colors.primary,
39
+ width: `${clampedProgress}%`,
40
+ },
41
+ ]}
42
+ />
43
+ </View>
44
+ <Text
45
+ style={[
46
+ styles.text,
47
+ { color: textColor || tokens.colors.textPrimary },
48
+ ]}
49
+ >
50
+ {Math.round(clampedProgress)}%
51
+ </Text>
52
+ </View>
53
+ );
54
+ };
55
+
56
+ const styles = StyleSheet.create({
57
+ container: {
58
+ width: "100%",
59
+ marginBottom: 16,
60
+ alignItems: "center",
61
+ },
62
+ background: {
63
+ width: "100%",
64
+ height: 8,
65
+ borderRadius: 4,
66
+ overflow: "hidden",
67
+ },
68
+ fill: {
69
+ height: "100%",
70
+ borderRadius: 4,
71
+ },
72
+ text: {
73
+ fontSize: 14,
74
+ fontWeight: "600",
75
+ marginTop: 8,
76
+ },
77
+ });
@@ -0,0 +1,123 @@
1
+ /**
2
+ * GenerationProgressContent
3
+ * Content for the AI generation progress modal
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
8
+ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
9
+ import { GenerationProgressBar } from "./GenerationProgressBar";
10
+
11
+ export interface GenerationProgressContentProps {
12
+ progress: number;
13
+ title?: string;
14
+ message?: string;
15
+ hint?: string;
16
+ dismissLabel?: string;
17
+ onDismiss?: () => void;
18
+ backgroundColor?: string;
19
+ textColor?: string;
20
+ progressColor?: string;
21
+ progressBackgroundColor?: string;
22
+ dismissButtonColor?: string;
23
+ }
24
+
25
+ export const GenerationProgressContent: React.FC<
26
+ GenerationProgressContentProps
27
+ > = ({
28
+ progress,
29
+ title,
30
+ message,
31
+ hint,
32
+ dismissLabel = "Got it",
33
+ onDismiss,
34
+ backgroundColor,
35
+ textColor,
36
+ progressColor,
37
+ progressBackgroundColor,
38
+ dismissButtonColor,
39
+ }) => {
40
+ const tokens = useAppDesignTokens();
41
+
42
+ const activeTextColor = textColor || tokens.colors.textPrimary;
43
+ const activeBgColor = backgroundColor || tokens.colors.surface;
44
+
45
+ return (
46
+ <View style={[styles.modal, { backgroundColor: activeBgColor }]}>
47
+ {title && (
48
+ <Text style={[styles.title, { color: activeTextColor }]}>{title}</Text>
49
+ )}
50
+
51
+ {message && (
52
+ <Text style={[styles.message, { color: activeTextColor }]}>
53
+ {message}
54
+ </Text>
55
+ )}
56
+
57
+ <GenerationProgressBar
58
+ progress={progress}
59
+ textColor={activeTextColor}
60
+ progressColor={progressColor}
61
+ backgroundColor={progressBackgroundColor}
62
+ />
63
+
64
+ {hint && (
65
+ <Text style={[styles.hint, { color: activeTextColor }]}>{hint}</Text>
66
+ )}
67
+
68
+ {onDismiss && (
69
+ <TouchableOpacity
70
+ style={[
71
+ styles.dismissButton,
72
+ { backgroundColor: dismissButtonColor || tokens.colors.primary },
73
+ ]}
74
+ onPress={onDismiss}
75
+ >
76
+ <Text style={styles.dismissText}>{dismissLabel}</Text>
77
+ </TouchableOpacity>
78
+ )}
79
+ </View>
80
+ );
81
+ };
82
+
83
+ const styles = StyleSheet.create({
84
+ modal: {
85
+ width: "100%",
86
+ maxWidth: 400,
87
+ borderRadius: 24,
88
+ padding: 32,
89
+ alignItems: "center",
90
+ },
91
+ title: {
92
+ fontSize: 20,
93
+ fontWeight: "700",
94
+ marginBottom: 12,
95
+ textAlign: "center",
96
+ },
97
+ message: {
98
+ fontSize: 16,
99
+ marginBottom: 24,
100
+ textAlign: "center",
101
+ opacity: 0.8,
102
+ },
103
+ hint: {
104
+ fontSize: 14,
105
+ textAlign: "center",
106
+ fontStyle: "italic",
107
+ opacity: 0.6,
108
+ marginBottom: 16,
109
+ },
110
+ dismissButton: {
111
+ marginTop: 8,
112
+ paddingVertical: 14,
113
+ paddingHorizontal: 32,
114
+ borderRadius: 12,
115
+ minWidth: 140,
116
+ alignItems: "center",
117
+ },
118
+ dismissText: {
119
+ color: "#FFFFFF",
120
+ fontSize: 16,
121
+ fontWeight: "600",
122
+ },
123
+ });
@@ -4,39 +4,29 @@
4
4
  */
5
5
 
6
6
  import React from "react";
7
+ import { Modal, View, StyleSheet } from "react-native";
8
+ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
7
9
  import {
8
- Modal,
9
- View,
10
- Text,
11
- TouchableOpacity,
12
- StyleSheet,
13
- } from "react-native";
10
+ GenerationProgressContent,
11
+ GenerationProgressContentProps,
12
+ } from "./GenerationProgressContent";
14
13
 
15
- export interface GenerationProgressModalProps {
16
- visible: boolean;
14
+ export interface GenerationProgressRenderProps {
17
15
  progress: number;
18
16
  title?: string;
19
17
  message?: string;
20
18
  hint?: string;
21
- dismissLabel?: string;
22
19
  onDismiss?: () => void;
20
+ }
21
+
22
+ export interface GenerationProgressModalProps
23
+ extends Omit<GenerationProgressContentProps, "backgroundColor"> {
24
+ visible: boolean;
23
25
  overlayColor?: string;
24
26
  modalBackgroundColor?: string;
25
- textColor?: string;
26
- progressColor?: string;
27
- progressBackgroundColor?: string;
28
- dismissButtonColor?: string;
29
27
  renderContent?: (props: GenerationProgressRenderProps) => React.ReactNode;
30
28
  }
31
29
 
32
- export interface GenerationProgressRenderProps {
33
- progress: number;
34
- title?: string;
35
- message?: string;
36
- hint?: string;
37
- onDismiss?: () => void;
38
- }
39
-
40
30
  export const GenerationProgressModal: React.FC<
41
31
  GenerationProgressModalProps
42
32
  > = ({
@@ -45,19 +35,45 @@ export const GenerationProgressModal: React.FC<
45
35
  title,
46
36
  message,
47
37
  hint,
48
- dismissLabel = "Got it",
38
+ dismissLabel,
49
39
  onDismiss,
50
40
  overlayColor = "rgba(0, 0, 0, 0.7)",
51
- modalBackgroundColor = "#1C1C1E",
52
- textColor = "#FFFFFF",
53
- progressColor = "#007AFF",
54
- progressBackgroundColor = "#3A3A3C",
55
- dismissButtonColor = "#007AFF",
41
+ modalBackgroundColor,
42
+ textColor,
43
+ progressColor,
44
+ progressBackgroundColor,
45
+ dismissButtonColor,
56
46
  renderContent,
57
47
  }) => {
58
- const clampedProgress = Math.max(0, Math.min(100, progress));
48
+ const tokens = useAppDesignTokens();
49
+ const clampedProgress = Math.max(0, Math.min(100, progress));
50
+
51
+ const content = renderContent ? (
52
+ renderContent({
53
+ progress: clampedProgress,
54
+ title,
55
+ message,
56
+ hint,
57
+ onDismiss,
58
+ })
59
+ ) : (
60
+ <GenerationProgressContent
61
+ progress={clampedProgress}
62
+ title={title}
63
+ message={message}
64
+ hint={hint}
65
+ dismissLabel={dismissLabel}
66
+ onDismiss={onDismiss}
67
+ backgroundColor={modalBackgroundColor || tokens.colors.surface}
68
+ textColor={textColor || tokens.colors.textPrimary}
69
+ progressColor={progressColor || tokens.colors.primary}
70
+ progressBackgroundColor={
71
+ progressBackgroundColor || tokens.colors.borderLight
72
+ }
73
+ dismissButtonColor={dismissButtonColor || tokens.colors.primary}
74
+ />
75
+ );
59
76
 
60
- if (renderContent) {
61
77
  return (
62
78
  <Modal
63
79
  visible={visible}
@@ -66,81 +82,11 @@ export const GenerationProgressModal: React.FC<
66
82
  statusBarTranslucent
67
83
  >
68
84
  <View style={[styles.overlay, { backgroundColor: overlayColor }]}>
69
- {renderContent({
70
- progress: clampedProgress,
71
- title,
72
- message,
73
- hint,
74
- onDismiss,
75
- })}
85
+ {content}
76
86
  </View>
77
87
  </Modal>
78
88
  );
79
- }
80
-
81
- return (
82
- <Modal
83
- visible={visible}
84
- transparent
85
- animationType="fade"
86
- statusBarTranslucent
87
- >
88
- <View style={[styles.overlay, { backgroundColor: overlayColor }]}>
89
- <View
90
- style={[styles.modal, { backgroundColor: modalBackgroundColor }]}
91
- >
92
- {title && (
93
- <Text style={[styles.title, { color: textColor }]}>{title}</Text>
94
- )}
95
-
96
- {message && (
97
- <Text style={[styles.message, { color: textColor }]}>
98
- {message}
99
- </Text>
100
- )}
101
-
102
- <View style={styles.progressContainer}>
103
- <View
104
- style={[
105
- styles.progressBackground,
106
- { backgroundColor: progressBackgroundColor },
107
- ]}
108
- >
109
- <View
110
- style={[
111
- styles.progressFill,
112
- {
113
- backgroundColor: progressColor,
114
- width: `${clampedProgress}%`,
115
- },
116
- ]}
117
- />
118
- </View>
119
- <Text style={[styles.progressText, { color: textColor }]}>
120
- {Math.round(clampedProgress)}%
121
- </Text>
122
- </View>
123
-
124
- {hint && (
125
- <Text style={[styles.hint, { color: textColor }]}>{hint}</Text>
126
- )}
127
-
128
- {onDismiss && (
129
- <TouchableOpacity
130
- style={[
131
- styles.dismissButton,
132
- { backgroundColor: dismissButtonColor },
133
- ]}
134
- onPress={onDismiss}
135
- >
136
- <Text style={styles.dismissText}>{dismissLabel}</Text>
137
- </TouchableOpacity>
138
- )}
139
- </View>
140
- </View>
141
- </Modal>
142
- );
143
- };
89
+ };
144
90
 
145
91
  const styles = StyleSheet.create({
146
92
  overlay: {
@@ -149,63 +95,4 @@ const styles = StyleSheet.create({
149
95
  alignItems: "center",
150
96
  padding: 20,
151
97
  },
152
- modal: {
153
- width: "100%",
154
- maxWidth: 400,
155
- borderRadius: 24,
156
- padding: 32,
157
- alignItems: "center",
158
- },
159
- title: {
160
- fontSize: 20,
161
- fontWeight: "700",
162
- marginBottom: 12,
163
- textAlign: "center",
164
- },
165
- message: {
166
- fontSize: 16,
167
- marginBottom: 24,
168
- textAlign: "center",
169
- opacity: 0.8,
170
- },
171
- progressContainer: {
172
- width: "100%",
173
- marginBottom: 16,
174
- alignItems: "center",
175
- },
176
- progressBackground: {
177
- width: "100%",
178
- height: 8,
179
- borderRadius: 4,
180
- overflow: "hidden",
181
- },
182
- progressFill: {
183
- height: "100%",
184
- borderRadius: 4,
185
- },
186
- progressText: {
187
- fontSize: 14,
188
- fontWeight: "600",
189
- marginTop: 8,
190
- },
191
- hint: {
192
- fontSize: 14,
193
- textAlign: "center",
194
- fontStyle: "italic",
195
- opacity: 0.6,
196
- marginBottom: 16,
197
- },
198
- dismissButton: {
199
- marginTop: 8,
200
- paddingVertical: 14,
201
- paddingHorizontal: 32,
202
- borderRadius: 12,
203
- minWidth: 140,
204
- alignItems: "center",
205
- },
206
- dismissText: {
207
- color: "#FFFFFF",
208
- fontSize: 16,
209
- fontWeight: "600",
210
- },
211
98
  });
@@ -4,36 +4,11 @@
4
4
  */
5
5
 
6
6
  import React from "react";
7
- import {
8
- View,
9
- Text,
10
- TouchableOpacity,
11
- ActivityIndicator,
12
- StyleSheet,
13
- } from "react-native";
7
+ import { View, Text, ActivityIndicator, StyleSheet } from "react-native";
8
+ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
14
9
  import type { BackgroundJob } from "../../domain/entities/job.types";
15
-
16
- export interface PendingJobCardStyles {
17
- readonly card?: object;
18
- readonly content?: object;
19
- readonly header?: object;
20
- readonly typeText?: object;
21
- readonly statusText?: object;
22
- readonly progressBar?: object;
23
- readonly progressFill?: object;
24
- readonly actions?: object;
25
- readonly actionButton?: object;
26
- }
27
-
28
- export interface PendingJobCardColors {
29
- readonly background?: string;
30
- readonly text?: string;
31
- readonly textSecondary?: string;
32
- readonly error?: string;
33
- readonly progressBackground?: string;
34
- readonly progressFill?: string;
35
- readonly actionBackground?: string;
36
- }
10
+ import { PendingJobProgressBar } from "./PendingJobProgressBar";
11
+ import { PendingJobCardActions } from "./PendingJobCardActions";
37
12
 
38
13
  export interface StatusLabels {
39
14
  readonly queued?: string;
@@ -49,10 +24,12 @@ export interface PendingJobCardProps<TInput = unknown, TResult = unknown> {
49
24
  readonly onRetry?: (id: string) => void;
50
25
  readonly typeLabel?: string;
51
26
  readonly statusLabels?: StatusLabels;
52
- readonly colors?: PendingJobCardColors;
53
- readonly styles?: PendingJobCardStyles;
54
- readonly renderThumbnail?: (job: BackgroundJob<TInput, TResult>) => React.ReactNode;
55
- readonly renderActions?: (job: BackgroundJob<TInput, TResult>) => React.ReactNode;
27
+ readonly renderThumbnail?: (
28
+ job: BackgroundJob<TInput, TResult>,
29
+ ) => React.ReactNode;
30
+ readonly renderActions?: (
31
+ job: BackgroundJob<TInput, TResult>,
32
+ ) => React.ReactNode;
56
33
  }
57
34
 
58
35
  const DEFAULT_STATUS_LABELS: StatusLabels = {
@@ -63,28 +40,16 @@ const DEFAULT_STATUS_LABELS: StatusLabels = {
63
40
  failed: "Failed",
64
41
  };
65
42
 
66
- const DEFAULT_COLORS: Required<PendingJobCardColors> = {
67
- background: "#1C1C1E",
68
- text: "#FFFFFF",
69
- textSecondary: "#8E8E93",
70
- error: "#FF453A",
71
- progressBackground: "#3A3A3C",
72
- progressFill: "#007AFF",
73
- actionBackground: "#2C2C2E",
74
- };
75
-
76
43
  export function PendingJobCard<TInput = unknown, TResult = unknown>({
77
44
  job,
78
45
  onCancel,
79
46
  onRetry,
80
47
  typeLabel,
81
48
  statusLabels = DEFAULT_STATUS_LABELS,
82
- colors = {},
83
- styles: customStyles = {},
84
49
  renderThumbnail,
85
50
  renderActions,
86
51
  }: PendingJobCardProps<TInput, TResult>): React.ReactElement {
87
- const mergedColors = { ...DEFAULT_COLORS, ...colors };
52
+ const tokens = useAppDesignTokens();
88
53
  const isFailed = job.status === "failed";
89
54
 
90
55
  const statusText =
@@ -95,86 +60,44 @@ export function PendingJobCard<TInput = unknown, TResult = unknown>({
95
60
  const styles = StyleSheet.create({
96
61
  card: {
97
62
  flexDirection: "row",
98
- backgroundColor: mergedColors.background,
63
+ backgroundColor: tokens.colors.surface,
99
64
  borderRadius: 16,
100
65
  overflow: "hidden",
101
66
  opacity: isFailed ? 0.7 : 1,
102
- ...((customStyles.card as object) || {}),
67
+ },
68
+ thumbnailWrapper: {
69
+ width: 80,
70
+ height: 80,
71
+ justifyContent: "center",
72
+ alignItems: "center",
73
+ backgroundColor: tokens.colors.backgroundSecondary,
103
74
  },
104
75
  content: {
105
76
  flex: 1,
106
77
  padding: 12,
107
78
  justifyContent: "space-between",
108
- ...((customStyles.content as object) || {}),
109
- },
110
- header: {
111
- flexDirection: "row",
112
- alignItems: "center",
113
- gap: 8,
114
- ...((customStyles.header as object) || {}),
115
79
  },
116
80
  typeText: {
117
81
  fontSize: 14,
118
82
  fontWeight: "600",
119
- color: mergedColors.text,
120
- ...((customStyles.typeText as object) || {}),
83
+ color: tokens.colors.textPrimary,
121
84
  },
122
85
  statusText: {
123
86
  fontSize: 12,
124
- color: isFailed ? mergedColors.error : mergedColors.textSecondary,
87
+ color: isFailed ? tokens.colors.error : tokens.colors.textSecondary,
125
88
  marginTop: 4,
126
- ...((customStyles.statusText as object) || {}),
127
- },
128
- progressBar: {
129
- height: 4,
130
- backgroundColor: mergedColors.progressBackground,
131
- borderRadius: 2,
132
- marginTop: 8,
133
- overflow: "hidden",
134
- ...((customStyles.progressBar as object) || {}),
135
- },
136
- progressFill: {
137
- height: "100%",
138
- backgroundColor: mergedColors.progressFill,
139
- borderRadius: 2,
140
- width: `${job.progress}%`,
141
- ...((customStyles.progressFill as object) || {}),
142
- },
143
- actions: {
144
- flexDirection: "row",
145
- gap: 8,
146
- marginTop: 8,
147
- ...((customStyles.actions as object) || {}),
148
- },
149
- actionButton: {
150
- width: 32,
151
- height: 32,
152
- borderRadius: 16,
153
- backgroundColor: mergedColors.actionBackground,
154
- justifyContent: "center",
155
- alignItems: "center",
156
- ...((customStyles.actionButton as object) || {}),
157
- },
158
- thumbnail: {
159
- width: 80,
160
- height: 80,
161
- justifyContent: "center",
162
- alignItems: "center",
163
- backgroundColor: mergedColors.progressBackground,
164
- },
165
- loader: {
166
- position: "absolute",
167
89
  },
90
+ loader: { position: "absolute" },
168
91
  });
169
92
 
170
93
  return (
171
94
  <View style={styles.card}>
172
95
  {renderThumbnail && (
173
- <View style={styles.thumbnail}>
96
+ <View style={styles.thumbnailWrapper}>
174
97
  {renderThumbnail(job)}
175
98
  {!isFailed && (
176
99
  <ActivityIndicator
177
- color={mergedColors.text}
100
+ color={tokens.colors.primary}
178
101
  size="small"
179
102
  style={styles.loader}
180
103
  />
@@ -183,39 +106,21 @@ export function PendingJobCard<TInput = unknown, TResult = unknown>({
183
106
  )}
184
107
  <View style={styles.content}>
185
108
  <View>
186
- <View style={styles.header}>
187
- {typeLabel && <Text style={styles.typeText}>{typeLabel}</Text>}
188
- </View>
109
+ {typeLabel && <Text style={styles.typeText}>{typeLabel}</Text>}
189
110
  <Text style={styles.statusText} numberOfLines={1}>
190
111
  {statusText}
191
112
  </Text>
192
- {!isFailed && (
193
- <View style={styles.progressBar}>
194
- <View style={styles.progressFill} />
195
- </View>
196
- )}
113
+ {!isFailed && <PendingJobProgressBar progress={job.progress} />}
197
114
  </View>
198
115
  {renderActions ? (
199
116
  renderActions(job)
200
117
  ) : (
201
- <View style={styles.actions}>
202
- {isFailed && onRetry && (
203
- <TouchableOpacity
204
- style={styles.actionButton}
205
- onPress={() => onRetry(job.id)}
206
- >
207
- <Text style={{ color: mergedColors.text }}>↻</Text>
208
- </TouchableOpacity>
209
- )}
210
- {onCancel && (
211
- <TouchableOpacity
212
- style={styles.actionButton}
213
- onPress={() => onCancel(job.id)}
214
- >
215
- <Text style={{ color: mergedColors.error }}>✕</Text>
216
- </TouchableOpacity>
217
- )}
218
- </View>
118
+ <PendingJobCardActions
119
+ id={job.id}
120
+ isFailed={isFailed}
121
+ onCancel={onCancel}
122
+ onRetry={onRetry}
123
+ />
219
124
  )}
220
125
  </View>
221
126
  </View>
@@ -0,0 +1,73 @@
1
+ /**
2
+ * PendingJobCardActions
3
+ * Action buttons for the PendingJobCard
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TouchableOpacity, Text, StyleSheet } from "react-native";
8
+ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
9
+
10
+ export interface PendingJobCardActionsProps {
11
+ id: string;
12
+ isFailed: boolean;
13
+ onCancel?: (id: string) => void;
14
+ onRetry?: (id: string) => void;
15
+ textColor?: string;
16
+ errorColor?: string;
17
+ backgroundColor?: string;
18
+ }
19
+
20
+ export const PendingJobCardActions: React.FC<PendingJobCardActionsProps> = ({
21
+ id,
22
+ isFailed,
23
+ onCancel,
24
+ onRetry,
25
+ textColor,
26
+ errorColor,
27
+ backgroundColor,
28
+ }) => {
29
+ const tokens = useAppDesignTokens();
30
+
31
+ const styles = StyleSheet.create({
32
+ actions: {
33
+ flexDirection: "row",
34
+ gap: 8,
35
+ marginTop: 8,
36
+ },
37
+ actionButton: {
38
+ width: 32,
39
+ height: 32,
40
+ borderRadius: 16,
41
+ backgroundColor: backgroundColor || tokens.colors.backgroundSecondary,
42
+ justifyContent: "center",
43
+ alignItems: "center",
44
+ },
45
+ text: {
46
+ color: textColor || tokens.colors.textPrimary,
47
+ },
48
+ errorText: {
49
+ color: errorColor || tokens.colors.error,
50
+ },
51
+ });
52
+
53
+ return (
54
+ <View style={styles.actions}>
55
+ {isFailed && onRetry && (
56
+ <TouchableOpacity
57
+ style={styles.actionButton}
58
+ onPress={() => onRetry(id)}
59
+ >
60
+ <Text style={styles.text}>↻</Text>
61
+ </TouchableOpacity>
62
+ )}
63
+ {onCancel && (
64
+ <TouchableOpacity
65
+ style={styles.actionButton}
66
+ onPress={() => onCancel(id)}
67
+ >
68
+ <Text style={styles.errorText}>✕</Text>
69
+ </TouchableOpacity>
70
+ )}
71
+ </View>
72
+ );
73
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * PendingJobProgressBar
3
+ * Individual progress bar for the PendingJobCard
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
9
+
10
+ export interface PendingJobProgressBarProps {
11
+ progress: number;
12
+ backgroundColor?: string;
13
+ fillColor?: string;
14
+ }
15
+
16
+ export const PendingJobProgressBar: React.FC<PendingJobProgressBarProps> = ({
17
+ progress,
18
+ backgroundColor,
19
+ fillColor,
20
+ }) => {
21
+ const tokens = useAppDesignTokens();
22
+
23
+ return (
24
+ <View
25
+ style={[
26
+ styles.progressContainer,
27
+ { backgroundColor: backgroundColor || tokens.colors.borderLight },
28
+ ]}
29
+ >
30
+ <View
31
+ style={[
32
+ styles.progressFill,
33
+ {
34
+ backgroundColor: fillColor || tokens.colors.primary,
35
+ width: `${Math.max(0, Math.min(100, progress))}%`,
36
+ },
37
+ ]}
38
+ />
39
+ </View>
40
+ );
41
+ };
42
+
43
+ const styles = StyleSheet.create({
44
+ progressContainer: {
45
+ height: 4,
46
+ borderRadius: 2,
47
+ marginTop: 8,
48
+ overflow: "hidden",
49
+ },
50
+ progressFill: {
51
+ height: "100%",
52
+ borderRadius: 2,
53
+ },
54
+ });
@@ -1,21 +1,22 @@
1
- /**
2
- * Presentation Components
3
- */
4
-
5
- export {
6
- GenerationProgressModal,
7
- } from "./GenerationProgressModal";
1
+ export { GenerationProgressModal } from "./GenerationProgressModal";
2
+ export { GenerationProgressContent } from "./GenerationProgressContent";
3
+ export { GenerationProgressBar } from "./GenerationProgressBar";
4
+ export { PendingJobCard } from "./PendingJobCard";
5
+ export { PendingJobProgressBar } from "./PendingJobProgressBar";
6
+ export { PendingJobCardActions } from "./PendingJobCardActions";
8
7
 
9
8
  export type {
10
9
  GenerationProgressModalProps,
11
10
  GenerationProgressRenderProps,
12
11
  } from "./GenerationProgressModal";
13
12
 
14
- export { PendingJobCard } from "./PendingJobCard";
13
+ export type { GenerationProgressContentProps } from "./GenerationProgressContent";
14
+ export type { GenerationProgressBarProps } from "./GenerationProgressBar";
15
15
 
16
16
  export type {
17
17
  PendingJobCardProps,
18
- PendingJobCardStyles,
19
- PendingJobCardColors,
20
18
  StatusLabels,
21
19
  } from "./PendingJobCard";
20
+
21
+ export type { PendingJobProgressBarProps } from "./PendingJobProgressBar";
22
+ export type { PendingJobCardActionsProps } from "./PendingJobCardActions";
@@ -18,4 +18,5 @@ export { useBackgroundGeneration } from "./use-background-generation";
18
18
  export type {
19
19
  UseBackgroundGenerationOptions,
20
20
  UseBackgroundGenerationReturn,
21
+ DirectExecutionResult,
21
22
  } from "./use-background-generation";
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * useBackgroundGeneration Hook
3
- * Executes AI generation tasks in the background with queue management
3
+ * Executes AI generation tasks with optional queue management
4
+ * - mode: 'direct' - Execute immediately, no queue UI (for images)
5
+ * - mode: 'queued' - Use pending jobs queue with UI (for videos)
4
6
  */
5
7
 
6
- import { useCallback, useRef } from "react";
8
+ import { useCallback, useRef, useState } from "react";
7
9
  import { usePendingJobs } from "./use-pending-jobs";
8
10
  import type {
9
11
  BackgroundJob,
@@ -18,15 +20,27 @@ export interface UseBackgroundGenerationOptions<TInput, TResult>
18
20
  readonly onJobComplete?: (job: BackgroundJob<TInput, TResult>) => void;
19
21
  readonly onJobError?: (job: BackgroundJob<TInput, TResult>) => void;
20
22
  readonly onAllComplete?: () => void;
23
+ readonly onProgress?: (progress: number) => void;
24
+ }
25
+
26
+ export interface DirectExecutionResult<TResult> {
27
+ readonly success: boolean;
28
+ readonly result?: TResult;
29
+ readonly error?: string;
21
30
  }
22
31
 
23
32
  export interface UseBackgroundGenerationReturn<TInput, TResult> {
24
33
  readonly startJob: (input: TInput, type: string) => Promise<string>;
34
+ readonly executeDirectly: (
35
+ input: TInput,
36
+ ) => Promise<DirectExecutionResult<TResult>>;
25
37
  readonly cancelJob: (id: string) => void;
26
38
  readonly retryJob: (id: string) => void;
27
39
  readonly pendingJobs: BackgroundJob<TInput, TResult>[];
28
40
  readonly activeJobCount: number;
29
41
  readonly hasActiveJobs: boolean;
42
+ readonly isProcessing: boolean;
43
+ readonly progress: number;
30
44
  }
31
45
 
32
46
  export function useBackgroundGeneration<TInput = unknown, TResult = unknown>(
@@ -38,16 +52,41 @@ export function useBackgroundGeneration<TInput = unknown, TResult = unknown>(
38
52
  new Map(),
39
53
  );
40
54
 
41
- const {
42
- jobs,
43
- addJobAsync,
44
- updateJob,
45
- removeJob,
46
- getJob,
47
- } = usePendingJobs<TInput, TResult>({
55
+ const [isProcessing, setIsProcessing] = useState(false);
56
+ const [progress, setProgress] = useState(0);
57
+
58
+ const { jobs, addJobAsync, updateJob, removeJob, getJob } = usePendingJobs<
59
+ TInput,
60
+ TResult
61
+ >({
48
62
  queryKey: config.queryKey,
49
63
  });
50
64
 
65
+ const executeDirectly = useCallback(
66
+ async (input: TInput): Promise<DirectExecutionResult<TResult>> => {
67
+ const { executor } = options;
68
+
69
+ setIsProcessing(true);
70
+ setProgress(0);
71
+
72
+ try {
73
+ const result = await executor.execute(input, (p) => {
74
+ setProgress(p);
75
+ options.onProgress?.(p);
76
+ });
77
+
78
+ setProgress(100);
79
+ return { success: true, result };
80
+ } catch (error) {
81
+ const errorMsg = error instanceof Error ? error.message : String(error);
82
+ return { success: false, error: errorMsg };
83
+ } finally {
84
+ setIsProcessing(false);
85
+ }
86
+ },
87
+ [options],
88
+ );
89
+
51
90
  const executeJob = useCallback(
52
91
  async (jobId: string, input: TInput) => {
53
92
  const { executor } = options;
@@ -58,8 +97,8 @@ export function useBackgroundGeneration<TInput = unknown, TResult = unknown>(
58
97
  updates: { status: "processing", progress: 10 },
59
98
  });
60
99
 
61
- const result = await executor.execute(input, (progress) => {
62
- updateJob({ id: jobId, updates: { progress } });
100
+ const result = await executor.execute(input, (p) => {
101
+ updateJob({ id: jobId, updates: { progress: p } });
63
102
  });
64
103
 
65
104
  updateJob({
@@ -84,11 +123,7 @@ export function useBackgroundGeneration<TInput = unknown, TResult = unknown>(
84
123
 
85
124
  updateJob({
86
125
  id: jobId,
87
- updates: {
88
- status: "failed",
89
- error: errorMsg,
90
- progress: 0,
91
- },
126
+ updates: { status: "failed", error: errorMsg, progress: 0 },
92
127
  });
93
128
 
94
129
  const failedJob = getJob(jobId);
@@ -101,7 +136,6 @@ export function useBackgroundGeneration<TInput = unknown, TResult = unknown>(
101
136
  }
102
137
  } finally {
103
138
  activeJobsRef.current.delete(jobId);
104
-
105
139
  if (activeJobsRef.current.size === 0) {
106
140
  options.onAllComplete?.();
107
141
  }
@@ -125,8 +159,7 @@ export function useBackgroundGeneration<TInput = unknown, TResult = unknown>(
125
159
  });
126
160
 
127
161
  activeJobsRef.current.add(jobId);
128
-
129
- executeJob(jobId, input);
162
+ void executeJob(jobId, input);
130
163
 
131
164
  return jobId;
132
165
  },
@@ -146,19 +179,21 @@ export function useBackgroundGeneration<TInput = unknown, TResult = unknown>(
146
179
  (id: string) => {
147
180
  const jobData = jobInputsRef.current.get(id);
148
181
  if (!jobData) return;
149
-
150
182
  removeJob(id);
151
- startJob(jobData.input, jobData.type);
183
+ void startJob(jobData.input, jobData.type);
152
184
  },
153
185
  [removeJob, startJob],
154
186
  );
155
187
 
156
188
  return {
157
189
  startJob,
190
+ executeDirectly,
158
191
  cancelJob,
159
192
  retryJob,
160
193
  pendingJobs: jobs,
161
194
  activeJobCount: activeJobsRef.current.size,
162
195
  hasActiveJobs: activeJobsRef.current.size > 0,
196
+ isProcessing,
197
+ progress,
163
198
  };
164
199
  }
@@ -43,7 +43,7 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
43
43
  });
44
44
 
45
45
  const addJobMutation = useMutation({
46
- mutationFn: async (input: AddJobInput<TInput>) => {
46
+ mutationFn: (input: AddJobInput<TInput>) => {
47
47
  const newJob: BackgroundJob<TInput, TResult> = {
48
48
  id: input.id,
49
49
  input: input.input,
@@ -52,7 +52,7 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
52
52
  progress: input.progress ?? 0,
53
53
  createdAt: new Date(),
54
54
  };
55
- return newJob;
55
+ return Promise.resolve(newJob);
56
56
  },
57
57
  onSuccess: (newJob) => {
58
58
  queryClient.setQueryData<BackgroundJob<TInput, TResult>[]>(
@@ -63,7 +63,8 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
63
63
  });
64
64
 
65
65
  const updateJobMutation = useMutation({
66
- mutationFn: async ({ id, updates }: UpdateJobInput) => ({ id, updates }),
66
+ mutationFn: ({ id, updates }: UpdateJobInput) =>
67
+ Promise.resolve({ id, updates }),
67
68
  onSuccess: ({ id, updates }) => {
68
69
  queryClient.setQueryData<BackgroundJob<TInput, TResult>[]>(
69
70
  queryKey,
@@ -78,7 +79,7 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
78
79
  });
79
80
 
80
81
  const removeJobMutation = useMutation({
81
- mutationFn: async (id: string) => id,
82
+ mutationFn: (id: string) => Promise.resolve(id),
82
83
  onSuccess: (id) => {
83
84
  queryClient.setQueryData<BackgroundJob<TInput, TResult>[]>(
84
85
  queryKey,
@@ -88,7 +89,7 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
88
89
  });
89
90
 
90
91
  const clearCompletedMutation = useMutation({
91
- mutationFn: async () => null,
92
+ mutationFn: () => Promise.resolve(null),
92
93
  onSuccess: () => {
93
94
  queryClient.setQueryData<BackgroundJob<TInput, TResult>[]>(
94
95
  queryKey,
@@ -98,7 +99,7 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
98
99
  });
99
100
 
100
101
  const clearFailedMutation = useMutation({
101
- mutationFn: async () => null,
102
+ mutationFn: () => Promise.resolve(null),
102
103
  onSuccess: () => {
103
104
  queryClient.setQueryData<BackgroundJob<TInput, TResult>[]>(
104
105
  queryKey,