react-native-ai-hooks 0.2.0 → 0.4.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.
@@ -0,0 +1,158 @@
1
+ import { useCallback, useRef, useState } from 'react';
2
+
3
+ type SummaryLength = 'short' | 'medium' | 'long';
4
+
5
+ interface UseAISummarizeOptions {
6
+ apiKey: string;
7
+ model?: string;
8
+ system?: string;
9
+ maxTokens?: number;
10
+ temperature?: number;
11
+ defaultLength?: SummaryLength;
12
+ }
13
+
14
+ interface SummarizeInput {
15
+ text: string;
16
+ length?: SummaryLength;
17
+ }
18
+
19
+ interface UseAISummarizeReturn {
20
+ summary: string;
21
+ length: SummaryLength;
22
+ isLoading: boolean;
23
+ error: string | null;
24
+ setLength: (length: SummaryLength) => void;
25
+ summarizeText: (input: SummarizeInput) => Promise<string | null>;
26
+ clearSummary: () => void;
27
+ }
28
+
29
+ interface ClaudeTextBlock {
30
+ type?: string;
31
+ text?: string;
32
+ }
33
+
34
+ interface ClaudeApiResult {
35
+ content?: ClaudeTextBlock[];
36
+ error?: {
37
+ message?: string;
38
+ };
39
+ }
40
+
41
+ function getClaudeTextContent(data: unknown): string {
42
+ const content = (data as ClaudeApiResult)?.content;
43
+ if (!Array.isArray(content)) {
44
+ return '';
45
+ }
46
+
47
+ return content
48
+ .filter(item => item?.type === 'text' && typeof item.text === 'string')
49
+ .map(item => item.text as string)
50
+ .join('\n')
51
+ .trim();
52
+ }
53
+
54
+ function lengthInstruction(length: SummaryLength): string {
55
+ if (length === 'short') {
56
+ return 'Produce a concise summary in 2-4 bullet points.';
57
+ }
58
+
59
+ if (length === 'long') {
60
+ return 'Produce a detailed summary with key points, context, and implications in 3-5 short paragraphs.';
61
+ }
62
+
63
+ return 'Produce a balanced summary in 1-2 paragraphs plus a short bullet list of key takeaways.';
64
+ }
65
+
66
+ export function useAISummarize(options: UseAISummarizeOptions): UseAISummarizeReturn {
67
+ const [summary, setSummary] = useState('');
68
+ const [length, setLength] = useState<SummaryLength>(options.defaultLength || 'medium');
69
+ const [isLoading, setIsLoading] = useState(false);
70
+ const [error, setError] = useState<string | null>(null);
71
+
72
+ const isMountedRef = useRef(true);
73
+
74
+ const clearSummary = useCallback(() => {
75
+ setSummary('');
76
+ setError(null);
77
+ }, []);
78
+
79
+ const summarizeText = useCallback(
80
+ async (input: SummarizeInput) => {
81
+ const text = input.text.trim();
82
+ const selectedLength = input.length || length;
83
+
84
+ if (!text) {
85
+ setError('No text provided for summarization.');
86
+ return null;
87
+ }
88
+
89
+ if (!options.apiKey) {
90
+ setError('Missing Claude API key.');
91
+ return null;
92
+ }
93
+
94
+ setIsLoading(true);
95
+ setError(null);
96
+ setLength(selectedLength);
97
+
98
+ try {
99
+ const prompt = [
100
+ 'Summarize the following text.',
101
+ lengthInstruction(selectedLength),
102
+ 'Text:',
103
+ text,
104
+ ].join('\n');
105
+
106
+ const apiResponse = await fetch('https://api.anthropic.com/v1/messages', {
107
+ method: 'POST',
108
+ headers: {
109
+ 'Content-Type': 'application/json',
110
+ 'x-api-key': options.apiKey,
111
+ 'anthropic-version': '2023-06-01',
112
+ },
113
+ body: JSON.stringify({
114
+ model: options.model || 'claude-sonnet-4-20250514',
115
+ max_tokens: options.maxTokens ?? 1200,
116
+ temperature: options.temperature ?? 0.3,
117
+ system:
118
+ options.system ||
119
+ 'You are an expert summarization assistant. Keep summaries faithful to source text and avoid fabrications.',
120
+ messages: [{ role: 'user', content: prompt }],
121
+ }),
122
+ });
123
+
124
+ const data = (await apiResponse.json()) as ClaudeApiResult;
125
+ if (!apiResponse.ok) {
126
+ throw new Error(data?.error?.message || `Claude API error: ${apiResponse.status}`);
127
+ }
128
+
129
+ const result = getClaudeTextContent(data);
130
+ if (!result) {
131
+ throw new Error('No summary returned by Claude API.');
132
+ }
133
+
134
+ setSummary(result);
135
+ return result;
136
+ } catch (err) {
137
+ const message = (err as Error).message || 'Failed to summarize text';
138
+ setError(message);
139
+ return null;
140
+ } finally {
141
+ if (isMountedRef.current) {
142
+ setIsLoading(false);
143
+ }
144
+ }
145
+ },
146
+ [length, options.apiKey, options.maxTokens, options.model, options.system, options.temperature],
147
+ );
148
+
149
+ return {
150
+ summary,
151
+ length,
152
+ isLoading,
153
+ error,
154
+ setLength,
155
+ summarizeText,
156
+ clearSummary,
157
+ };
158
+ }
@@ -0,0 +1,207 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ interface UseAITranslateOptions {
4
+ apiKey: string;
5
+ model?: string;
6
+ system?: string;
7
+ maxTokens?: number;
8
+ temperature?: number;
9
+ initialTargetLanguage?: string;
10
+ autoTranslate?: boolean;
11
+ debounceMs?: number;
12
+ }
13
+
14
+ interface TranslationResult {
15
+ translatedText: string;
16
+ detectedSourceLanguage: string;
17
+ targetLanguage: string;
18
+ }
19
+
20
+ interface UseAITranslateReturn {
21
+ sourceText: string;
22
+ translatedText: string;
23
+ detectedSourceLanguage: string;
24
+ targetLanguage: string;
25
+ isTranslating: boolean;
26
+ error: string | null;
27
+ setSourceText: (text: string) => void;
28
+ setTargetLanguage: (language: string) => void;
29
+ translateText: (overrideText?: string) => Promise<TranslationResult | null>;
30
+ clearTranslation: () => void;
31
+ }
32
+
33
+ interface ClaudeTextBlock {
34
+ type?: string;
35
+ text?: string;
36
+ }
37
+
38
+ interface ClaudeApiResult {
39
+ content?: ClaudeTextBlock[];
40
+ error?: {
41
+ message?: string;
42
+ };
43
+ }
44
+
45
+ function getClaudeTextContent(data: unknown): string {
46
+ const content = (data as ClaudeApiResult)?.content;
47
+ if (!Array.isArray(content)) {
48
+ return '';
49
+ }
50
+
51
+ return content
52
+ .filter(item => item?.type === 'text' && typeof item.text === 'string')
53
+ .map(item => item.text as string)
54
+ .join('\n')
55
+ .trim();
56
+ }
57
+
58
+ function parseJsonFromText<T>(text: string): T {
59
+ const trimmed = text.trim();
60
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
61
+ const candidate = fenced?.[1]?.trim() || trimmed;
62
+ return JSON.parse(candidate) as T;
63
+ }
64
+
65
+ export function useAITranslate(options: UseAITranslateOptions): UseAITranslateReturn {
66
+ const [sourceText, setSourceText] = useState('');
67
+ const [translatedText, setTranslatedText] = useState('');
68
+ const [detectedSourceLanguage, setDetectedSourceLanguage] = useState('');
69
+ const [targetLanguage, setTargetLanguage] = useState(options.initialTargetLanguage || 'English');
70
+ const [isTranslating, setIsTranslating] = useState(false);
71
+ const [error, setError] = useState<string | null>(null);
72
+
73
+ const isMountedRef = useRef(true);
74
+
75
+ const clearTranslation = useCallback(() => {
76
+ setSourceText('');
77
+ setTranslatedText('');
78
+ setDetectedSourceLanguage('');
79
+ setError(null);
80
+ }, []);
81
+
82
+ const translateText = useCallback(
83
+ async (overrideText?: string) => {
84
+ const textToTranslate = (overrideText ?? sourceText).trim();
85
+
86
+ if (!textToTranslate) {
87
+ setError('No text to translate.');
88
+ return null;
89
+ }
90
+
91
+ if (!options.apiKey) {
92
+ setError('Missing Claude API key.');
93
+ return null;
94
+ }
95
+
96
+ setIsTranslating(true);
97
+ setError(null);
98
+
99
+ try {
100
+ const prompt = [
101
+ 'Detect source language and translate text.',
102
+ 'Return JSON only using this schema:',
103
+ '{"detectedSourceLanguage":"string","targetLanguage":"string","translatedText":"string"}',
104
+ `Target language: ${targetLanguage}`,
105
+ 'Text:',
106
+ textToTranslate,
107
+ ].join('\n');
108
+
109
+ const apiResponse = await fetch('https://api.anthropic.com/v1/messages', {
110
+ method: 'POST',
111
+ headers: {
112
+ 'Content-Type': 'application/json',
113
+ 'x-api-key': options.apiKey,
114
+ 'anthropic-version': '2023-06-01',
115
+ },
116
+ body: JSON.stringify({
117
+ model: options.model || 'claude-sonnet-4-20250514',
118
+ max_tokens: options.maxTokens ?? 800,
119
+ temperature: options.temperature ?? 0.2,
120
+ system:
121
+ options.system ||
122
+ 'You are a translation assistant. Always detect source language and translate accurately. Return valid JSON only.',
123
+ messages: [{ role: 'user', content: prompt }],
124
+ }),
125
+ });
126
+
127
+ const data = (await apiResponse.json()) as ClaudeApiResult;
128
+ if (!apiResponse.ok) {
129
+ throw new Error(data?.error?.message || `Claude API error: ${apiResponse.status}`);
130
+ }
131
+
132
+ const text = getClaudeTextContent(data);
133
+ if (!text) {
134
+ throw new Error('No translation returned by Claude API.');
135
+ }
136
+
137
+ const parsed = parseJsonFromText<{
138
+ translatedText?: string;
139
+ detectedSourceLanguage?: string;
140
+ targetLanguage?: string;
141
+ }>(text);
142
+
143
+ const result: TranslationResult = {
144
+ translatedText: parsed?.translatedText?.trim() || '',
145
+ detectedSourceLanguage: parsed?.detectedSourceLanguage?.trim() || 'Unknown',
146
+ targetLanguage: parsed?.targetLanguage?.trim() || targetLanguage,
147
+ };
148
+
149
+ if (!result.translatedText) {
150
+ throw new Error('Invalid translation format returned by Claude API.');
151
+ }
152
+
153
+ setTranslatedText(result.translatedText);
154
+ setDetectedSourceLanguage(result.detectedSourceLanguage);
155
+ return result;
156
+ } catch (err) {
157
+ const message = (err as Error).message || 'Failed to translate text';
158
+ setError(message);
159
+ return null;
160
+ } finally {
161
+ if (isMountedRef.current) {
162
+ setIsTranslating(false);
163
+ }
164
+ }
165
+ },
166
+ [options.apiKey, options.maxTokens, options.model, options.system, options.temperature, sourceText, targetLanguage],
167
+ );
168
+
169
+ useEffect(() => {
170
+ isMountedRef.current = true;
171
+ return () => {
172
+ isMountedRef.current = false;
173
+ };
174
+ }, []);
175
+
176
+ useEffect(() => {
177
+ if (options.autoTranslate === false) {
178
+ return;
179
+ }
180
+
181
+ const text = sourceText.trim();
182
+ if (!text) {
183
+ setTranslatedText('');
184
+ setDetectedSourceLanguage('');
185
+ return;
186
+ }
187
+
188
+ const timer = setTimeout(() => {
189
+ translateText(text).catch(() => undefined);
190
+ }, options.debounceMs ?? 500);
191
+
192
+ return () => clearTimeout(timer);
193
+ }, [options.autoTranslate, options.debounceMs, sourceText, targetLanguage, translateText]);
194
+
195
+ return {
196
+ sourceText,
197
+ translatedText,
198
+ detectedSourceLanguage,
199
+ targetLanguage,
200
+ isTranslating,
201
+ error,
202
+ setSourceText,
203
+ setTargetLanguage,
204
+ translateText,
205
+ clearTranslation,
206
+ };
207
+ }
@@ -1,47 +1,20 @@
1
- import { useCallback, useState } from 'react';
2
-
3
- interface UseImageAnalysisOptions {
4
- apiKey: string;
5
- model?: string;
6
- maxTokens?: number;
7
- system?: string;
8
- uriToBase64?: (uri: string) => Promise<string>;
9
- }
10
-
11
- interface AnalyzeImageInput {
12
- image: string;
13
- mediaType?: string;
14
- prompt?: string;
15
- }
1
+ import { useCallback, useState, useRef, useMemo } from 'react';
2
+ import type { UseImageAnalysisOptions, UseImageAnalysisReturn } from '../types';
3
+ import { createProvider } from '../utils/providerFactory';
16
4
 
