@umituz/react-native-ai-generation-content 1.0.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 ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@umituz/react-native-ai-generation-content",
3
+ "version": "1.0.1",
4
+ "description": "Provider-agnostic AI generation orchestration for React Native",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "files": [
8
+ "src"
9
+ ],
10
+ "scripts": {
11
+ "typecheck": "tsc --noEmit",
12
+ "lint": "eslint src --ext .ts,.tsx",
13
+ "lint:fix": "eslint src --ext .ts,.tsx --fix"
14
+ },
15
+ "keywords": [
16
+ "react-native",
17
+ "ai",
18
+ "generation",
19
+ "orchestration",
20
+ "fal",
21
+ "gemini",
22
+ "provider-agnostic"
23
+ ],
24
+ "author": "umituz",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/umituz/react-native-ai-generation-content.git"
29
+ },
30
+ "peerDependencies": {
31
+ "react": ">=18.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/react": "^19.0.0",
35
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
36
+ "@typescript-eslint/parser": "^7.0.0",
37
+ "eslint": "^8.57.0",
38
+ "typescript": "^5.3.0"
39
+ }
40
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * AI Generation Error Types
3
+ * Provider-agnostic error classification
4
+ */
5
+
6
+ export enum AIErrorType {
7
+ NETWORK = "NETWORK",
8
+ RATE_LIMIT = "RATE_LIMIT",
9
+ AUTHENTICATION = "AUTHENTICATION",
10
+ VALIDATION = "VALIDATION",
11
+ CONTENT_POLICY = "CONTENT_POLICY",
12
+ SERVER = "SERVER",
13
+ TIMEOUT = "TIMEOUT",
14
+ UNKNOWN = "UNKNOWN",
15
+ }
16
+
17
+ export interface AIErrorInfo {
18
+ type: AIErrorType;
19
+ messageKey: string;
20
+ retryable: boolean;
21
+ originalError?: unknown;
22
+ statusCode?: number;
23
+ }
24
+
25
+ export interface AIErrorMessages {
26
+ [AIErrorType.NETWORK]: string;
27
+ [AIErrorType.RATE_LIMIT]: string;
28
+ [AIErrorType.AUTHENTICATION]: string;
29
+ [AIErrorType.VALIDATION]: string;
30
+ [AIErrorType.CONTENT_POLICY]: string;
31
+ [AIErrorType.SERVER]: string;
32
+ [AIErrorType.TIMEOUT]: string;
33
+ [AIErrorType.UNKNOWN]: string;
34
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * AI Generation Types
3
+ * Core types for generation workflow
4
+ */
5
+
6
+ export type GenerationCapability =
7
+ | "text-to-image"
8
+ | "image-to-image"
9
+ | "text-to-video"
10
+ | "image-to-video"
11
+ | "text-to-voice"
12
+ | "voice-to-text";
13
+
14
+ export type GenerationStatus =
15
+ | "idle"
16
+ | "preparing"
17
+ | "moderating"
18
+ | "submitting"
19
+ | "generating"
20
+ | "polling"
21
+ | "finalizing"
22
+ | "completed"
23
+ | "failed";
24
+
25
+ export interface GenerationMetadata {
26
+ model?: string;
27
+ provider?: string;
28
+ capability?: GenerationCapability;
29
+ creditCost?: number;
30
+ startTime?: number;
31
+ endTime?: number;
32
+ duration?: number;
33
+ }
34
+
35
+ export interface GenerationResult<T = unknown> {
36
+ success: boolean;
37
+ data?: T;
38
+ error?: string;
39
+ requestId?: string;
40
+ metadata?: GenerationMetadata;
41
+ }
42
+
43
+ export interface GenerationProgress {
44
+ stage: GenerationStatus;
45
+ progress: number;
46
+ message?: string;
47
+ eta?: number;
48
+ }
49
+
50
+ export interface GenerationRequest {
51
+ model: string;
52
+ input: Record<string, unknown>;
53
+ userId?: string;
54
+ capability?: GenerationCapability;
55
+ onProgress?: (progress: GenerationProgress) => void;
56
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Domain Entities
3
+ * Core type definitions
4
+ */
5
+
6
+ export * from "./error.types";
7
+ export * from "./generation.types";
8
+ export * from "./polling.types";
9
+ export * from "./progress.types";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Polling Types
3
+ * Configuration for job polling
4
+ */
5
+
6
+ export interface PollingConfig {
7
+ maxAttempts: number;
8
+ initialIntervalMs: number;
9
+ maxIntervalMs: number;
10
+ backoffMultiplier: number;
11
+ maxConsecutiveErrors: number;
12
+ }
13
+
14
+ export const DEFAULT_POLLING_CONFIG: PollingConfig = {
15
+ maxAttempts: 60,
16
+ initialIntervalMs: 1000,
17
+ maxIntervalMs: 3000,
18
+ backoffMultiplier: 1.2,
19
+ maxConsecutiveErrors: 5,
20
+ };
21
+
22
+ export interface PollingState {
23
+ attempt: number;
24
+ lastProgress: number;
25
+ consecutiveErrors: number;
26
+ startTime: number;
27
+ }
28
+
29
+ export interface PollingOptions {
30
+ model: string;
31
+ requestId: string;
32
+ statusUrl?: string;
33
+ responseUrl?: string;
34
+ userId?: string;
35
+ jobType?: string;
36
+ config?: Partial<PollingConfig>;
37
+ onProgress?: (progress: number) => void;
38
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Progress Types
3
+ * Progress tracking for generation stages
4
+ */
5
+
6
+ import type { GenerationStatus } from "./generation.types";
7
+
8
+ export interface ProgressStageConfig {
9
+ status: GenerationStatus;
10
+ minProgress: number;
11
+ maxProgress: number;
12
+ weight: number;
13
+ }
14
+
15
+ export const DEFAULT_PROGRESS_STAGES: ProgressStageConfig[] = [
16
+ { status: "preparing", minProgress: 0, maxProgress: 5, weight: 1 },
17
+ { status: "moderating", minProgress: 5, maxProgress: 15, weight: 1 },
18
+ { status: "submitting", minProgress: 15, maxProgress: 25, weight: 1 },
19
+ { status: "generating", minProgress: 25, maxProgress: 85, weight: 6 },
20
+ { status: "finalizing", minProgress: 85, maxProgress: 95, weight: 1 },
21
+ { status: "completed", minProgress: 95, maxProgress: 100, weight: 1 },
22
+ ];
23
+
24
+ export interface ProgressConfig {
25
+ stages: ProgressStageConfig[];
26
+ estimatedDurations?: Record<string, number>;
27
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * AI Provider Interface
3
+ * Provider-agnostic interface for AI generation services
4
+ * Implementations: FAL, Gemini, etc.
5
+ */
6
+
7
+ export interface AIProviderConfig {
8
+ apiKey: string;
9
+ maxRetries?: number;
10
+ baseDelay?: number;
11
+ maxDelay?: number;
12
+ defaultTimeoutMs?: number;
13
+ }
14
+
15
+ export interface JobSubmission {
16
+ requestId: string;
17
+ statusUrl?: string;
18
+ responseUrl?: string;
19
+ }
20
+
21
+ export interface JobStatus {
22
+ status: AIJobStatusType;
23
+ logs?: AILogEntry[];
24
+ queuePosition?: number;
25
+ eta?: number;
26
+ }
27
+
28
+ export type AIJobStatusType =
29
+ | "IN_QUEUE"
30
+ | "IN_PROGRESS"
31
+ | "COMPLETED"
32
+ | "FAILED";
33
+
34
+ export interface AILogEntry {
35
+ message: string;
36
+ level: "info" | "warn" | "error";
37
+ timestamp?: string;
38
+ }
39
+
40
+ export interface SubscribeOptions<T = unknown> {
41
+ timeoutMs?: number;
42
+ onQueueUpdate?: (status: JobStatus) => void;
43
+ onProgress?: (progress: number) => void;
44
+ onResult?: (result: T) => void;
45
+ }
46
+
47
+ /**
48
+ * AI Provider Interface
49
+ * All AI providers must implement this interface
50
+ */
51
+ export interface IAIProvider {
52
+ readonly providerId: string;
53
+ readonly providerName: string;
54
+
55
+ initialize(config: AIProviderConfig): void;
56
+ isInitialized(): boolean;
57
+
58
+ submitJob(
59
+ model: string,
60
+ input: Record<string, unknown>,
61
+ ): Promise<JobSubmission>;
62
+
63
+ getJobStatus(model: string, requestId: string): Promise<JobStatus>;
64
+
65
+ getJobResult<T = unknown>(model: string, requestId: string): Promise<T>;
66
+
67
+ subscribe<T = unknown>(
68
+ model: string,
69
+ input: Record<string, unknown>,
70
+ options?: SubscribeOptions<T>,
71
+ ): Promise<T>;
72
+
73
+ run<T = unknown>(model: string, input: Record<string, unknown>): Promise<T>;
74
+
75
+ reset(): void;
76
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Domain Interfaces
3
+ * Provider-agnostic contracts
4
+ */
5
+
6
+ export * from "./ai-provider.interface";
package/src/index.ts ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @umituz/react-native-ai-generation-content
3
+ * Provider-agnostic AI generation orchestration
4
+ *
5
+ * Usage:
6
+ * import {
7
+ * providerRegistry,
8
+ * generationOrchestrator,
9
+ * useGeneration,
10
+ * } from '@umituz/react-native-ai-generation-content';
11
+ */
12
+
13
+ // =============================================================================
14
+ // DOMAIN LAYER - Types & Interfaces
15
+ // =============================================================================
16
+
17
+ export type {
18
+ AIProviderConfig,
19
+ IAIProvider,
20
+ JobSubmission,
21
+ JobStatus,
22
+ AIJobStatusType,
23
+ AILogEntry,
24
+ SubscribeOptions,
25
+ } from "./domain/interfaces";
26
+
27
+ export {
28
+ AIErrorType,
29
+ } from "./domain/entities";
30
+
31
+ export type {
32
+ AIErrorInfo,
33
+ AIErrorMessages,
34
+ GenerationCapability,
35
+ GenerationStatus,
36
+ GenerationMetadata,
37
+ GenerationResult,
38
+ GenerationProgress,
39
+ GenerationRequest,
40
+ PollingConfig,
41
+ PollingState,
42
+ PollingOptions,
43
+ ProgressStageConfig,
44
+ ProgressConfig,
45
+ } from "./domain/entities";
46
+
47
+ export { DEFAULT_POLLING_CONFIG, DEFAULT_PROGRESS_STAGES } from "./domain/entities";
48
+
49
+ // =============================================================================
50
+ // INFRASTRUCTURE LAYER - Services
51
+ // =============================================================================
52
+
53
+ export {
54
+ providerRegistry,
55
+ generationOrchestrator,
56
+ } from "./infrastructure/services";
57
+
58
+ export type { OrchestratorConfig } from "./infrastructure/services";
59
+
60
+ // =============================================================================
61
+ // INFRASTRUCTURE LAYER - Utils
62
+ // =============================================================================
63
+
64
+ export {
65
+ classifyError,
66
+ isTransientError,
67
+ isPermanentError,
68
+ calculatePollingInterval,
69
+ createPollingDelay,
70
+ getProgressForStatus,
71
+ interpolateProgress,
72
+ createProgressTracker,
73
+ } from "./infrastructure/utils";
74
+
75
+ export type { IntervalOptions, ProgressOptions } from "./infrastructure/utils";
76
+
77
+ // =============================================================================
78
+ // PRESENTATION LAYER - Hooks
79
+ // =============================================================================
80
+
81
+ export { useGeneration } from "./presentation/hooks";
82
+
83
+ export type {
84
+ UseGenerationOptions,
85
+ UseGenerationReturn,
86
+ } from "./presentation/hooks";
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Generation Orchestrator Service
3
+ * Provider-agnostic AI generation workflow
4
+ */
5
+
6
+ import type {
7
+ GenerationRequest,
8
+ GenerationResult,
9
+ GenerationProgress,
10
+ PollingConfig,
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";
21
+
22
+ declare const __DEV__: boolean;
23
+
24
+ export interface OrchestratorConfig {
25
+ polling?: Partial<PollingConfig>;
26
+ onStatusUpdate?: (requestId: string, status: string) => Promise<void>;
27
+ }
28
+
29
+ class GenerationOrchestratorService {
30
+ private config: OrchestratorConfig = {};
31
+
32
+ configure(config: OrchestratorConfig): void {
33
+ this.config = { ...this.config, ...config };
34
+ }
35
+
36
+ async generate<T = unknown>(
37
+ request: GenerationRequest,
38
+ ): Promise<GenerationResult<T>> {
39
+ const provider = this.getProvider();
40
+ const progressTracker = createProgressTracker();
41
+ const startTime = Date.now();
42
+
43
+ const updateProgress = (
44
+ stage: GenerationProgress["stage"],
45
+ subProgress = 0,
46
+ ) => {
47
+ const progress = progressTracker.setStatus(stage);
48
+ request.onProgress?.({
49
+ stage,
50
+ progress: progress + subProgress,
51
+ });
52
+ };
53
+
54
+ try {
55
+ updateProgress("preparing");
56
+
57
+ const submission = await provider.submitJob(request.model, request.input);
58
+
59
+ updateProgress("submitting");
60
+
61
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
62
+ // eslint-disable-next-line no-console
63
+ console.log("[Orchestrator] Job submitted:", submission.requestId);
64
+ }
65
+
66
+ updateProgress("generating");
67
+
68
+ const result = await this.pollForResult<T>(
69
+ provider,
70
+ request.model,
71
+ submission.requestId,
72
+ request.onProgress,
73
+ );
74
+
75
+ updateProgress("completed");
76
+
77
+ return {
78
+ success: true,
79
+ data: result,
80
+ requestId: submission.requestId,
81
+ metadata: {
82
+ model: request.model,
83
+ provider: provider.providerId,
84
+ capability: request.capability,
85
+ startTime,
86
+ endTime: Date.now(),
87
+ duration: Date.now() - startTime,
88
+ },
89
+ };
90
+ } catch (error) {
91
+ const errorInfo = classifyError(error);
92
+
93
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
94
+ // eslint-disable-next-line no-console
95
+ console.error("[Orchestrator] Generation failed:", errorInfo);
96
+ }
97
+
98
+ return {
99
+ success: false,
100
+ error: errorInfo.messageKey,
101
+ metadata: {
102
+ model: request.model,
103
+ provider: provider.providerId,
104
+ capability: request.capability,
105
+ startTime,
106
+ endTime: Date.now(),
107
+ duration: Date.now() - startTime,
108
+ },
109
+ };
110
+ }
111
+ }
112
+
113
+ private async pollForResult<T>(
114
+ provider: IAIProvider,
115
+ model: string,
116
+ requestId: string,
117
+ onProgress?: (progress: GenerationProgress) => void,
118
+ ): Promise<T> {
119
+ const config = {
120
+ ...DEFAULT_POLLING_CONFIG,
121
+ ...this.config.polling,
122
+ };
123
+
124
+ let consecutiveErrors = 0;
125
+
126
+ for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
127
+ await createPollingDelay(attempt, config);
128
+
129
+ try {
130
+ const status = await provider.getJobStatus(model, requestId);
131
+
132
+ consecutiveErrors = 0;
133
+
134
+ this.updateProgressFromStatus(status, attempt, config, onProgress);
135
+
136
+ if (status.status === "COMPLETED") {
137
+ return provider.getJobResult<T>(model, requestId);
138
+ }
139
+
140
+ if (status.status === "FAILED") {
141
+ throw new Error("Job failed on provider");
142
+ }
143
+
144
+ await this.config.onStatusUpdate?.(requestId, status.status);
145
+ } catch (error) {
146
+ if (isTransientError(error)) {
147
+ consecutiveErrors++;
148
+
149
+ if (consecutiveErrors >= config.maxConsecutiveErrors) {
150
+ throw error;
151
+ }
152
+
153
+ continue;
154
+ }
155
+
156
+ throw error;
157
+ }
158
+ }
159
+
160
+ throw new Error(
161
+ `Polling timeout after ${config.maxAttempts} attempts`,
162
+ );
163
+ }
164
+
165
+ private updateProgressFromStatus(
166
+ status: JobStatus,
167
+ attempt: number,
168
+ config: PollingConfig,
169
+ onProgress?: (progress: GenerationProgress) => void,
170
+ ): void {
171
+ const baseProgress = 25;
172
+ const maxProgress = 85;
173
+ const range = maxProgress - baseProgress;
174
+
175
+ let progress: number;
176
+
177
+ if (status.status === "IN_QUEUE") {
178
+ progress = baseProgress + range * 0.2;
179
+ } else if (status.status === "IN_PROGRESS") {
180
+ const ratio = Math.min(attempt / (config.maxAttempts * 0.7), 1);
181
+ progress = baseProgress + range * (0.2 + 0.6 * ratio);
182
+ } else {
183
+ progress = baseProgress;
184
+ }
185
+
186
+ onProgress?.({
187
+ stage: "generating",
188
+ progress: Math.round(progress),
189
+ eta: status.eta,
190
+ });
191
+ }
192
+
193
+ private getProvider(): IAIProvider {
194
+ const provider = providerRegistry.getActiveProvider();
195
+
196
+ if (!provider) {
197
+ throw new Error(
198
+ "No active AI provider. Register and set a provider first.",
199
+ );
200
+ }
201
+
202
+ if (!provider.isInitialized()) {
203
+ throw new Error(
204
+ `Provider ${provider.providerId} is not initialized.`,
205
+ );
206
+ }
207
+
208
+ return provider;
209
+ }
210
+ }
211
+
212
+ export const generationOrchestrator = new GenerationOrchestratorService();
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Infrastructure Services
3
+ */
4
+
5
+ export { providerRegistry } from "./provider-registry.service";
6
+ export { generationOrchestrator } from "./generation-orchestrator.service";
7
+ export type { OrchestratorConfig } from "./generation-orchestrator.service";
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Provider Registry Service
3
+ * Manages AI provider registration and selection
4
+ */
5
+
6
+ import type { IAIProvider } from "../../domain/interfaces";
7
+
8
+ declare const __DEV__: boolean;
9
+
10
+ class ProviderRegistry {
11
+ private providers: Map<string, IAIProvider> = new Map();
12
+ private activeProviderId: string | null = null;
13
+
14
+ register(provider: IAIProvider): void {
15
+ if (this.providers.has(provider.providerId)) {
16
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
17
+ // eslint-disable-next-line no-console
18
+ console.warn(
19
+ `[ProviderRegistry] Provider ${provider.providerId} already registered`,
20
+ );
21
+ }
22
+ return;
23
+ }
24
+
25
+ this.providers.set(provider.providerId, provider);
26
+
27
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
28
+ // eslint-disable-next-line no-console
29
+ console.log(
30
+ `[ProviderRegistry] Registered provider: ${provider.providerId}`,
31
+ );
32
+ }
33
+ }
34
+
35
+ unregister(providerId: string): void {
36
+ if (this.activeProviderId === providerId) {
37
+ this.activeProviderId = null;
38
+ }
39
+ this.providers.delete(providerId);
40
+ }
41
+
42
+ setActiveProvider(providerId: string): void {
43
+ if (!this.providers.has(providerId)) {
44
+ throw new Error(`Provider ${providerId} is not registered`);
45
+ }
46
+ this.activeProviderId = providerId;
47
+
48
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
49
+ // eslint-disable-next-line no-console
50
+ console.log(`[ProviderRegistry] Active provider set to: ${providerId}`);
51
+ }
52
+ }
53
+
54
+ getActiveProvider(): IAIProvider | null {
55
+ if (!this.activeProviderId) {
56
+ return null;
57
+ }
58
+ return this.providers.get(this.activeProviderId) ?? null;
59
+ }
60
+
61
+ getProvider(providerId: string): IAIProvider | null {
62
+ return this.providers.get(providerId) ?? null;
63
+ }
64
+
65
+ getActiveProviderId(): string | null {
66
+ return this.activeProviderId;
67
+ }
68
+
69
+ listProviders(): Array<{ id: string; name: string; initialized: boolean }> {
70
+ return Array.from(this.providers.values()).map((provider) => ({
71
+ id: provider.providerId,
72
+ name: provider.providerName,
73
+ initialized: provider.isInitialized(),
74
+ }));
75
+ }
76
+
77
+ hasProvider(providerId: string): boolean {
78
+ return this.providers.has(providerId);
79
+ }
80
+
81
+ clear(): void {
82
+ this.providers.clear();
83
+ this.activeProviderId = null;
84
+ }
85
+ }
86
+
87
+ export const providerRegistry = new ProviderRegistry();
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Error Classifier Utility
3
+ * Classifies errors for retry and user feedback decisions
4
+ */
5
+
6
+ import { AIErrorType, type AIErrorInfo } from "../../domain/entities";
7
+
8
+ const NETWORK_ERROR_PATTERNS = [
9
+ "network",
10
+ "timeout",
11
+ "socket",
12
+ "econnrefused",
13
+ "enotfound",
14
+ "fetch failed",
15
+ "connection",
16
+ ];
17
+
18
+ const RATE_LIMIT_PATTERNS = ["rate limit", "too many requests", "429"];
19
+
20
+ const AUTH_ERROR_PATTERNS = [
21
+ "unauthorized",
22
+ "authentication",
23
+ "invalid api key",
24
+ "401",
25
+ "403",
26
+ ];
27
+
28
+ const CONTENT_POLICY_PATTERNS = [
29
+ "content policy",
30
+ "safety",
31
+ "moderation",
32
+ "inappropriate",
33
+ "blocked",
34
+ ];
35
+
36
+ const SERVER_ERROR_PATTERNS = [
37
+ "internal server error",
38
+ "500",
39
+ "502",
40
+ "503",
41
+ "504",
42
+ "service unavailable",
43
+ ];
44
+
45
+ function matchesPatterns(message: string, patterns: string[]): boolean {
46
+ const lowerMessage = message.toLowerCase();
47
+ return patterns.some((pattern) => lowerMessage.includes(pattern));
48
+ }
49
+
50
+ function getStatusCode(error: unknown): number | undefined {
51
+ if (error && typeof error === "object") {
52
+ const err = error as Record<string, unknown>;
53
+ if (typeof err.status === "number") return err.status;
54
+ if (typeof err.statusCode === "number") return err.statusCode;
55
+ if (err.response && typeof err.response === "object") {
56
+ const resp = err.response as Record<string, unknown>;
57
+ if (typeof resp.status === "number") return resp.status;
58
+ }
59
+ }
60
+ return undefined;
61
+ }
62
+
63
+ export function classifyError(error: unknown): AIErrorInfo {
64
+ const message = error instanceof Error ? error.message : String(error);
65
+ const statusCode = getStatusCode(error);
66
+
67
+ if (statusCode === 429 || matchesPatterns(message, RATE_LIMIT_PATTERNS)) {
68
+ return {
69
+ type: AIErrorType.RATE_LIMIT,
70
+ messageKey: "error.rateLimit",
71
+ retryable: true,
72
+ originalError: error,
73
+ statusCode,
74
+ };
75
+ }
76
+
77
+ if (
78
+ statusCode === 401 ||
79
+ statusCode === 403 ||
80
+ matchesPatterns(message, AUTH_ERROR_PATTERNS)
81
+ ) {
82
+ return {
83
+ type: AIErrorType.AUTHENTICATION,
84
+ messageKey: "error.authentication",
85
+ retryable: false,
86
+ originalError: error,
87
+ statusCode,
88
+ };
89
+ }
90
+
91
+ if (matchesPatterns(message, CONTENT_POLICY_PATTERNS)) {
92
+ return {
93
+ type: AIErrorType.CONTENT_POLICY,
94
+ messageKey: "error.contentPolicy",
95
+ retryable: false,
96
+ originalError: error,
97
+ statusCode,
98
+ };
99
+ }
100
+
101
+ if (matchesPatterns(message, NETWORK_ERROR_PATTERNS)) {
102
+ return {
103
+ type: AIErrorType.NETWORK,
104
+ messageKey: "error.network",
105
+ retryable: true,
106
+ originalError: error,
107
+ statusCode,
108
+ };
109
+ }
110
+
111
+ if (
112
+ (statusCode && statusCode >= 500) ||
113
+ matchesPatterns(message, SERVER_ERROR_PATTERNS)
114
+ ) {
115
+ return {
116
+ type: AIErrorType.SERVER,
117
+ messageKey: "error.server",
118
+ retryable: true,
119
+ originalError: error,
120
+ statusCode,
121
+ };
122
+ }
123
+
124
+ if (message.toLowerCase().includes("timeout")) {
125
+ return {
126
+ type: AIErrorType.TIMEOUT,
127
+ messageKey: "error.timeout",
128
+ retryable: true,
129
+ originalError: error,
130
+ statusCode,
131
+ };
132
+ }
133
+
134
+ return {
135
+ type: AIErrorType.UNKNOWN,
136
+ messageKey: "error.unknown",
137
+ retryable: false,
138
+ originalError: error,
139
+ statusCode,
140
+ };
141
+ }
142
+
143
+ export function isTransientError(error: unknown): boolean {
144
+ const info = classifyError(error);
145
+ return info.retryable;
146
+ }
147
+
148
+ export function isPermanentError(error: unknown): boolean {
149
+ return !isTransientError(error);
150
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Infrastructure Utils
3
+ */
4
+
5
+ export * from "./error-classifier.util";
6
+ export * from "./polling-interval.util";
7
+ export * from "./progress-calculator.util";
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Polling Interval Calculator
3
+ * Calculates polling intervals with exponential backoff
4
+ */
5
+
6
+ import {
7
+ DEFAULT_POLLING_CONFIG,
8
+ type PollingConfig,
9
+ } from "../../domain/entities";
10
+
11
+ export interface IntervalOptions {
12
+ attempt: number;
13
+ config?: Partial<PollingConfig>;
14
+ }
15
+
16
+ export function calculatePollingInterval(options: IntervalOptions): number {
17
+ const { attempt, config } = options;
18
+ const {
19
+ initialIntervalMs = DEFAULT_POLLING_CONFIG.initialIntervalMs,
20
+ maxIntervalMs = DEFAULT_POLLING_CONFIG.maxIntervalMs,
21
+ backoffMultiplier = DEFAULT_POLLING_CONFIG.backoffMultiplier,
22
+ } = config ?? {};
23
+
24
+ if (attempt === 0) {
25
+ return 0;
26
+ }
27
+
28
+ const interval = initialIntervalMs * Math.pow(backoffMultiplier, attempt - 1);
29
+ return Math.min(interval, maxIntervalMs);
30
+ }
31
+
32
+ export function createPollingDelay(
33
+ attempt: number,
34
+ config?: Partial<PollingConfig>,
35
+ ): Promise<void> {
36
+ const interval = calculatePollingInterval({ attempt, config });
37
+
38
+ if (interval === 0) {
39
+ return Promise.resolve();
40
+ }
41
+
42
+ return new Promise((resolve) => setTimeout(resolve, interval));
43
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Progress Calculator Utility
3
+ * Calculates progress based on generation stage
4
+ */
5
+
6
+ import type { GenerationStatus } from "../../domain/entities";
7
+ import {
8
+ DEFAULT_PROGRESS_STAGES,
9
+ type ProgressStageConfig,
10
+ } from "../../domain/entities";
11
+
12
+ export interface ProgressOptions {
13
+ status: GenerationStatus;
14
+ stages?: ProgressStageConfig[];
15
+ subProgress?: number;
16
+ }
17
+
18
+ export function getProgressForStatus(options: ProgressOptions): number {
19
+ const { status, stages = DEFAULT_PROGRESS_STAGES, subProgress = 0 } = options;
20
+
21
+ const stage = stages.find((s) => s.status === status);
22
+
23
+ if (!stage) {
24
+ return 0;
25
+ }
26
+
27
+ const range = stage.maxProgress - stage.minProgress;
28
+ const adjustedProgress = stage.minProgress + range * (subProgress / 100);
29
+
30
+ return Math.round(adjustedProgress);
31
+ }
32
+
33
+ export function interpolateProgress(
34
+ startTime: number,
35
+ estimatedDurationMs: number,
36
+ minProgress: number,
37
+ maxProgress: number,
38
+ ): number {
39
+ const elapsed = Date.now() - startTime;
40
+ const ratio = Math.min(elapsed / estimatedDurationMs, 1);
41
+
42
+ const eased = 1 - Math.pow(1 - ratio, 2);
43
+
44
+ const progress = minProgress + (maxProgress - minProgress) * eased;
45
+ return Math.round(progress);
46
+ }
47
+
48
+ export function createProgressTracker(stages?: ProgressStageConfig[]) {
49
+ const effectiveStages = stages ?? DEFAULT_PROGRESS_STAGES;
50
+ let currentStatus: GenerationStatus = "idle";
51
+ let stageStartTime = Date.now();
52
+
53
+ return {
54
+ setStatus(status: GenerationStatus): number {
55
+ currentStatus = status;
56
+ stageStartTime = Date.now();
57
+ return getProgressForStatus({ status, stages: effectiveStages });
58
+ },
59
+
60
+ getProgress(subProgress = 0): number {
61
+ return getProgressForStatus({
62
+ status: currentStatus,
63
+ stages: effectiveStages,
64
+ subProgress,
65
+ });
66
+ },
67
+
68
+ getCurrentStatus(): GenerationStatus {
69
+ return currentStatus;
70
+ },
71
+
72
+ getElapsedTime(): number {
73
+ return Date.now() - stageStartTime;
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Presentation Hooks
3
+ */
4
+
5
+ export { useGeneration } from "./use-generation";
6
+ export type {
7
+ UseGenerationOptions,
8
+ UseGenerationReturn,
9
+ } from "./use-generation";
@@ -0,0 +1,112 @@
1
+ /**
2
+ * useGeneration Hook
3
+ * React hook for AI generation with progress tracking
4
+ */
5
+
6
+ import { useState, useCallback, useRef } from "react";
7
+ import type {
8
+ GenerationRequest,
9
+ GenerationResult,
10
+ GenerationProgress,
11
+ GenerationCapability,
12
+ } from "../../domain/entities";
13
+ import { generationOrchestrator } from "../../infrastructure/services";
14
+
15
+ export interface UseGenerationOptions {
16
+ model: string;
17
+ capability?: GenerationCapability;
18
+ onSuccess?: <T>(result: GenerationResult<T>) => void;
19
+ onError?: (error: string) => void;
20
+ onProgress?: (progress: GenerationProgress) => void;
21
+ }
22
+
23
+ export interface UseGenerationReturn<T = unknown> {
24
+ generate: (input: Record<string, unknown>, userId?: string) => Promise<void>;
25
+ result: GenerationResult<T> | null;
26
+ progress: GenerationProgress | null;
27
+ isGenerating: boolean;
28
+ error: string | null;
29
+ reset: () => void;
30
+ }
31
+
32
+ export function useGeneration<T = unknown>(
33
+ options: UseGenerationOptions,
34
+ ): UseGenerationReturn<T> {
35
+ const [result, setResult] = useState<GenerationResult<T> | null>(null);
36
+ const [progress, setProgress] = useState<GenerationProgress | null>(null);
37
+ const [isGenerating, setIsGenerating] = useState(false);
38
+ const [error, setError] = useState<string | null>(null);
39
+
40
+ const abortRef = useRef(false);
41
+
42
+ const handleProgress = useCallback(
43
+ (prog: GenerationProgress) => {
44
+ if (abortRef.current) return;
45
+ setProgress(prog);
46
+ options.onProgress?.(prog);
47
+ },
48
+ [options],
49
+ );
50
+
51
+ const generate = useCallback(
52
+ async (input: Record<string, unknown>, userId?: string) => {
53
+ abortRef.current = false;
54
+ setIsGenerating(true);
55
+ setError(null);
56
+ setResult(null);
57
+ setProgress({ stage: "preparing", progress: 0 });
58
+
59
+ try {
60
+ const request: GenerationRequest = {
61
+ model: options.model,
62
+ input,
63
+ userId,
64
+ capability: options.capability,
65
+ onProgress: handleProgress,
66
+ };
67
+
68
+ const genResult = await generationOrchestrator.generate<T>(request);
69
+
70
+ if (abortRef.current) return;
71
+
72
+ setResult(genResult);
73
+
74
+ if (genResult.success) {
75
+ options.onSuccess?.(genResult);
76
+ } else if (genResult.error) {
77
+ setError(genResult.error);
78
+ options.onError?.(genResult.error);
79
+ }
80
+ } catch (err) {
81
+ if (abortRef.current) return;
82
+
83
+ const errorMessage =
84
+ err instanceof Error ? err.message : "error.unknown";
85
+ setError(errorMessage);
86
+ options.onError?.(errorMessage);
87
+ } finally {
88
+ if (!abortRef.current) {
89
+ setIsGenerating(false);
90
+ }
91
+ }
92
+ },
93
+ [options, handleProgress],
94
+ );
95
+
96
+ const reset = useCallback(() => {
97
+ abortRef.current = true;
98
+ setResult(null);
99
+ setProgress(null);
100
+ setIsGenerating(false);
101
+ setError(null);
102
+ }, []);
103
+
104
+ return {
105
+ generate,
106
+ result,
107
+ progress,
108
+ isGenerating,
109
+ error,
110
+ reset,
111
+ };
112
+ }