react-native-ai-hooks 0.1.0 → 0.3.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/README.md CHANGED
@@ -1,3 +1,8 @@
1
+ [![npm downloads](https://img.shields.io/npm/dw/react-native-ai-hooks)](https://npmjs.com/package/react-native-ai-hooks)
2
+ [![npm version](https://img.shields.io/npm/v/react-native-ai-hooks)](https://npmjs.com/package/react-native-ai-hooks)
3
+ [![GitHub stars](https://img.shields.io/github/stars/nikapkh/react-native-ai-hooks)](https://github.com/nikapkh/react-native-ai-hooks)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
1
6
  # react-native-ai-hooks
2
7
 
3
8
  AI hooks for React Native — add Claude, OpenAI & Gemini to your app in minutes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-ai-hooks",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "AI hooks for React Native — useAIChat, useAIStream, useImageAnalysis. Works with Claude, OpenAI & Gemini.",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -0,0 +1,206 @@
1
+ import { useCallback, useRef, useState } from 'react';
2
+
3
+ interface UseAICodeOptions {
4
+ apiKey: string;
5
+ model?: string;
6
+ system?: string;
7
+ maxTokens?: number;
8
+ temperature?: number;
9
+ defaultLanguage?: string;
10
+ }
11
+
12
+ interface GenerateCodeInput {
13
+ prompt: string;
14
+ language?: string;
15
+ }
16
+
17
+ interface ExplainCodeInput {
18
+ code: string;
19
+ language?: string;
20
+ focus?: string;
21
+ }
22
+
23
+ interface UseAICodeReturn {
24
+ language: string;
25
+ generatedCode: string;
26
+ explanation: string;
27
+ isLoading: boolean;
28
+ error: string | null;
29
+ setLanguage: (language: string) => void;
30
+ generateCode: (input: GenerateCodeInput) => Promise<string | null>;
31
+ explainCode: (input: ExplainCodeInput) => Promise<string | null>;
32
+ clearCodeState: () => void;
33
+ }
34
+
35
+ interface ClaudeTextBlock {
36
+ type?: string;
37
+ text?: string;
38
+ }
39
+
40
+ interface ClaudeApiResult {
41
+ content?: ClaudeTextBlock[];
42
+ error?: {
43
+ message?: string;
44
+ };
45
+ }
46
+
47
+ function getClaudeTextContent(data: unknown): string {
48
+ const content = (data as ClaudeApiResult)?.content;
49
+ if (!Array.isArray(content)) {
50
+ return '';
51
+ }
52
+
53
+ return content
54
+ .filter(item => item?.type === 'text' && typeof item.text === 'string')
55
+ .map(item => item.text as string)
56
+ .join('\n')
57
+ .trim();
58
+ }
59
+
60
+ export function useAICode(options: UseAICodeOptions): UseAICodeReturn {
61
+ const [language, setLanguage] = useState(options.defaultLanguage || 'typescript');
62
+ const [generatedCode, setGeneratedCode] = useState('');
63
+ const [explanation, setExplanation] = useState('');
64
+ const [isLoading, setIsLoading] = useState(false);
65
+ const [error, setError] = useState<string | null>(null);
66
+
67
+ const isMountedRef = useRef(true);
68
+
69
+ const clearCodeState = useCallback(() => {
70
+ setGeneratedCode('');
71
+ setExplanation('');
72
+ setError(null);
73
+ }, []);
74
+
75
+ const sendClaudeRequest = useCallback(
76
+ async (prompt: string) => {
77
+ const apiResponse = await fetch('https://api.anthropic.com/v1/messages', {
78
+ method: 'POST',
79
+ headers: {
80
+ 'Content-Type': 'application/json',
81
+ 'x-api-key': options.apiKey,
82
+ 'anthropic-version': '2023-06-01',
83
+ },
84
+ body: JSON.stringify({
85
+ model: options.model || 'claude-sonnet-4-20250514',
86
+ max_tokens: options.maxTokens ?? 1800,
87
+ temperature: options.temperature ?? 0.2,
88
+ system:
89
+ options.system ||
90
+ 'You are an expert software engineer. Produce practical, correct code and clear explanations.',
91
+ messages: [{ role: 'user', content: prompt }],
92
+ }),
93
+ });
94
+
95
+ const data = (await apiResponse.json()) as ClaudeApiResult;
96
+ if (!apiResponse.ok) {
97
+ throw new Error(data?.error?.message || `Claude API error: ${apiResponse.status}`);
98
+ }
99
+
100
+ const text = getClaudeTextContent(data);
101
+ if (!text) {
102
+ throw new Error('No content returned by Claude API.');
103
+ }
104
+
105
+ return text;
106
+ },
107
+ [options.apiKey, options.maxTokens, options.model, options.system, options.temperature],
108
+ );
109
+
110
+ const generateCode = useCallback(
111
+ async (input: GenerateCodeInput) => {
112
+ const taskPrompt = input.prompt.trim();
113
+ const selectedLanguage = (input.language || language).trim();
114
+
115
+ if (!taskPrompt) {
116
+ setError('No code generation prompt provided.');
117
+ return null;
118
+ }
119
+
120
+ if (!options.apiKey) {
121
+ setError('Missing Claude API key.');
122
+ return null;
123
+ }
124
+
125
+ setIsLoading(true);
126
+ setError(null);
127
+ setLanguage(selectedLanguage);
128
+
129
+ try {
130
+ const prompt = [
131
+ `Generate ${selectedLanguage} code for the following request:`,
132
+ taskPrompt,
133
+ 'Return runnable code and include brief usage notes only when necessary.',
134
+ ].join('\n');
135
+
136
+ const result = await sendClaudeRequest(prompt);
137
+ setGeneratedCode(result);
138
+ return result;
139
+ } catch (err) {
140
+ const message = (err as Error).message || 'Failed to generate code';
141
+ setError(message);
142
+ return null;
143
+ } finally {
144
+ if (isMountedRef.current) {
145
+ setIsLoading(false);
146
+ }
147
+ }
148
+ },
149
+ [language, options.apiKey, sendClaudeRequest],
150
+ );
151
+
152
+ const explainCode = useCallback(
153
+ async (input: ExplainCodeInput) => {
154
+ const code = input.code.trim();
155
+ const selectedLanguage = (input.language || language).trim();
156
+
157
+ if (!code) {
158
+ setError('No code provided for explanation.');
159
+ return null;
160
+ }
161
+
162
+ if (!options.apiKey) {
163
+ setError('Missing Claude API key.');
164
+ return null;
165
+ }
166
+
167
+ setIsLoading(true);
168
+ setError(null);
169
+ setLanguage(selectedLanguage);
170
+
171
+ try {
172
+ const prompt = [
173
+ `Explain the following ${selectedLanguage} code.`,
174
+ input.focus ? `Focus: ${input.focus}` : 'Focus: logic, structure, and potential pitfalls.',
175
+ 'Code:',
176
+ code,
177
+ ].join('\n');
178
+
179
+ const result = await sendClaudeRequest(prompt);
180
+ setExplanation(result);
181
+ return result;
182
+ } catch (err) {
183
+ const message = (err as Error).message || 'Failed to explain code';
184
+ setError(message);
185
+ return null;
186
+ } finally {
187
+ if (isMountedRef.current) {
188
+ setIsLoading(false);
189
+ }
190
+ }
191
+ },
192
+ [language, options.apiKey, sendClaudeRequest],
193
+ );
194
+
195
+ return {
196
+ language,
197
+ generatedCode,
198
+ explanation,
199
+ isLoading,
200
+ error,
201
+ setLanguage,
202
+ generateCode,
203
+ explainCode,
204
+ clearCodeState,
205
+ };
206
+ }
@@ -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
+ }
@@ -0,0 +1,219 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { PermissionsAndroid, Platform } from 'react-native';
3
+ import Voice from '@react-native-voice/voice';
4
+
5
+ interface UseAIVoiceOptions {
6
+ apiKey: string;
7
+ model?: string;
8
+ maxTokens?: number;
9
+ system?: string;
10
+ language?: string;
11
+ autoSendOnStop?: boolean;
12
+ }
13
+
14
+ interface UseAIVoiceReturn {
15
+ transcription: string;
16
+ response: string;
17
+ isRecording: boolean;
18
+ isLoading: boolean;
19
+ error: string | null;
20
+ startRecording: () => Promise<void>;
21
+ stopRecording: () => Promise<void>;
22
+ sendTranscription: (overrideText?: string) => Promise<string | null>;
23
+ clearVoiceState: () => void;
24
+ }
25
+
26
+ interface ClaudeTextBlock {
27
+ type?: string;
28
+ text?: string;
29
+ }
30
+
31
+ interface ClaudeApiResult {
32
+ content?: ClaudeTextBlock[];
33
+ error?: {
34
+ message?: string;
35
+ };
36
+ }
37
+
38
+ interface SpeechResultsEvent {
39
+ value?: string[];
40
+ }
41
+
42
+ interface SpeechErrorEvent {
43
+ error?: {
44
+ message?: string;
45
+ };
46
+ }
47
+
48
+ function getClaudeTextContent(data: unknown): string {
49
+ const content = (data as ClaudeApiResult)?.content;
50
+ if (!Array.isArray(content)) {
51
+ return '';
52
+ }
53
+
54
+ return content
55
+ .filter(item => item?.type === 'text' && typeof item.text === 'string')
56
+ .map(item => item.text as string)
57
+ .join('\n')
58
+ .trim();
59
+ }
60
+
61
+ export function useAIVoice(options: UseAIVoiceOptions): UseAIVoiceReturn {
62
+ const [transcription, setTranscription] = useState('');
63
+ const [response, setResponse] = useState('');
64
+ const [isRecording, setIsRecording] = useState(false);
65
+ const [isLoading, setIsLoading] = useState(false);
66
+ const [error, setError] = useState<string | null>(null);
67
+
68
+ const transcriptionRef = useRef('');
69
+ const isMountedRef = useRef(true);
70
+
71
+ const requestMicPermission = useCallback(async () => {
72
+ if (Platform.OS !== 'android') {
73
+ return true;
74
+ }
75
+
76
+ const permission = await PermissionsAndroid.request(
77
+ PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
78
+ );
79
+ return permission === PermissionsAndroid.RESULTS.GRANTED;
80
+ }, []);
81
+
82
+ const sendTranscription = useCallback(
83
+ async (overrideText?: string) => {
84
+ const prompt = (overrideText ?? transcriptionRef.current).trim();
85
+
86
+ if (!prompt) {
87
+ setError('No transcription available to send.');
88
+ return null;
89
+ }
90
+
91
+ if (!options.apiKey) {
92
+ setError('Missing Claude API key.');
93
+ return null;
94
+ }
95
+
96
+ setIsLoading(true);
97
+ setError(null);
98
+
99
+ try {
100
+ const apiResponse = await fetch('https://api.anthropic.com/v1/messages', {
101
+ method: 'POST',
102
+ headers: {
103
+ 'Content-Type': 'application/json',
104
+ 'x-api-key': options.apiKey,
105
+ 'anthropic-version': '2023-06-01',
106
+ },
107
+ body: JSON.stringify({
108
+ model: options.model || 'claude-sonnet-4-20250514',
109
+ max_tokens: options.maxTokens ?? 1024,
110
+ system: options.system,
111
+ messages: [{ role: 'user', content: prompt }],
112
+ }),
113
+ });
114
+
115
+ const data = (await apiResponse.json()) as ClaudeApiResult;
116
+ if (!apiResponse.ok) {
117
+ throw new Error(data?.error?.message || `Claude API error: ${apiResponse.status}`);
118
+ }
119
+
120
+ const text = getClaudeTextContent(data);
121
+ if (!text) {
122
+ throw new Error('No text response returned by Claude API.');
123
+ }
124
+
125
+ setResponse(text);
126
+ return text;
127
+ } catch (err) {
128
+ const message = (err as Error).message || 'Failed to send transcription';
129
+ setError(message);
130
+ return null;
131
+ } finally {
132
+ if (isMountedRef.current) {
133
+ setIsLoading(false);
134
+ }
135
+ }
136
+ },
137
+ [options.apiKey, options.maxTokens, options.model, options.system],
138
+ );
139
+
140
+ const startRecording = useCallback(async () => {
141
+ setError(null);
142
+
143
+ const permissionGranted = await requestMicPermission();
144
+ if (!permissionGranted) {
145
+ setError('Microphone permission not granted.');
146
+ return;
147
+ }
148
+
149
+ try {
150
+ transcriptionRef.current = '';
151
+ setTranscription('');
152
+ await Voice.start(options.language || 'en-US');
153
+ setIsRecording(true);
154
+ } catch (err) {
155
+ const message = (err as Error).message || 'Failed to start voice recording';
156
+ setError(message);
157
+ setIsRecording(false);
158
+ }
159
+ }, [options.language, requestMicPermission]);
160
+
161
+ const stopRecording = useCallback(async () => {
162
+ try {
163
+ await Voice.stop();
164
+ } catch {
165
+ // Ignore stop failures; state is reset below.
166
+ } finally {
167
+ setIsRecording(false);
168
+ }
169
+
170
+ if (options.autoSendOnStop !== false) {
171
+ await sendTranscription();
172
+ }
173
+ }, [options.autoSendOnStop, sendTranscription]);
174
+
175
+ const clearVoiceState = useCallback(() => {
176
+ transcriptionRef.current = '';
177
+ setTranscription('');
178
+ setResponse('');
179
+ setError(null);
180
+ }, []);
181
+
182
+ useEffect(() => {
183
+ isMountedRef.current = true;
184
+
185
+ Voice.onSpeechResults = (event: SpeechResultsEvent) => {
186
+ const latestText = event?.value?.[0]?.trim() || '';
187
+ transcriptionRef.current = latestText;
188
+ setTranscription(latestText);
189
+ };
190
+
191
+ Voice.onSpeechError = (event: SpeechErrorEvent) => {
192
+ const message = event?.error?.message || 'Speech recognition failed.';
193
+ setError(message);
194
+ setIsRecording(false);
195
+ };
196
+
197
+ Voice.onSpeechEnd = () => {
198
+ setIsRecording(false);
199
+ };
200
+
201
+ return () => {
202
+ isMountedRef.current = false;
203
+ Voice.destroy().catch(() => undefined);
204
+ Voice.removeAllListeners();
205
+ };
206
+ }, []);
207
+
208
+ return {
209
+ transcription,
210
+ response,
211
+ isRecording,
212
+ isLoading,
213
+ error,
214
+ startRecording,
215
+ stopRecording,
216
+ sendTranscription,
217
+ clearVoiceState,
218
+ };
219
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  export { useAIChat } from './hooks/useAIChat';
2
2
  export { useAIStream } from './hooks/useAIStream';
3
3
  export { useImageAnalysis } from './hooks/useImageAnalysis';
4
- export { useAIForm } from './hooks/useAIForm';
4
+ export { useAIForm } from './hooks/useAIForm';
5
+ export { useAIVoice } from './hooks/useAIVoice';
6
+ export { useAITranslate } from './hooks/useAITranslate';
7
+ export { useAISummarize } from './hooks/useAISummarize';
8
+ export { useAICode } from './hooks/useAICode';