@umituz/react-native-ai-groq-provider 1.0.0
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/LICENSE +21 -0
- package/README.md +266 -0
- package/package.json +96 -0
- package/src/domain/entities/error.types.ts +54 -0
- package/src/domain/entities/groq.types.ts +235 -0
- package/src/domain/entities/index.ts +7 -0
- package/src/domain/entities/models.ts +212 -0
- package/src/index.ts +113 -0
- package/src/infrastructure/services/ChatSession.ts +265 -0
- package/src/infrastructure/services/GroqClient.ts +280 -0
- package/src/infrastructure/services/Streaming.ts +114 -0
- package/src/infrastructure/services/StructuredText.ts +171 -0
- package/src/infrastructure/services/TextGeneration.ts +94 -0
- package/src/infrastructure/services/index.ts +19 -0
- package/src/infrastructure/telemetry/TelemetryHooks.ts +79 -0
- package/src/infrastructure/telemetry/index.ts +5 -0
- package/src/infrastructure/utils/async/execute-state.util.ts +76 -0
- package/src/infrastructure/utils/async/index.ts +6 -0
- package/src/infrastructure/utils/content-mapper.util.ts +86 -0
- package/src/infrastructure/utils/error-mapper.util.ts +77 -0
- package/src/infrastructure/utils/index.ts +7 -0
- package/src/presentation/hooks/useGroq.ts +224 -0
- package/src/presentation/hooks/useOperationManager.ts +110 -0
- package/src/providers/ConfigBuilder.ts +159 -0
- package/src/providers/ProviderFactory.ts +98 -0
- package/src/providers/index.ts +16 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry Hooks
|
|
3
|
+
* Simple telemetry tracking for Groq operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
type TelemetryEvent = {
|
|
7
|
+
name: string;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
data?: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
class Telemetry {
|
|
13
|
+
private events: TelemetryEvent[] = [];
|
|
14
|
+
private enabled = __DEV__;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Log a telemetry event
|
|
18
|
+
*/
|
|
19
|
+
log(name: string, data?: Record<string, unknown>): void {
|
|
20
|
+
if (!this.enabled) return;
|
|
21
|
+
|
|
22
|
+
const event: TelemetryEvent = {
|
|
23
|
+
name,
|
|
24
|
+
timestamp: Date.now(),
|
|
25
|
+
data,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
this.events.push(event);
|
|
29
|
+
|
|
30
|
+
if (__DEV__) {
|
|
31
|
+
console.log(`[Groq Telemetry] ${name}`, data);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get all events
|
|
37
|
+
*/
|
|
38
|
+
getEvents(): TelemetryEvent[] {
|
|
39
|
+
return [...this.events];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Clear all events
|
|
44
|
+
*/
|
|
45
|
+
clear(): void {
|
|
46
|
+
this.events = [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Enable/disable telemetry
|
|
51
|
+
*/
|
|
52
|
+
setEnabled(enabled: boolean): void {
|
|
53
|
+
this.enabled = enabled;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if telemetry is enabled
|
|
58
|
+
*/
|
|
59
|
+
isEnabled(): boolean {
|
|
60
|
+
return this.enabled;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Singleton instance
|
|
66
|
+
*/
|
|
67
|
+
export const telemetry = new Telemetry();
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Hook to use telemetry in components
|
|
71
|
+
*/
|
|
72
|
+
export function useTelemetry() {
|
|
73
|
+
return {
|
|
74
|
+
log: telemetry.log.bind(telemetry),
|
|
75
|
+
getEvents: telemetry.getEvents.bind(telemetry),
|
|
76
|
+
clear: telemetry.clear.bind(telemetry),
|
|
77
|
+
isEnabled: telemetry.isEnabled.bind(telemetry),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async State Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* State setters for async operations
|
|
7
|
+
*/
|
|
8
|
+
export interface AsyncStateSetters<T> {
|
|
9
|
+
setLoading: (loading: boolean) => void;
|
|
10
|
+
setError: (error: string | null) => void;
|
|
11
|
+
setData?: (data: T | null) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Callbacks for async operations
|
|
16
|
+
*/
|
|
17
|
+
export interface AsyncCallbacks<T> {
|
|
18
|
+
onSuccess?: (data: T) => void;
|
|
19
|
+
onError?: (error: Error) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Execute async function with state management
|
|
24
|
+
*/
|
|
25
|
+
export async function executeWithState<T>(
|
|
26
|
+
setters: AsyncStateSetters<T>,
|
|
27
|
+
asyncFn: () => Promise<T>,
|
|
28
|
+
callbacks?: AsyncCallbacks<T>
|
|
29
|
+
): Promise<T> {
|
|
30
|
+
const { setLoading, setError, setData } = setters;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
setLoading(true);
|
|
34
|
+
setError(null);
|
|
35
|
+
|
|
36
|
+
const result = await asyncFn();
|
|
37
|
+
|
|
38
|
+
setData?.(result);
|
|
39
|
+
callbacks?.onSuccess?.(result);
|
|
40
|
+
|
|
41
|
+
return result;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
44
|
+
setError(errorMessage);
|
|
45
|
+
callbacks?.onError?.(error as Error);
|
|
46
|
+
throw error;
|
|
47
|
+
} finally {
|
|
48
|
+
setLoading(false);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Execute async function with retry logic
|
|
54
|
+
*/
|
|
55
|
+
export async function executeWithRetry<T>(
|
|
56
|
+
asyncFn: () => Promise<T>,
|
|
57
|
+
maxRetries: number = 3,
|
|
58
|
+
delayMs: number = 1000
|
|
59
|
+
): Promise<T> {
|
|
60
|
+
let lastError: Error | null = null;
|
|
61
|
+
|
|
62
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
63
|
+
try {
|
|
64
|
+
return await asyncFn();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
lastError = error as Error;
|
|
67
|
+
|
|
68
|
+
if (attempt < maxRetries - 1) {
|
|
69
|
+
// Wait before retrying with exponential backoff
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs * Math.pow(2, attempt)));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw lastError;
|
|
76
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Mapper Utility
|
|
3
|
+
* Utilities for working with message content
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { GroqMessage } from "../../domain/entities";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a user message
|
|
10
|
+
*/
|
|
11
|
+
export function createUserMessage(content: string): GroqMessage {
|
|
12
|
+
return {
|
|
13
|
+
role: "user",
|
|
14
|
+
content,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create an assistant message
|
|
20
|
+
*/
|
|
21
|
+
export function createAssistantMessage(content: string): GroqMessage {
|
|
22
|
+
return {
|
|
23
|
+
role: "assistant",
|
|
24
|
+
content,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a system message
|
|
30
|
+
*/
|
|
31
|
+
export function createSystemMessage(content: string): GroqMessage {
|
|
32
|
+
return {
|
|
33
|
+
role: "system",
|
|
34
|
+
content,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a text message (defaults to user role)
|
|
40
|
+
*/
|
|
41
|
+
export function createTextMessage(content: string, role: "user" | "assistant" | "system" = "user"): GroqMessage {
|
|
42
|
+
return {
|
|
43
|
+
role,
|
|
44
|
+
content,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Convert a simple prompt string to an array of messages
|
|
50
|
+
*/
|
|
51
|
+
export function promptToMessages(prompt: string, systemPrompt?: string): GroqMessage[] {
|
|
52
|
+
const messages: GroqMessage[] = [];
|
|
53
|
+
|
|
54
|
+
if (systemPrompt) {
|
|
55
|
+
messages.push({
|
|
56
|
+
role: "system",
|
|
57
|
+
content: systemPrompt,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
messages.push({
|
|
62
|
+
role: "user",
|
|
63
|
+
content: prompt,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return messages;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Extract text content from an array of messages
|
|
71
|
+
*/
|
|
72
|
+
export function extractTextFromMessages(messages: GroqMessage[]): string {
|
|
73
|
+
return messages.map((m) => `[${m.role}]: ${m.content}`).join("\n\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Format messages for display in UI
|
|
78
|
+
*/
|
|
79
|
+
export function formatMessagesForDisplay(messages: GroqMessage[]): string {
|
|
80
|
+
return messages
|
|
81
|
+
.map((m) => {
|
|
82
|
+
const role = m.role === "system" ? "System" : m.role === "user" ? "You" : "Assistant";
|
|
83
|
+
return `${role}:\n${m.content}`;
|
|
84
|
+
})
|
|
85
|
+
.join("\n\n---\n\n");
|
|
86
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Mapper Utility
|
|
3
|
+
* Utilities for handling and mapping errors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { GroqError, GroqErrorType } from "../../domain/entities/error.types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get user-friendly error message
|
|
10
|
+
*/
|
|
11
|
+
export function getUserFriendlyError(error: unknown): string {
|
|
12
|
+
if (error instanceof GroqError) {
|
|
13
|
+
switch (error.type) {
|
|
14
|
+
case GroqErrorType.INVALID_API_KEY:
|
|
15
|
+
return "Invalid API key. Please check your Groq API credentials.";
|
|
16
|
+
case GroqErrorType.MISSING_CONFIG:
|
|
17
|
+
return "Configuration missing. Please initialize the Groq provider.";
|
|
18
|
+
case GroqErrorType.NETWORK_ERROR:
|
|
19
|
+
return "Network error. Please check your internet connection.";
|
|
20
|
+
case GroqErrorType.ABORT_ERROR:
|
|
21
|
+
return "Request was cancelled.";
|
|
22
|
+
case GroqErrorType.RATE_LIMIT_ERROR:
|
|
23
|
+
return "Rate limit exceeded. Please wait a moment and try again.";
|
|
24
|
+
case GroqErrorType.QUOTA_EXCEEDED:
|
|
25
|
+
return "API quota exceeded. Please check your Groq account.";
|
|
26
|
+
case GroqErrorType.SERVER_ERROR:
|
|
27
|
+
return "Groq server error. Please try again later.";
|
|
28
|
+
default:
|
|
29
|
+
return error.message || "An unknown error occurred.";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (error instanceof Error) {
|
|
34
|
+
return error.message;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return "An unknown error occurred.";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if error is retryable
|
|
42
|
+
*/
|
|
43
|
+
export function isRetryableError(error: unknown): boolean {
|
|
44
|
+
if (error instanceof GroqError) {
|
|
45
|
+
return (
|
|
46
|
+
error.type === GroqErrorType.RATE_LIMIT_ERROR ||
|
|
47
|
+
error.type === GroqErrorType.SERVER_ERROR ||
|
|
48
|
+
error.type === GroqErrorType.NETWORK_ERROR
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if error is authentication related
|
|
56
|
+
*/
|
|
57
|
+
export function isAuthError(error: unknown): boolean {
|
|
58
|
+
if (error instanceof GroqError) {
|
|
59
|
+
return error.type === GroqErrorType.INVALID_API_KEY;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Format error for logging
|
|
66
|
+
*/
|
|
67
|
+
export function formatErrorForLogging(error: unknown): string {
|
|
68
|
+
if (error instanceof GroqError) {
|
|
69
|
+
return `[GroqError:${error.type}] ${error.message}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (error instanceof Error) {
|
|
73
|
+
return `[${error.name}] ${error.message}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return String(error);
|
|
77
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGroq Hook
|
|
3
|
+
* Main React hook for Groq text generation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback, useRef } from "react";
|
|
7
|
+
import type { GroqGenerationConfig, GroqChatConfig } from "../../domain/entities";
|
|
8
|
+
import { groqHttpClient } from "../../infrastructure/services/GroqClient";
|
|
9
|
+
import { chatSessionService } from "../../infrastructure/services/ChatSession";
|
|
10
|
+
import { textGeneration, chatGeneration } from "../../infrastructure/services/TextGeneration";
|
|
11
|
+
import { structuredText } from "../../infrastructure/services/StructuredText";
|
|
12
|
+
import { streaming } from "../../infrastructure/services/Streaming";
|
|
13
|
+
import { getUserFriendlyError } from "../../infrastructure/utils/error-mapper.util";
|
|
14
|
+
import { telemetry } from "../../infrastructure/telemetry";
|
|
15
|
+
|
|
16
|
+
export interface UseGroqOptions {
|
|
17
|
+
/** Initial model to use */
|
|
18
|
+
model?: string;
|
|
19
|
+
/** Default generation config */
|
|
20
|
+
generationConfig?: GroqGenerationConfig;
|
|
21
|
+
/** Callback when generation starts */
|
|
22
|
+
onStart?: () => void;
|
|
23
|
+
/** Callback when generation completes */
|
|
24
|
+
onSuccess?: (result: string) => void;
|
|
25
|
+
/** Callback when generation fails */
|
|
26
|
+
onError?: (error: string) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UseGroqReturn {
|
|
30
|
+
/** Loading state */
|
|
31
|
+
isLoading: boolean;
|
|
32
|
+
/** Error message */
|
|
33
|
+
error: string | null;
|
|
34
|
+
/** Generated result */
|
|
35
|
+
result: string | null;
|
|
36
|
+
/** Generate text from a prompt */
|
|
37
|
+
generate: (prompt: string, options?: GroqGenerationConfig) => Promise<string>;
|
|
38
|
+
/** Generate structured JSON output */
|
|
39
|
+
generateJSON: <T = Record<string, unknown>>(
|
|
40
|
+
prompt: string,
|
|
41
|
+
options?: GroqGenerationConfig & { schema?: Record<string, unknown> }
|
|
42
|
+
) => Promise<T>;
|
|
43
|
+
/** Stream text generation */
|
|
44
|
+
stream: (
|
|
45
|
+
prompt: string,
|
|
46
|
+
onChunk: (chunk: string) => void,
|
|
47
|
+
options?: GroqGenerationConfig
|
|
48
|
+
) => Promise<void>;
|
|
49
|
+
/** Reset state */
|
|
50
|
+
reset: () => void;
|
|
51
|
+
/** Clear error */
|
|
52
|
+
clearError: () => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Hook for Groq text generation
|
|
57
|
+
*/
|
|
58
|
+
export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
|
|
59
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
60
|
+
const [error, setError] = useState<string | null>(null);
|
|
61
|
+
const [result, setResult] = useState<string | null>(null);
|
|
62
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
63
|
+
|
|
64
|
+
const generate = useCallback(
|
|
65
|
+
async (prompt: string, config?: GroqGenerationConfig): Promise<string> => {
|
|
66
|
+
// Cancel any ongoing request
|
|
67
|
+
if (abortControllerRef.current) {
|
|
68
|
+
abortControllerRef.current.abort();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
abortControllerRef.current = new AbortController();
|
|
72
|
+
setIsLoading(true);
|
|
73
|
+
setError(null);
|
|
74
|
+
setResult(null);
|
|
75
|
+
|
|
76
|
+
telemetry.log("groq_generate_start", { prompt: prompt.substring(0, 100) });
|
|
77
|
+
options.onStart?.();
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const response = await textGeneration(prompt, {
|
|
81
|
+
model: options.model,
|
|
82
|
+
generationConfig: { ...options.generationConfig, ...config },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
setResult(response);
|
|
86
|
+
options.onSuccess?.(response);
|
|
87
|
+
telemetry.log("groq_generate_success", { responseLength: response.length });
|
|
88
|
+
|
|
89
|
+
return response;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
const errorMessage = getUserFriendlyError(err);
|
|
92
|
+
setError(errorMessage);
|
|
93
|
+
options.onError?.(errorMessage);
|
|
94
|
+
telemetry.log("groq_generate_error", { error: errorMessage });
|
|
95
|
+
throw err;
|
|
96
|
+
} finally {
|
|
97
|
+
setIsLoading(false);
|
|
98
|
+
abortControllerRef.current = null;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
[options]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const generateJSON = useCallback(
|
|
105
|
+
async <T = Record<string, unknown>,>(
|
|
106
|
+
prompt: string,
|
|
107
|
+
config?: GroqGenerationConfig & { schema?: Record<string, unknown> }
|
|
108
|
+
): Promise<T> => {
|
|
109
|
+
// Cancel any ongoing request
|
|
110
|
+
if (abortControllerRef.current) {
|
|
111
|
+
abortControllerRef.current.abort();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
abortControllerRef.current = new AbortController();
|
|
115
|
+
setIsLoading(true);
|
|
116
|
+
setError(null);
|
|
117
|
+
setResult(null);
|
|
118
|
+
|
|
119
|
+
telemetry.log("groq_generate_json_start", { prompt: prompt.substring(0, 100) });
|
|
120
|
+
options.onStart?.();
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const response = await structuredText<T>(prompt, {
|
|
124
|
+
model: options.model,
|
|
125
|
+
generationConfig: { ...options.generationConfig, ...config },
|
|
126
|
+
schema: config?.schema,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
setResult(JSON.stringify(response, null, 2));
|
|
130
|
+
options.onSuccess?.(JSON.stringify(response, null, 2));
|
|
131
|
+
telemetry.log("groq_generate_json_success");
|
|
132
|
+
|
|
133
|
+
return response;
|
|
134
|
+
} catch (err) {
|
|
135
|
+
const errorMessage = getUserFriendlyError(err);
|
|
136
|
+
setError(errorMessage);
|
|
137
|
+
options.onError?.(errorMessage);
|
|
138
|
+
telemetry.log("groq_generate_json_error", { error: errorMessage });
|
|
139
|
+
throw err;
|
|
140
|
+
} finally {
|
|
141
|
+
setIsLoading(false);
|
|
142
|
+
abortControllerRef.current = null;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
[options]
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const stream = useCallback(
|
|
149
|
+
async (
|
|
150
|
+
prompt: string,
|
|
151
|
+
onChunk: (chunk: string) => void,
|
|
152
|
+
config?: GroqGenerationConfig
|
|
153
|
+
): Promise<void> => {
|
|
154
|
+
// Cancel any ongoing request
|
|
155
|
+
if (abortControllerRef.current) {
|
|
156
|
+
abortControllerRef.current.abort();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
abortControllerRef.current = new AbortController();
|
|
160
|
+
setIsLoading(true);
|
|
161
|
+
setError(null);
|
|
162
|
+
setResult(null);
|
|
163
|
+
|
|
164
|
+
let fullContent = "";
|
|
165
|
+
|
|
166
|
+
telemetry.log("groq_stream_start", { prompt: prompt.substring(0, 100) });
|
|
167
|
+
options.onStart?.();
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
for await (const chunk of streaming(prompt, {
|
|
171
|
+
model: options.model,
|
|
172
|
+
generationConfig: { ...options.generationConfig, ...config },
|
|
173
|
+
callbacks: {
|
|
174
|
+
onChunk: (c) => {
|
|
175
|
+
fullContent += c;
|
|
176
|
+
onChunk(c);
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
})) {
|
|
180
|
+
// Streamed via callback
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
setResult(fullContent);
|
|
184
|
+
options.onSuccess?.(fullContent);
|
|
185
|
+
telemetry.log("groq_stream_success", { contentLength: fullContent.length });
|
|
186
|
+
} catch (err) {
|
|
187
|
+
const errorMessage = getUserFriendlyError(err);
|
|
188
|
+
setError(errorMessage);
|
|
189
|
+
options.onError?.(errorMessage);
|
|
190
|
+
telemetry.log("groq_stream_error", { error: errorMessage });
|
|
191
|
+
throw err;
|
|
192
|
+
} finally {
|
|
193
|
+
setIsLoading(false);
|
|
194
|
+
abortControllerRef.current = null;
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
[options]
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const reset = useCallback(() => {
|
|
201
|
+
if (abortControllerRef.current) {
|
|
202
|
+
abortControllerRef.current.abort();
|
|
203
|
+
abortControllerRef.current = null;
|
|
204
|
+
}
|
|
205
|
+
setIsLoading(false);
|
|
206
|
+
setError(null);
|
|
207
|
+
setResult(null);
|
|
208
|
+
}, []);
|
|
209
|
+
|
|
210
|
+
const clearError = useCallback(() => {
|
|
211
|
+
setError(null);
|
|
212
|
+
}, []);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
isLoading,
|
|
216
|
+
error,
|
|
217
|
+
result,
|
|
218
|
+
generate,
|
|
219
|
+
generateJSON,
|
|
220
|
+
stream,
|
|
221
|
+
reset,
|
|
222
|
+
clearError,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useOperationManager Hook
|
|
3
|
+
* Manages async operations with loading, error, and success states
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback, useRef } from "react";
|
|
7
|
+
import { getUserFriendlyError } from "../../infrastructure/utils/error-mapper.util";
|
|
8
|
+
import { telemetry } from "../../infrastructure/telemetry";
|
|
9
|
+
|
|
10
|
+
export interface OperationState<T> {
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
error: string | null;
|
|
13
|
+
data: T | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UseOperationManagerOptions<T> {
|
|
17
|
+
/** Initial data */
|
|
18
|
+
initialData?: T | null;
|
|
19
|
+
/** Callback when operation starts */
|
|
20
|
+
onStart?: () => void;
|
|
21
|
+
/** Callback when operation succeeds */
|
|
22
|
+
onSuccess?: (data: T) => void;
|
|
23
|
+
/** Callback when operation fails */
|
|
24
|
+
onError?: (error: string) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hook for managing async operations
|
|
29
|
+
*/
|
|
30
|
+
export function useOperationManager<T = unknown>(
|
|
31
|
+
options: UseOperationManagerOptions<T> = {}
|
|
32
|
+
) {
|
|
33
|
+
const [state, setState] = useState<OperationState<T>>({
|
|
34
|
+
isLoading: false,
|
|
35
|
+
error: null,
|
|
36
|
+
data: options.initialData ?? null,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
40
|
+
const operationNameRef = useRef<string>("");
|
|
41
|
+
|
|
42
|
+
const execute = useCallback(
|
|
43
|
+
async <R = T,>(
|
|
44
|
+
operationName: string,
|
|
45
|
+
asyncFn: (signal?: AbortSignal) => Promise<R>,
|
|
46
|
+
config?: { abortPrevious?: boolean }
|
|
47
|
+
): Promise<R> => {
|
|
48
|
+
// Cancel previous operation if configured
|
|
49
|
+
if (config?.abortPrevious && abortControllerRef.current) {
|
|
50
|
+
abortControllerRef.current.abort();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
abortControllerRef.current = new AbortController();
|
|
54
|
+
operationNameRef.current = operationName;
|
|
55
|
+
|
|
56
|
+
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
57
|
+
|
|
58
|
+
telemetry.log(`${operationName}_start`);
|
|
59
|
+
options.onStart?.();
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const result = await asyncFn(abortControllerRef.current.signal);
|
|
63
|
+
|
|
64
|
+
setState((prev) => ({ ...prev, isLoading: false, data: result as unknown as T }));
|
|
65
|
+
options.onSuccess?.(result as unknown as T);
|
|
66
|
+
telemetry.log(`${operationName}_success`);
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const errorMessage = getUserFriendlyError(error);
|
|
71
|
+
setState((prev) => ({ ...prev, isLoading: false, error: errorMessage }));
|
|
72
|
+
options.onError?.(errorMessage);
|
|
73
|
+
telemetry.log(`${operationName}_error`, { error: errorMessage });
|
|
74
|
+
throw error;
|
|
75
|
+
} finally {
|
|
76
|
+
abortControllerRef.current = null;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
[options]
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const reset = useCallback(() => {
|
|
83
|
+
if (abortControllerRef.current) {
|
|
84
|
+
abortControllerRef.current.abort();
|
|
85
|
+
abortControllerRef.current = null;
|
|
86
|
+
}
|
|
87
|
+
setState((prev) => ({
|
|
88
|
+
...prev,
|
|
89
|
+
isLoading: false,
|
|
90
|
+
error: null,
|
|
91
|
+
data: options.initialData ?? null,
|
|
92
|
+
}));
|
|
93
|
+
}, [options.initialData]);
|
|
94
|
+
|
|
95
|
+
const clearError = useCallback(() => {
|
|
96
|
+
setState((prev) => ({ ...prev, error: null }));
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
const setData = useCallback((data: T | null) => {
|
|
100
|
+
setState((prev) => ({ ...prev, data }));
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
...state,
|
|
105
|
+
execute,
|
|
106
|
+
reset,
|
|
107
|
+
clearError,
|
|
108
|
+
setData,
|
|
109
|
+
};
|
|
110
|
+
}
|