@umituz/react-native-ai-generation-content 1.12.26 → 1.12.29

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.12.26",
3
+ "version": "1.12.29",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Gallery Empty States
3
+ * Handles different empty state scenarios for gallery
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, ActivityIndicator, StyleSheet } from "react-native";
8
+ import type { DesignTokens } from "@umituz/react-native-design-system";
9
+ import { EmptyState } from "./EmptyState";
10
+ import type { Creation } from "../../domain/entities/Creation";
11
+
12
+ interface GalleryEmptyStatesProps {
13
+ isLoading: boolean;
14
+ creations: Creation[] | undefined;
15
+ isFiltered: boolean;
16
+ tokens: DesignTokens;
17
+ t: (key: string) => string;
18
+ emptyTitle: string;
19
+ emptyDescription: string;
20
+ emptyActionLabel?: string;
21
+ onEmptyAction?: () => void;
22
+ onClearFilters: () => void;
23
+ }
24
+
25
+ export function GalleryEmptyStates({
26
+ isLoading,
27
+ creations,
28
+ isFiltered,
29
+ tokens,
30
+ t,
31
+ emptyTitle,
32
+ emptyDescription,
33
+ emptyActionLabel,
34
+ onEmptyAction,
35
+ onClearFilters,
36
+ }: GalleryEmptyStatesProps) {
37
+ const styles = createStyles(tokens);
38
+
39
+ // 1. Loading State
40
+ if (isLoading && (!creations || creations?.length === 0)) {
41
+ return (
42
+ <View style={styles.centerContainer}>
43
+ <ActivityIndicator size="large" color={tokens.colors.primary} />
44
+ </View>
45
+ );
46
+ }
47
+
48
+ // 2. System Empty State (User has NO creations at all)
49
+ if (!creations || creations?.length === 0) {
50
+ return (
51
+ <View style={styles.centerContainer}>
52
+ <EmptyState
53
+ title={emptyTitle}
54
+ description={emptyDescription}
55
+ actionLabel={emptyActionLabel}
56
+ onAction={onEmptyAction}
57
+ />
58
+ </View>
59
+ );
60
+ }
61
+
62
+ // 3. Filter Empty State (User has creations, but filter returns none)
63
+ if (isFiltered) {
64
+ return (
65
+ <View style={styles.centerContainer}>
66
+ <EmptyState
67
+ title={t("common.no_results") || "No results"}
68
+ description={t("common.no_results_description") || "Try changing your filters"}
69
+ actionLabel={t("common.clear_all") || "Clear All"}
70
+ onAction={onClearFilters}
71
+ />
72
+ </View>
73
+ );
74
+ }
75
+
76
+ return null;
77
+ }
78
+
79
+ const createStyles = (tokens: DesignTokens) => StyleSheet.create({
80
+ centerContainer: {
81
+ flex: 1,
82
+ justifyContent: 'center',
83
+ alignItems: 'center',
84
+ minHeight: 400,
85
+ paddingHorizontal: tokens.spacing.xl,
86
+ },
87
+ });
@@ -4,6 +4,7 @@
4
4
 
5
5
  export { GalleryHeader } from "./GalleryHeader";
6
6
  export { EmptyState } from "./EmptyState";
7
+ export { GalleryEmptyStates } from "./GalleryEmptyStates";
7
8
  export { FilterChips } from "./FilterChips";
8
9
  export { CreationsHomeCard } from "./CreationsHomeCard";
9
10
  export { CreationCard } from "./CreationCard";
@@ -1,5 +1,5 @@
1
1
  import React, { useMemo, useCallback, useState } from "react";
2
- import { View, StyleSheet, ActivityIndicator, type LayoutChangeEvent } from "react-native";
2
+ import { View, StyleSheet, type LayoutChangeEvent } from "react-native";
3
3
  import { useAppDesignTokens, useAlert, AlertType, AlertMode, useSharing, type DesignTokens } from "@umituz/react-native-design-system";
4
4
  import { BottomSheetModal } from '@gorhom/bottom-sheet';
5
5
  import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -7,7 +7,7 @@ import { useFocusEffect } from "@react-navigation/native";
7
7
  import { useCreations } from "../hooks/useCreations";
8
8
  import { useDeleteCreation } from "../hooks/useDeleteCreation";
9
9
  import { useCreationsFilter } from "../hooks/useCreationsFilter";
10
- import { GalleryHeader, EmptyState, CreationsGrid, FilterBottomSheet, CreationImageViewer } from "../components";
10
+ import { GalleryHeader, CreationsGrid, FilterBottomSheet, CreationImageViewer, GalleryEmptyStates } from "../components";
11
11
  import { getTranslatedTypes, getFilterCategoriesFromConfig } from "../utils/filterUtils";