17
- interface UseImageAnalysisReturn {
18
- description: string;
19
- isLoading: boolean;
20
- error: string | null;
21
- analyzeImage: (input: AnalyzeImageInput) => Promise<string | null>;
22
- clearDescription: () => void;
23
- }
24
-
25
- interface ParsedImageData {
26
- base64: string;
27
- mediaType: string;
28
- }
5
+ const DEFAULT_MODEL_MAP = {
6
+ anthropic: 'claude-sonnet-4-20250514',
7
+ openai: 'gpt-4-vision',
8
+ gemini: 'gemini-pro-vision',
9
+ };
29
10
 
30
11
  const DATA_URI_REGEX = /^data:(image\/[a-zA-Z0-9.+-]+);base64,(.*)$/;
31
12
 
32
13
  function getMediaTypeFromUri(uri: string): string {
33
14
  const normalized = uri.toLowerCase();
34
-
35
- if (normalized.includes('.png')) {
36
- return 'image/png';
37
- }
38
- if (normalized.includes('.webp')) {
39
- return 'image/webp';
40
- }
41
- if (normalized.includes('.gif')) {
42
- return 'image/gif';
43
- }
44
-
15
+ if (normalized.includes('.png')) return 'image/png';
16
+ if (normalized.includes('.webp')) return 'image/webp';
17
+ if (normalized.includes('.gif')) return 'image/gif';
45
18
  return 'image/jpeg';
46
19
  }
