@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 +6 -3
- package/src/domain/entities/job.types.ts +4 -0
- package/src/index.ts +10 -2
- package/src/presentation/components/GenerationProgressBar.tsx +77 -0
- package/src/presentation/components/GenerationProgressContent.tsx +123 -0
- package/src/presentation/components/GenerationProgressModal.tsx +47 -160
- package/src/presentation/components/PendingJobCard.tsx +32 -127
- package/src/presentation/components/PendingJobCardActions.tsx +73 -0
- package/src/presentation/components/PendingJobProgressBar.tsx +54 -0
- package/src/presentation/components/index.ts +11 -10
- package/src/presentation/hooks/index.ts +1 -0
- package/src/presentation/hooks/use-background-generation.ts +56 -21
- package/src/presentation/hooks/use-pending-jobs.ts +7 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.
|
|
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
|
-
"@
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
TouchableOpacity,
|
|
12
|
-
StyleSheet,
|
|
13
|
-
} from "react-native";
|
|
10
|
+
GenerationProgressContent,
|
|
11
|
+
GenerationProgressContentProps,
|
|
12
|
+
} from "./GenerationProgressContent";
|
|
14
13
|
|
|
15
|
-
export interface
|
|
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
|
|
38
|
+
dismissLabel,
|
|
49
39
|
onDismiss,
|
|
50
40
|
overlayColor = "rgba(0, 0, 0, 0.7)",
|
|
51
|
-
modalBackgroundColor
|
|
52
|
-
textColor
|
|
53
|
-
progressColor
|
|
54
|
-
progressBackgroundColor
|
|
55
|
-
dismissButtonColor
|
|
41
|
+
modalBackgroundColor,
|
|
42
|
+
textColor,
|
|
43
|
+
progressColor,
|
|
44
|
+
progressBackgroundColor,
|
|
45
|
+
dismissButtonColor,
|
|
56
46
|
renderContent,
|
|
57
47
|
}) => {
|
|
58
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
readonly renderActions?: (
|
|
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
|
|
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:
|
|
63
|
+
backgroundColor: tokens.colors.surface,
|
|
99
64
|
borderRadius: 16,
|
|
100
65
|
overflow: "hidden",
|
|
101
66
|
opacity: isFailed ? 0.7 : 1,
|
|
102
|
-
|
|
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:
|
|
120
|
-
...((customStyles.typeText as object) || {}),
|
|
83
|
+
color: tokens.colors.textPrimary,
|
|
121
84
|
},
|
|
122
85
|
statusText: {
|
|
123
86
|
fontSize: 12,
|
|
124
|
-
color: isFailed ?
|
|
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.
|
|
96
|
+
<View style={styles.thumbnailWrapper}>
|
|
174
97
|
{renderThumbnail(job)}
|
|
175
98
|
{!isFailed && (
|
|
176
99
|
<ActivityIndicator
|
|
177
|
-
color={
|
|
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
|
-
<
|
|
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
|
-
<
|
|
202
|
-
{
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export {
|
|
6
|
-
|
|
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 {
|
|
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";
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useBackgroundGeneration Hook
|
|
3
|
-
* Executes AI generation tasks
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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, (
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
102
|
+
mutationFn: () => Promise.resolve(null),
|
|
102
103
|
onSuccess: () => {
|
|
103
104
|
queryClient.setQueryData<BackgroundJob<TInput, TResult>[]>(
|
|
104
105
|
queryKey,
|