12
12
  import type { Creation } from "../../domain/entities/Creation";
13
13
  import type { CreationsConfig } from "../../domain/value-objects/CreationsConfig";
@@ -97,43 +97,20 @@ export function CreationsGalleryScreen({
97
97
 
98
98
  const styles = useStyles(tokens);
99
99
 
100
- // Define empty state content based on state
101
- const renderEmptyComponent = useMemo(() => {
102
- // 1. Loading State
103
- if (isLoading && (!creations || creations?.length === 0)) {
104
- return (
105
- <View style={styles.centerContainer}>
106
- <ActivityIndicator size="large" color={tokens.colors.primary} />
107
- </View>
108
- );
109
- }
110
-
111
- // 2. System Empty State (User has NO creations at all)
112
- if (!creations || creations?.length === 0) {
113
- return (
114
- <View style={styles.centerContainer}>
115
- <EmptyState
116
- title={t(config.translations.empty)}
117
- description={t(config.translations.emptyDescription)}
118
- actionLabel={emptyActionLabel}
119
- onAction={onEmptyAction}
120
- />
121
- </View>
122
- );
123
- }
124
-
125
- // 3. Filter Empty State (User has creations, but filter returns none)
126
- return (
127
- <View style={styles.centerContainer}>
128
- <EmptyState
129
- title={t("common.no_results") || "No results"}
130
- description={t("common.no_results_description") || "Try changing your filters"}
131
- actionLabel={t("common.clear_all") || "Clear All"}
132
- onAction={clearFilters}
133
- />
134
- </View>
135
- );
136
- }, [isLoading, creations, config, t, emptyActionLabel, onEmptyAction, clearFilters, styles.centerContainer, tokens.colors.primary]);
100
+ const renderEmptyComponent = useMemo(() => (
101
+ <GalleryEmptyStates
102
+ isLoading={isLoading}
103
+ creations={creations}
104
+ isFiltered={isFiltered}
105
+ tokens={tokens}
106
+ t={t}
107
+ emptyTitle={t(config.translations.empty)}
108
+ emptyDescription={t(config.translations.emptyDescription)}
109
+ emptyActionLabel={emptyActionLabel}
110
+ onEmptyAction={onEmptyAction}
111
+ onClearFilters={clearFilters}
112
+ />
113
+ ), [isLoading, creations, isFiltered, tokens, t, config, emptyActionLabel, onEmptyAction, clearFilters]);
137
114
 
138
115
  if (selectedCreation) {
139
116
  return (
@@ -0,0 +1,142 @@
1
+ /**
2
+ * AI Service Processor
3
+ * Handles processing of different AI service types
4
+ */
5
+
6
+ import type { FaceSwapConfig } from '../../domain/entities/FaceSwapConfig';
7
+ import type { PhotoRestorationConfig } from '../../domain/entities/PhotoRestorationConfig';
8
+ import type { ImageEnhancementConfig } from '../../domain/entities/ImageEnhancementConfig';
9
+ import type { StyleTransferConfig } from '../../domain/entities/StyleTransferConfig';
10
+ import type { BackgroundRemovalConfig } from '../../domain/entities/BackgroundRemovalConfig';
11
+ import type { TextGenerationConfig } from '../../domain/entities/TextGenerationConfig';
12
+ import type { ColorizationConfig } from '../../domain/entities/ColorizationConfig';
13
+ import type {
14
+ IFaceSwapService,
15
+ IPhotoRestorationService,
16
+ IImageEnhancementService,
17
+ IStyleTransferService,
18
+ IBackgroundRemovalService,
19
+ ITextGenerationService,
20
+ IColorizationService,
21
+ } from '../../domain/repositories/IAIPromptServices';
22
+ import type { AIPromptResult } from '../../domain/entities/types';
23
+ import type { AIPromptTemplate } from '../../domain/entities/AIPromptTemplate';
24
+
25
+ export type AIConfig =
26
+ | { type: 'face-swap'; config: FaceSwapConfig }
27
+ | { type: 'photo-restoration'; config: PhotoRestorationConfig }
28
+ | { type: 'image-enhancement'; config: ImageEnhancementConfig }
29
+ | { type: 'style-transfer'; config: StyleTransferConfig }
30
+ | { type: 'background-removal'; config: BackgroundRemovalConfig }
31
+ | { type: 'text-generation'; config: TextGenerationConfig }
32
+ | { type: 'colorization'; config: ColorizationConfig };
33
+
34
+ export interface AIServices {
35
+ faceSwap: IFaceSwapService;
36
+ photoRestoration: IPhotoRestorationService;
37
+ imageEnhancement: IImageEnhancementService;
38
+ styleTransfer: IStyleTransferService;
39
+ backgroundRemoval: IBackgroundRemovalService;
40
+ textGeneration: ITextGenerationService;
41
+ colorization: IColorizationService;
42
+ }
43
+
44
+ export interface ProcessResult {
45
+ template: AIPromptTemplate;
46
+ prompt: string;
47
+ config: Record<string, unknown>;
48
+ }
49
+
50
+ export class AIServiceProcessor {
51
+ constructor(private services: AIServices) { }
52
+
53
+ async process(aiConfig: AIConfig): Promise<ProcessResult> {
54
+ const { templateResult, promptResult } = await this.executeService(aiConfig);
55
+
56
+ if (!templateResult?.success || !templateResult.data) {
57
+ throw new Error('Failed to generate template');
58
+ }
59
+
60
+ if (!promptResult?.success || !promptResult.data) {
61
+ throw new Error('Failed to generate prompt');
62
+ }
63
+
64
+ return {
65
+ template: templateResult.data,
66
+ prompt: promptResult.data,
67
+ config: aiConfig.config as unknown as Record<string, unknown>,
68
+ };
69
+ }
70
+
71
+ async getAvailableStyles(serviceType: string): Promise<string[]> {
72
+ switch (serviceType) {
73
+ case 'face-swap':
74
+ return await this.services.faceSwap.getAvailableStyles();
75
+ case 'style-transfer':
76
+ return await this.services.styleTransfer.getAvailableStyles();
77
+ default:
78
+ return [];
79
+ }
80
+ }
81
+
82
+ private async executeService(aiConfig: AIConfig): Promise<{
83
+ templateResult: AIPromptResult<AIPromptTemplate> | undefined;
84
+ promptResult: AIPromptResult<string> | undefined;
85
+ }> {
86
+ let templateResult: AIPromptResult<AIPromptTemplate> | undefined;
87
+ let promptResult: AIPromptResult<string> | undefined;
88
+
89
+ switch (aiConfig.type) {
90
+ case 'face-swap':
91
+ templateResult = await this.services.faceSwap.generateTemplate(aiConfig.config);
92
+ if (templateResult.success && templateResult.data) {
93
+ promptResult = await this.services.faceSwap.generatePrompt(templateResult.data, aiConfig.config);
94
+ }
95
+ break;
96
+
97
+ case 'photo-restoration':
98
+ templateResult = await this.services.photoRestoration.generateTemplate(aiConfig.config);
99
+ if (templateResult.success && templateResult.data) {
100
+ promptResult = await this.services.photoRestoration.generatePrompt(templateResult.data, aiConfig.config);
101
+ }
102
+ break;
103
+
104
+ case 'image-enhancement':
105
+ templateResult = await this.services.imageEnhancement.generateTemplate(aiConfig.config);
106
+ if (templateResult.success && templateResult.data) {
107
+ promptResult = await this.services.imageEnhancement.generatePrompt(templateResult.data, aiConfig.config);
108
+ }
109
+ break;
110
+
111
+ case 'style-transfer':
112
+ templateResult = await this.services.styleTransfer.generateTemplate(aiConfig.config);
113
+ if (templateResult.success && templateResult.data) {
114
+ promptResult = await this.services.styleTransfer.generatePrompt(templateResult.data, aiConfig.config);
115
+ }
116
+ break;
117
+
118
+ case 'background-removal':
119
+ templateResult = await this.services.backgroundRemoval.generateTemplate(aiConfig.config);
120
+ if (templateResult.success && templateResult.data) {
121
+ promptResult = await this.services.backgroundRemoval.generatePrompt(templateResult.data, aiConfig.config);
122
+ }
123
+ break;
124
+
125
+ case 'text-generation':
126
+ templateResult = await this.services.textGeneration.generateTemplate(aiConfig.config);
127
+ if (templateResult.success && templateResult.data) {
128
+ promptResult = await this.services.textGeneration.generatePrompt(templateResult.data, aiConfig.config);
129
+ }
130
+ break;
131
+
132
+ case 'colorization':
133
+ templateResult = await this.services.colorization.generateTemplate(aiConfig.config);
134
+ if (templateResult.success && templateResult.data) {
135
+ promptResult = await this.services.colorization.generatePrompt(templateResult.data, aiConfig.config);
136
+ }
137
+ break;
138
+ }
139
+
140
+ return { templateResult, promptResult };
141
+ }
142
+ }
@@ -1,50 +1,12 @@
1
- import { useState, useCallback } from 'react';
2
- import type {
3
- FaceSwapConfig
4
- } from '../../domain/entities/FaceSwapConfig';
5
- import type {
6
- PhotoRestorationConfig
7
- } from '../../domain/entities/PhotoRestorationConfig';
8
- import type {
9
- ImageEnhancementConfig
10
- } from '../../domain/entities/ImageEnhancementConfig';
11
- import type {
12
- StyleTransferConfig
13
- } from '../../domain/entities/StyleTransferConfig';
14
- import type {
15
- BackgroundRemovalConfig
16
- } from '../../domain/entities/BackgroundRemovalConfig';
17
- import type {
18
- TextGenerationConfig
19
- } from '../../domain/entities/TextGenerationConfig';
20
- import type {
21
- ColorizationConfig
22
- } from '../../domain/entities/ColorizationConfig';
23
- import type {
24
- IFaceSwapService,
25
- IPhotoRestorationService,
26
- IImageEnhancementService,
27
- IStyleTransferService,
28
- IBackgroundRemovalService,
29
- ITextGenerationService,
30
- IColorizationService
31
- } from '../../domain/repositories/IAIPromptServices';
1
+ import { useState, useCallback, useMemo } from 'react';
32
2
  import type { ITemplateRepository } from '../../domain/repositories/ITemplateRepository';
33
3
  import type { IPromptHistoryRepository } from '../../domain/repositories/IPromptHistoryRepository';
34
4
  import type { GeneratedPrompt } from '../../domain/entities/GeneratedPrompt';
35
5
  import { createGeneratedPrompt } from '../../domain/entities/GeneratedPrompt';
36
6
  import { useAsyncState } from './useAsyncState';
37
- import type { AIPromptResult } from '../../domain/entities/types';
38
- import type { AIPromptTemplate } from '../../domain/entities/AIPromptTemplate';
7
+ import { AIServiceProcessor, type AIConfig, type AIServices } from '../../infrastructure/services/AIServiceProcessor';
39
8
 
40
- export type AIConfig =
41
- | { type: 'face-swap'; config: FaceSwapConfig }
42
- | { type: 'photo-restoration'; config: PhotoRestorationConfig }
43
- | { type: 'image-enhancement'; config: ImageEnhancementConfig }
44
- | { type: 'style-transfer'; config: StyleTransferConfig }
45
- | { type: 'background-removal'; config: BackgroundRemovalConfig }
46
- | { type: 'text-generation'; config: TextGenerationConfig }
47
- | { type: 'colorization'; config: ColorizationConfig };
9
+ export type { AIConfig };
48
10
 
49
11
  export interface UseAIServicesState {
50
12
  generatedPrompt: GeneratedPrompt | null;
@@ -60,15 +22,7 @@ export interface UseAIServicesActions {
60
22
  }
61
23
 
62
24
  export const useAIServices = (
63
- services: {
64
- faceSwap: IFaceSwapService;
65
- photoRestoration: IPhotoRestorationService;
66
- imageEnhancement: IImageEnhancementService;
67
- styleTransfer: IStyleTransferService;
68
- backgroundRemoval: IBackgroundRemovalService;
69
- textGeneration: ITextGenerationService;
70
- colorization: IColorizationService;
71
- },
25
+ services: AIServices,
72
26
  repositories: {
73
27
  template: ITemplateRepository;
74
28
  history: IPromptHistoryRepository;
@@ -85,110 +39,39 @@ export const useAIServices = (
85
39
 
86
40
  const [currentService, setCurrentService] = useState<string | null>(null);
87
41
 
42
+ const processor = useMemo(() => new AIServiceProcessor(services), [services]);
43
+
88
44
  const processRequest = useCallback(async (aiConfig: AIConfig): Promise<void> => {
89
45
  clearError();
90
46
  setCurrentService(aiConfig.type);
91
47
 
92
48
  try {
93
- let templateResult: AIPromptResult<AIPromptTemplate> | undefined;
94
- let promptResult: AIPromptResult<string> | undefined;
95
-
96
- switch (aiConfig.type) {
97
- case 'face-swap':
98
- templateResult = await services.faceSwap.generateTemplate(aiConfig.config);
99
- if (templateResult.success && templateResult.data) {
100
- promptResult = await services.faceSwap.generatePrompt(templateResult.data, aiConfig.config);
101
- }
102
- break;
103
-
104
- case 'photo-restoration':
105
- templateResult = await services.photoRestoration.generateTemplate(aiConfig.config);
106
- if (templateResult.success && templateResult.data) {
107
- promptResult = await services.photoRestoration.generatePrompt(templateResult.data, aiConfig.config);
108
- }
109
- break;
110
-
111
- case 'image-enhancement':
112
- templateResult = await services.imageEnhancement.generateTemplate(aiConfig.config);
113
- if (templateResult.success && templateResult.data) {
114
- promptResult = await services.imageEnhancement.generatePrompt(templateResult.data, aiConfig.config);
115
- }
116
- break;
117
-
118
- case 'style-transfer':
119
- templateResult = await services.styleTransfer.generateTemplate(aiConfig.config);
120
- if (templateResult.success && templateResult.data) {
121
- promptResult = await services.styleTransfer.generatePrompt(templateResult.data, aiConfig.config);
122
- }
123
- break;
124
-
125
- case 'background-removal':
126
- templateResult = await services.backgroundRemoval.generateTemplate(aiConfig.config);
127
- if (templateResult.success && templateResult.data) {
128
- promptResult = await services.backgroundRemoval.generatePrompt(templateResult.data, aiConfig.config);
129
- }
130
- break;
131
-
132
- case 'text-generation':
133
- templateResult = await services.textGeneration.generateTemplate(aiConfig.config);
134
- if (templateResult.success && templateResult.data) {
135
- promptResult = await services.textGeneration.generatePrompt(templateResult.data, aiConfig.config);
136
- }
137
- break;
138
-
139
- case 'colorization':
140
- templateResult = await services.colorization.generateTemplate(aiConfig.config);
141
- if (templateResult.success && templateResult.data) {
142
- promptResult = await services.colorization.generatePrompt(templateResult.data, aiConfig.config);
143
- }
144
- break;
145
-
146
- default:
147
- setError('Unknown AI service type');
148
- return;
149
- }
150
-
151
- if (!templateResult?.success || !templateResult.data) {
152
- setError('Failed to generate template');
153
- return;
154
- }
155
-
156
- if (!promptResult?.success || !promptResult.data) {
157
- setError('Failed to generate prompt');
158
- return;
159
- }
49
+ const result = await processor.process(aiConfig);
160
50
 
161
51
  const newPrompt = createGeneratedPrompt({
162
- templateId: templateResult.data.id,
163
- generatedText: promptResult.data,
164
- variables: aiConfig.config as unknown as Record<string, unknown>,
52
+ templateId: result.template.id,
53
+ generatedText: result.prompt,
54
+ variables: result.config,
165
55
  });
166
56
 
167
57
  await repositories.history.save(newPrompt);
168
58
  setGeneratedPrompt(newPrompt);
169
59
 
170
- } catch {
171
- setError('An unexpected error occurred');
60
+ } catch (err) {
61
+ setError(err instanceof Error ? err.message : 'An unexpected error occurred');
172
62
  } finally {
173
63
  setCurrentService(null);
174
64
  }
175
- }, [services, repositories, setError, setGeneratedPrompt, clearError]);
65
+ }, [processor, repositories, setError, setGeneratedPrompt, clearError]);
176
66
 
177
67
  const getAvailableStyles = useCallback(async (serviceType: string): Promise<string[]> => {
178
68
  try {
179
- switch (serviceType) {
180
- case 'face-swap':
181
- return await services.faceSwap.getAvailableStyles();
182
- case 'style-transfer':
183
- return await services.styleTransfer.getAvailableStyles();
184
- default:
185
- return [];
186
- }
69
+ return await processor.getAvailableStyles(serviceType);
187
70
  } catch {
188
71
  setError('Failed to load available styles');
189
72
  return [];
190
73
  }
191
- }, [services, setError]);
74
+ }, [processor, setError]);
192
75
 
193
76
  const clearPrompt = useCallback(() => {
194
77
  setGeneratedPrompt(null);
@@ -9,15 +9,10 @@ import type {
9
9
  GenerationProgress,
10
10
  PollingConfig,
11
11
  } from "../../domain/entities";
12
- import { DEFAULT_POLLING_CONFIG } from "../../domain/entities";
13
- import type { IAIProvider, JobStatus } from "../../domain/interfaces";
14
- import { providerRegistry } from "./provider-registry.service";
15
- import {
16
- classifyError,
17
- isTransientError,
18
- } from "../utils/error-classifier.util";
19
- import { createPollingDelay } from "../utils/polling-interval.util";
20
- import { createProgressTracker } from "../utils/progress-calculator.util";
12
+ import { classifyError } from "../utils/error-classifier.util";
13
+ import { ProgressManager } from "./progress-manager";
14
+ import { JobPoller, type PollerConfig } from "./job-poller";
15
+ import { ProviderValidator } from "./provider-validator";
21
16
 
22
17
  declare const __DEV__: boolean;
23
18
 
@@ -27,7 +22,9 @@ export interface OrchestratorConfig {
27
22
  }
28
23
 
29
24
  class GenerationOrchestratorService {
30
- private config: OrchestratorConfig = {};
25
+ private progressManager = new ProgressManager();
26
+ private jobPoller = new JobPoller();
27
+ private providerValidator = new ProviderValidator();
31
28
 
32
29
  configure(config: OrchestratorConfig): void {
33
30
  if (typeof __DEV__ !== "undefined" && __DEV__) {
@@ -37,14 +34,19 @@ class GenerationOrchestratorService {
37
34
  hasStatusUpdate: !!config.onStatusUpdate,
38
35
  });
39
36
  }
40
- this.config = { ...this.config, ...config };
37
+
38
+ const pollerConfig: PollerConfig = {
39
+ polling: config.polling,
40
+ onStatusUpdate: config.onStatusUpdate,
41
+ };
42
+
43
+ this.jobPoller.configure(pollerConfig);
41
44
  }
42
45
 
43
46
  async generate<T = unknown>(
44
47
  request: GenerationRequest,
45
48
  ): Promise<GenerationResult<T>> {
46
- const provider = this.getProvider();
47
- const progressTracker = createProgressTracker();
49
+ const provider = this.providerValidator.getProvider();
48
50
  const startTime = Date.now();
49
51
 
50
52
  if (typeof __DEV__ !== "undefined" && __DEV__) {
@@ -60,11 +62,7 @@ class GenerationOrchestratorService {
60
62
  stage: GenerationProgress["stage"],
61
63
  subProgress = 0,
62
64
  ) => {
63
- const progress = progressTracker.setStatus(stage);
64
- request.onProgress?.({
65
- stage,
66
- progress: progress + subProgress,
67
- });
65
+ this.progressManager.updateProgress(stage, subProgress, request.onProgress);
68
66
  };
69
67
 
70
68
  try {
@@ -84,11 +82,19 @@ class GenerationOrchestratorService {
84
82
 
85
83
  updateProgress("generating");
86
84
 
87
- const result = await this.pollForResult<T>(
85
+ const result = await this.jobPoller.pollForResult<T>(
88
86
  provider,
89
87
  request.model,
90
88
  submission.requestId,
91
89
  request.onProgress,
90
+ (status, attempt, config) => {
91
+ this.progressManager.updateProgressFromStatus(
92
+ status,
93
+ attempt,
94
+ config,
95
+ request.onProgress,
96
+ );
97
+ },
92
98
  );
93
99
 
94
100
  updateProgress("completed");
@@ -139,149 +145,6 @@ class GenerationOrchestratorService {
139
145
  };
140
146
  }
141
147
  }
142
-
143
- private async pollForResult<T>(
144
- provider: IAIProvider,
145
- model: string,
146
- requestId: string,
147
- onProgress?: (progress: GenerationProgress) => void,
148
- ): Promise<T> {
149
- if (typeof __DEV__ !== "undefined" && __DEV__) {
150
- // eslint-disable-next-line no-console
151
- console.log("[Orchestrator] pollForResult() started", {
152
- provider: provider.providerId,
153
- model,
154
- requestId,
155
- });
156
- }
157
-
158
- const config = {
159
- ...DEFAULT_POLLING_CONFIG,
160
- ...this.config.polling,
161
- };
162
-
163
- let consecutiveErrors = 0;
164
-
165
- for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
166
- await createPollingDelay(attempt, config);
167
-
168
- if (typeof __DEV__ !== "undefined" && __DEV__ && attempt % 5 === 0) {
169
- // eslint-disable-next-line no-console
170
- console.log("[Orchestrator] pollForResult() attempt", {
171
- attempt,
172
- maxAttempts: config.maxAttempts,
173
- });
174
- }
175
-
176
- try {
177
- const status = await provider.getJobStatus(model, requestId);
178
-
179
- consecutiveErrors = 0;
180
-
181
- this.updateProgressFromStatus(status, attempt, config, onProgress);
182
-
183
- if (status.status === "COMPLETED") {
184
- if (typeof __DEV__ !== "undefined" && __DEV__) {
185
- // eslint-disable-next-line no-console
186
- console.log("[Orchestrator] pollForResult() job COMPLETED", {
187
- requestId,
188
- attempt,
189
- });
190
- }
191
- return provider.getJobResult<T>(model, requestId);
192
- }
193
-
194
- if (status.status === "FAILED") {
195
- throw new Error("Job failed on provider");
196
- }
197
-
198
- await this.config.onStatusUpdate?.(requestId, status.status);
199
- } catch (error) {
200
- if (isTransientError(error)) {
201
- consecutiveErrors++;
202
-
203
- if (consecutiveErrors >= config.maxConsecutiveErrors) {
204
- throw error;
205
- }
206
-
207
- continue;
208
- }
209
-
210
- throw error;
211
- }
212
- }
213
-
214
- throw new Error(
215
- `Polling timeout after ${config.maxAttempts} attempts`,
216
- );
217
- }
218
-
219
- private updateProgressFromStatus(
220
- status: JobStatus,
221
- attempt: number,
222
- config: PollingConfig,
223
- onProgress?: (progress: GenerationProgress) => void,
224
- ): void {
225
- const baseProgress = 25;
226
- const maxProgress = 85;
227
- const range = maxProgress - baseProgress;
228
-
229
- let progress: number;
230
-
231
- if (status.status === "IN_QUEUE") {
232
- progress = baseProgress + range * 0.2;
233
- } else if (status.status === "IN_PROGRESS") {
234
- const ratio = Math.min(attempt / (config.maxAttempts * 0.7), 1);
235
- progress = baseProgress + range * (0.2 + 0.6 * ratio);
236
- } else {
237
- progress = baseProgress;
238
- }
239
-
240
- onProgress?.({
241
- stage: "generating",
242
- progress: Math.round(progress),
243
- eta: status.eta,
244
- });
245
- }
246
-
247
- private getProvider(): IAIProvider {
248
- if (typeof __DEV__ !== "undefined" && __DEV__) {
249
- // eslint-disable-next-line no-console
250
- console.log("[Orchestrator] getProvider() called");
251
- }
252
-
253
- const provider = providerRegistry.getActiveProvider();
254
-
255
- if (!provider) {
256
- if (typeof __DEV__ !== "undefined" && __DEV__) {
257
- // eslint-disable-next-line no-console
258
- console.error("[Orchestrator] No active provider found!");
259
- }
260
- throw new Error(
261
- "No active AI provider. Register and set a provider first.",
262
- );
263
- }
264
-
265
- if (!provider.isInitialized()) {
266
- if (typeof __DEV__ !== "undefined" && __DEV__) {
267
- // eslint-disable-next-line no-console
268
- console.error("[Orchestrator] Provider not initialized:", provider.providerId);
269
- }
270
- throw new Error(
271
- `Provider ${provider.providerId} is not initialized.`,
272
- );
273
- }
274
-
275
- if (typeof __DEV__ !== "undefined" && __DEV__) {
276
- // eslint-disable-next-line no-console
277
- console.log("[Orchestrator] getProvider() returning:", {
278
- providerId: provider.providerId,
279
- isInitialized: provider.isInitialized(),
280
- });
281
- }
282
-
283
- return provider;
284
- }
285
148
  }
286
149
 
287
150
  export const generationOrchestrator = new GenerationOrchestratorService();
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Job Poller
3
+ * Handles polling logic for job status
4
+ */
5
+
6
+ import type { IAIProvider, JobStatus } from "../../domain/interfaces";
7
+ import { DEFAULT_POLLING_CONFIG, type PollingConfig, type GenerationProgress } from "../../domain/entities";
8
+ import { isTransientError } from "../utils/error-classifier.util";
9
+ import { createPollingDelay } from "../utils/polling-interval.util";
10
+
11
+ declare const __DEV__: boolean;
12
+
13
+ export interface PollerConfig {
14
+ polling?: Partial<PollingConfig>;
15
+ onStatusUpdate?: (requestId: string, status: string) => Promise<void>;
16
+ }
17
+
18
+ export class JobPoller {
19
+ private config: PollerConfig = {};
20
+
21
+ configure(config: PollerConfig): void {
22
+ this.config = { ...this.config, ...config };
23
+ }
24
+
25
+ async pollForResult<T>(
26
+ provider: IAIProvider,
27
+ model: string,
28
+ requestId: string,
29
+ onProgress?: (progress: GenerationProgress) => void,
30
+ onStatusUpdate?: (status: JobStatus, attempt: number, config: PollingConfig) => void,
31
+ ): Promise<T> {
32
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
33
+ // eslint-disable-next-line no-console
34
+ console.log("[JobPoller] pollForResult() started", {
35
+ provider: provider.providerId,
36
+ model,
37
+ requestId,
38
+ });
39
+ }
40
+
41
+ const config = {
42
+ ...DEFAULT_POLLING_CONFIG,
43
+ ...this.config.polling,
44
+ };
45
+
46
+ let consecutiveErrors = 0;
47
+
48
+ for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
49
+ await createPollingDelay(attempt, config);
50
+
51
+ if (typeof __DEV__ !== "undefined" && __DEV__ && attempt % 5 === 0) {
52
+ // eslint-disable-next-line no-console
53
+ console.log("[JobPoller] pollForResult() attempt", {
54
+ attempt,
55
+ maxAttempts: config.maxAttempts,
56
+ });
57
+ }
58
+
59
+ try {
60
+ const status = await provider.getJobStatus(model, requestId);
61
+
62
+ consecutiveErrors = 0;
63
+
64
+ onStatusUpdate?.(status, attempt, config);
65
+
66
+ if (status.status === "COMPLETED") {
67
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
68
+ // eslint-disable-next-line no-console
69
+ console.log("[JobPoller] pollForResult() job COMPLETED", {
70
+ requestId,
71
+ attempt,
72
+ });
73
+ }
74
+ return provider.getJobResult<T>(model, requestId);
75
+ }
76
+
77
+ if (status.status === "FAILED") {
78
+ throw new Error("Job failed on provider");
79
+ }
80
+
81
+ await this.config.onStatusUpdate?.(requestId, status.status);
82
+ } catch (error) {
83
+ if (isTransientError(error)) {
84
+ consecutiveErrors++;
85
+
86
+ if (consecutiveErrors >= config.maxConsecutiveErrors) {
87
+ throw error;
88
+ }
89
+
90
+ continue;
91
+ }
92
+
93
+ throw error;
94
+ }
95
+ }
96
+
97
+ throw new Error(
98
+ `Polling timeout after ${config.maxAttempts} attempts`,
99
+ );
100
+ }
101
+ }
102
+
103
+ export const jobPoller = new JobPoller();
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Progress Manager
3
+ * Handles progress tracking and updates during generation
4
+ */
5
+
6
+ import type { GenerationProgress, PollingConfig } from "../../domain/entities";
7
+ import type { JobStatus } from "../../domain/interfaces";
8
+ import { createProgressTracker } from "../utils/progress-calculator.util";
9
+
10
+ export class ProgressManager {
11
+ private progressTracker = createProgressTracker();
12
+
13
+ updateProgress(
14
+ stage: GenerationProgress["stage"],
15
+ subProgress: number,
16
+ onProgress?: (progress: GenerationProgress) => void,
17
+ ): void {
18
+ const progress = this.progressTracker.setStatus(stage);
19
+ onProgress?.({
20
+ stage,
21
+ progress: progress + subProgress,
22
+ });
23
+ }
24
+
25
+ updateProgressFromStatus(
26
+ status: JobStatus,
27
+ attempt: number,
28
+ config: PollingConfig,
29
+ onProgress?: (progress: GenerationProgress) => void,
30
+ ): void {
31
+ const baseProgress = 25;
32
+ const maxProgress = 85;
33
+ const range = maxProgress - baseProgress;
34
+
35
+ let progress: number;
36
+
37
+ if (status.status === "IN_QUEUE") {
38
+ progress = baseProgress + range * 0.2;
39
+ } else if (status.status === "IN_PROGRESS") {
40
+ const ratio = Math.min(attempt / (config.maxAttempts * 0.7), 1);
41
+ progress = baseProgress + range * (0.2 + 0.6 * ratio);
42
+ } else {
43
+ progress = baseProgress;
44
+ }
45
+
46
+ onProgress?.({
47
+ stage: "generating",
48
+ progress: Math.round(progress),
49
+ eta: status.eta,
50
+ });
51
+ }
52
+
53
+ reset(): void {
54
+ this.progressTracker = createProgressTracker();
55
+ }
56
+ }
57
+
58
+ export const progressManager = new ProgressManager();
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Provider Validator
3
+ * Validates provider availability and initialization
4
+ */
5
+
6
+ import type { IAIProvider } from "../../domain/interfaces";
7
+ import { providerRegistry } from "./provider-registry.service";
8
+
9
+ declare const __DEV__: boolean;
10
+
11
+ export class ProviderValidator {
12
+ getProvider(): IAIProvider {
13
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
14
+ // eslint-disable-next-line no-console
15
+ console.log("[ProviderValidator] getProvider() called");
16
+ }
17
+
18
+ const provider = providerRegistry.getActiveProvider();
19
+
20
+ if (!provider) {
21
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
22
+ // eslint-disable-next-line no-console
23
+ console.error("[ProviderValidator] No active provider found!");
24
+ }
25
+ throw new Error(
26
+ "No active AI provider. Register and set a provider first.",
27
+ );
28
+ }
29
+
30
+ if (!provider.isInitialized()) {
31
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
32
+ // eslint-disable-next-line no-console
33
+ console.error("[ProviderValidator] Provider not initialized:", provider.providerId);
34
+ }
35
+ throw new Error(
36
+ `Provider ${provider.providerId} is not initialized.`,
37
+ );
38
+ }
39
+
40
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
41
+ // eslint-disable-next-line no-console
42
+ console.log("[ProviderValidator] getProvider() returning:", {
43
+ providerId: provider.providerId,
44
+ isInitialized: provider.isInitialized(),
45
+ });
46
+ }
47
+
48
+ return provider;
49
+ }
50
+ }
51
+
52
+ export const providerValidator = new ProviderValidator();