47
20
 
@@ -56,7 +29,7 @@ function arrayBufferToBase64(buffer: ArrayBuffer): string {
56
29
  }
57
30
 
58
31
  if (typeof btoa !== 'function') {
59
- throw new Error('Base64 conversion is unavailable. Provide uriToBase64 in hook options.');
32
+ throw new Error('Base64 conversion unavailable. Provide uriToBase64 in hook options.');
60
33
  }
61
34
 
62
35
  return btoa(binary);
@@ -77,21 +50,31 @@ export function useImageAnalysis(options: UseImageAnalysisOptions): UseImageAnal
77
50
  const [isLoading, setIsLoading] = useState(false);
78
51
  const [error, setError] = useState<string | null>(null);
79
52
 
53
+ const isMountedRef = useRef(true);
54
+
55
+ const providerConfig = useMemo(
56
+ () => ({
57
+ provider: (options.provider || 'anthropic') as 'anthropic' | 'openai' | 'gemini',
58
+ apiKey: options.apiKey,
59
+ model: options.model || DEFAULT_MODEL_MAP[options.provider || 'anthropic'],
60
+ baseUrl: options.baseUrl,
61
+ timeout: options.timeout,
62
+ maxRetries: options.maxRetries,
63
+ }),
64
+ [options],
65
+ );
66
+
80
67
  const normalizeImage = useCallback(
81
- async (image: string, mediaType?: string): Promise<ParsedImageData> => {
68
+ async (image: string, mediaType?: string): Promise<{ base64: string; mediaType: string }> => {
82
69
  const dataUriMatch = image.match(DATA_URI_REGEX);
83
70
  if (dataUriMatch) {
84
- const matchedMediaType = dataUriMatch[1];
85
- const base64Data = dataUriMatch[2];
86
71
  return {
87
- base64: base64Data,
88
- mediaType: mediaType || matchedMediaType,
72
+ base64: dataUriMatch[2],
73
+ mediaType: mediaType || dataUriMatch[1],
89
74
  };
90
75
  }
91
76
 
92
- const isLikelyUri = /^(https?:\/\/|file:\/\/|content:\/\/|ph:\/\/|assets-library:\/\/)/i.test(
93
- image,
94
- );
77
+ const isLikelyUri = /^(https?:\/\/|file:\/\/|content:\/\/|ph:\/\/|assets-library:\/\/)/i.test(image);
95
78
 
96
79
  if (isLikelyUri) {
97
80
  const toBase64 = options.uriToBase64 || defaultUriToBase64;
@@ -116,24 +99,39 @@ export function useImageAnalysis(options: UseImageAnalysisOptions): UseImageAnal
116
99
  }, []);
117
100
 
118
101
  const analyzeImage = useCallback(
119
- async (input: AnalyzeImageInput) => {
120
- setIsLoading(true);
102
+ async (uri: string, prompt?: string) => {
103
+ if (!uri) {
104
+ setError('Image URI is required');
105
+ return null;
106
+ }
107
+
108
+ if (!options.apiKey) {
109
+ setError('Missing API key');
110
+ return null;
111
+ }
112
+
121
113
  setError(null);
114
+ setIsLoading(true);
122
115
 
123
116
  try {
124
- const parsedImage = await normalizeImage(input.image, input.mediaType);
117
+ const imagData = await normalizeImage(uri);
118
+ const analysisPrompt = prompt || 'Describe this image in detail.';
125
119
 
126
- const apiResponse = await fetch('https://api.anthropic.com/v1/messages', {
127
- method: 'POST',
128
- headers: {
129
- 'Content-Type': 'application/json',
130
- 'x-api-key': options.apiKey,
131
- 'anthropic-version': '2023-06-01',
132
- },
133
- body: JSON.stringify({
134
- model: options.model || 'claude-sonnet-4-20250514',
120
+ const base_url = providerConfig.baseUrl || 'https://api.anthropic.com';
121
+ const headers: Record<string, string> = {
122
+ 'Content-Type': 'application/json',
123
+ };
124
+
125
+ let url = '';
126
+ let body: Record<string, unknown> = {};
127
+
128
+ if (providerConfig.provider === 'anthropic') {
129
+ url = `${base_url}/v1/messages`;
130
+ headers['x-api-key'] = options.apiKey;
131
+ headers['anthropic-version'] = '2023-06-01';
132
+ body = {
133
+ model: providerConfig.model,
135
134
  max_tokens: options.maxTokens ?? 1024,
136
- system: options.system,
137
135
  messages: [
138
136
  {
139
137
  role: 'user',
@@ -142,47 +140,96 @@ export function useImageAnalysis(options: UseImageAnalysisOptions): UseImageAnal
142
140
  type: 'image',
143
141
  source: {
144
142
  type: 'base64',
145
- media_type: parsedImage.mediaType,
146
- data: parsedImage.base64,
143
+ media_type: imagData.mediaType,
144
+ data: imagData.base64,
147
145
  },
148
146
  },
149
147
  {
150
148
  type: 'text',
151
- text: input.prompt || 'Describe this image in detail.',
149
+ text: analysisPrompt,
152
150
  },
153
151
  ],
154
152
  },
155
153
  ],
156
- }),
154
+ };
155
+ } else if (providerConfig.provider === 'openai') {
156
+ url = `${providerConfig.baseUrl || 'https://api.openai.com'}/v1/chat/completions`;
157
+ headers.Authorization = `Bearer ${options.apiKey}`;
158
+ body = {
159
+ model: providerConfig.model,
160
+ max_tokens: options.maxTokens ?? 1024,
161
+ messages: [
162
+ {
163
+ role: 'user',
164
+ content: [
165
+ {
166
+ type: 'image_url',
167
+ image_url: {
168
+ url: `data:${imagData.mediaType};base64,${imagData.base64}`,
169
+ },
170
+ },
171
+ {
172
+ type: 'text',
173
+ text: analysisPrompt,
174
+ },
175
+ ],
176
+ },
177
+ ],
178
+ };
179
+ } else {
180
+ throw new Error(`Image analysis not supported for provider: ${providerConfig.provider}`);
181
+ }
182
+
183
+ const response = await fetch(url, {
184
+ method: 'POST',
185
+ headers,
186
+ body: JSON.stringify(body),
157
187
  });
158
188
 
159
- if (!apiResponse.ok) {
160
- const errorText = await apiResponse.text();
161
- throw new Error(errorText || `Claude API error: ${apiResponse.status}`);
189
+ if (!response.ok) {
190
+ const errorText = await response.text();
191
+ throw new Error(errorText || `API error: ${response.status}`);
162
192
  }
163
193
 
164
- const data = await apiResponse.json();
165
- const textResult = data?.content?.find?.(
166
- (item: { type?: string; text?: string }) => item?.type === 'text',
167
- )?.text;
194
+ const data = await response.json();
195
+ let resultText = '';
196
+
197
+ if (providerConfig.provider === 'anthropic') {
198
+ resultText = data?.content?.[0]?.text || '';
199
+ } else if (providerConfig.provider === 'openai') {
200
+ resultText = data?.choices?.[0]?.message?.content || '';
201
+ }
168
202
 
169
- if (!textResult) {
170
- throw new Error('No description returned from Claude vision API.');
203
+ if (!resultText) {
204
+ throw new Error('No description returned by vision API');
171
205
  }
172
206
 
173
- setDescription(textResult);
174
- return textResult;
207
+ if (isMountedRef.current) {
208
+ setDescription(resultText);
209
+ }
210
+ return resultText;
175
211
  } catch (err) {
176
- const message = (err as Error).message || 'Failed to analyze image';
177
- setError(message);
212
+ if (isMountedRef.current) {
213
+ const message = err instanceof Error ? err.message : 'Failed to analyze image';
214
+ setError(message);
215
+ }
178
216
  return null;
179
217
  } finally {
180
- setIsLoading(false);
218
+ if (isMountedRef.current) {
219
+ setIsLoading(false);
220
+ }
181
221
  }
182
222
  },
183
- [normalizeImage, options],
223
+ [normalizeImage, options, providerConfig],
184
224
  );
185
225
 
226
+ useState(() => {
227
+ isMountedRef.current = true;
228
+ return () => {
229
+ isMountedRef.current = false;
230
+ };
231
+ }, []);
232
+
186
233
  return {
187
234
  description,
188
235
  isLoading,
package/src/index.ts CHANGED
@@ -2,4 +2,31 @@ export { useAIChat } from './hooks/useAIChat';
2
2
  export { useAIStream } from './hooks/useAIStream';
3
3
  export { useImageAnalysis } from './hooks/useImageAnalysis';
4
4
  export { useAIForm } from './hooks/useAIForm';
5
- export { useAIVoice } from './hooks/useAIVoice';
5
+ export { useAIVoice } from './hooks/useAIVoice';
6
+ export { useAITranslate } from './hooks/useAITranslate';
7
+ export { useAISummarize } from './hooks/useAISummarize';
8
+ export { useAICode } from './hooks/useAICode';
9
+
10
+ // Type exports
11
+ export type {
12
+ Message,
13
+ AIProviderType,
14
+ ProviderConfig,
15
+ AIResponse,
16
+ AIRequestOptions,
17
+ UseAIChatOptions,
18
+ UseAIChatReturn,
19
+ UseAIStreamOptions,
20
+ UseAIStreamReturn,
21
+ UseImageAnalysisOptions,
22
+ UseImageAnalysisReturn,
23
+ UseAIFormOptions,
24
+ UseAIFormReturn,
25
+ FormValidationRequest,
26
+ FormValidationResult,
27
+ } from './types';
28
+
29
+ // Utility exports for advanced use cases
30
+ export { createProvider, ProviderFactory } from './utils/providerFactory';
31
+ export { fetchWithRetry } from './utils/fetchWithRetry';
32
+ export type { AIResponse, AIRequestOptions } from './utils/providerFactory';