react-native-ai-hooks 0.3.0 → 0.5.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.
Files changed (37) hide show
  1. package/.github/workflows/ci.yml +34 -0
  2. package/CONTRIBUTING.md +122 -0
  3. package/README.md +73 -20
  4. package/docs/ARCHITECTURE.md +301 -0
  5. package/docs/ARCHITECTURE_GUIDE.md +467 -0
  6. package/docs/IMPLEMENTATION_COMPLETE.md +349 -0
  7. package/docs/README.md +17 -0
  8. package/docs/TECHNICAL_SPECIFICATION.md +748 -0
  9. package/example/App.tsx +95 -0
  10. package/example/README.md +27 -0
  11. package/example/index.js +5 -0
  12. package/example/package.json +22 -0
  13. package/example/src/components/ProviderPicker.tsx +62 -0
  14. package/example/src/context/APIKeysContext.tsx +96 -0
  15. package/example/src/screens/ChatScreen.tsx +205 -0
  16. package/example/src/screens/SettingsScreen.tsx +124 -0
  17. package/example/tsconfig.json +7 -0
  18. package/jest.config.cjs +7 -0
  19. package/jest.setup.ts +28 -0
  20. package/package.json +17 -3
  21. package/src/hooks/__tests__/useAIForm.test.ts +345 -0
  22. package/src/hooks/__tests__/useAIStream.test.ts +427 -0
  23. package/src/hooks/useAIChat.ts +111 -51
  24. package/src/hooks/useAICode.ts +8 -0
  25. package/src/hooks/useAIForm.ts +92 -202
  26. package/src/hooks/useAIStream.ts +114 -58
  27. package/src/hooks/useAISummarize.ts +8 -0
  28. package/src/hooks/useAITranslate.ts +9 -0
  29. package/src/hooks/useAIVoice.ts +8 -0
  30. package/src/hooks/useImageAnalysis.ts +134 -79
  31. package/src/index.ts +25 -1
  32. package/src/types/index.ts +178 -4
  33. package/src/utils/__tests__/fetchWithRetry.test.ts +168 -0
  34. package/src/utils/__tests__/providerFactory.test.ts +493 -0
  35. package/src/utils/fetchWithRetry.ts +100 -0
  36. package/src/utils/index.ts +8 -0
  37. package/src/utils/providerFactory.ts +288 -0
@@ -1,57 +1,12 @@
1
- import { useCallback, useRef, useState } from 'react';
2
-
3
- interface UseAIFormOptions {
4
- apiKey: string;
5
- model?: string;
6
- system?: string;
7
- maxTokens?: number;
8
- temperature?: number;
9
- }
10
-
11
- interface ValidateFieldInput {
12
- fieldName: string;
13
- value: string;
14
- formData?: Record<string, string>;
15
- validationRule?: string;
16
- }
17
-
18
- interface AutocompleteFieldInput {
19
- fieldName: string;
20
- value: string;
21
- formData?: Record<string, string>;
22
- instruction?: string;
23
- maxSuggestions?: number;
24
- }
25
-
26
- interface AIFieldValidation {
27
- isValid: boolean;
28
- feedback: string;
29
- suggestion?: string;
30
- }
1
+ import { useCallback, useRef, useState, useMemo, useEffect } from 'react';
2
+ import type { UseAIFormOptions, UseAIFormReturn, FormValidationRequest, FormValidationResult } from '../types';
3
+ import { createProvider } from '../utils/providerFactory';
31
4
 
32
- interface UseAIFormReturn {
33
- validations: Record<string, AIFieldValidation>;
34
- autocomplete: Record<string, string[]>;
35
- isLoading: boolean;
36
- error: string | null;
37
- validateField: (input: ValidateFieldInput) => Promise<AIFieldValidation | null>;
38
- autocompleteField: (input: AutocompleteFieldInput) => Promise<string[] | null>;
39
- clearFormAI: () => void;
40
- cancelRequests: () => void;
41
- }
42
-
43
- function extractTextContent(data: unknown): string {
44
- const content = (data as { content?: Array<{ type?: string; text?: string }> })?.content;
45
- if (!Array.isArray(content)) {
46
- return '';
47
- }
48
-
49
- return content
50
- .filter(item => item?.type === 'text' && typeof item.text === 'string')
51
- .map(item => item.text as string)
52
- .join('\n')
53
- .trim();
54
- }
5
+ const DEFAULT_MODEL_MAP = {
6
+ anthropic: 'claude-sonnet-4-20250514',
7
+ openai: 'gpt-4',
8
+ gemini: 'gemini-pro',
9
+ };
55
10
 
56
11
  function parseJsonFromText<T>(text: string): T {
57
12
  const trimmed = text.trim();
@@ -60,186 +15,121 @@ function parseJsonFromText<T>(text: string): T {
60
15
  return JSON.parse(candidate) as T;
61
16
  }
62
17
 
18
+ /**
19
+ * Validates form payloads using an AI model and returns structured validation errors.
20
+ *
21
+ * @param options Validation configuration including provider, API key, model, transport
22
+ * settings, and generation controls used when composing validation prompts.
23
+ * @returns Form validation state containing the latest validation result,
24
+ * loading/error indicators, and actions to validate data or clear prior results.
25
+ */
63
26
  export function useAIForm(options: UseAIFormOptions): UseAIFormReturn {
64
- const [validations, setValidations] = useState<Record<string, AIFieldValidation>>({});
65
- const [autocomplete, setAutocomplete] = useState<Record<string, string[]>>({});
66
- const [error, setError] = useState<string | null>(null);
27
+ const [validationResult, setValidationResult] = useState<FormValidationResult | null>(null);
67
28
  const [isLoading, setIsLoading] = useState(false);
29
+ const [error, setError] = useState<string | null>(null);
68
30
 
69
- const validationAbortRef = useRef<AbortController | null>(null);
70
- const autocompleteAbortRef = useRef<AbortController | null>(null);
31
+ const abortControllerRef = useRef<AbortController | null>(null);
32
+ const isMountedRef = useRef(true);
33
+
34
+ const providerConfig = useMemo(
35
+ () => ({
36
+ provider: (options.provider || 'anthropic') as 'anthropic' | 'openai' | 'gemini',
37
+ apiKey: options.apiKey,
38
+ model: options.model || DEFAULT_MODEL_MAP[options.provider || 'anthropic'],
39
+ baseUrl: options.baseUrl,
40
+ timeout: options.timeout,
41
+ maxRetries: options.maxRetries,
42
+ }),
43
+ [options],
44
+ );
71
45
 
72
- const cancelRequests = useCallback(() => {
73
- validationAbortRef.current?.abort();
74
- autocompleteAbortRef.current?.abort();
75
- validationAbortRef.current = null;
76
- autocompleteAbortRef.current = null;
77
- setIsLoading(false);
78
- }, []);
46
+ const provider = useMemo(() => createProvider(providerConfig), [providerConfig]);
79
47
 
80
- const clearFormAI = useCallback(() => {
81
- setValidations({});
82
- setAutocomplete({});
48
+ const clearValidation = useCallback(() => {
49
+ setValidationResult(null);
83
50
  setError(null);
84
51
  }, []);
85
52
 
86
- const sendClaudeRequest = useCallback(
87
- async (prompt: string, signal: AbortSignal) => {
88
- const response = await fetch('https://api.anthropic.com/v1/messages', {
89
- method: 'POST',
90
- headers: {
91
- 'Content-Type': 'application/json',
92
- 'x-api-key': options.apiKey,
93
- 'anthropic-version': '2023-06-01',
94
- },
95
- body: JSON.stringify({
96
- model: options.model || 'claude-sonnet-4-20250514',
97
- max_tokens: options.maxTokens ?? 800,
98
- temperature: options.temperature ?? 0.2,
99
- system:
100
- options.system ||
101
- 'You are a form assistant. Return precise, concise, JSON-only responses with no markdown unless explicitly requested.',
102
- messages: [{ role: 'user', content: prompt }],
103
- }),
104
- signal,
105
- });
106
-
107
- if (!response.ok) {
108
- const errorText = await response.text();
109
- throw new Error(errorText || `Claude API error: ${response.status}`);
53
+ const validateForm = useCallback(
54
+ async (input: FormValidationRequest): Promise<FormValidationResult | null> => {
55
+ if (!input.formData || Object.keys(input.formData).length === 0) {
56
+ setError('Form data is empty');
57
+ return null;
110
58
  }
111
59
 
112
- return response.json();
113
- },
114
- [options],
115
- );
116
-
117
- const validateField = useCallback(
118
- async (input: ValidateFieldInput) => {
119
- validationAbortRef.current?.abort();
120
- const controller = new AbortController();
121
- validationAbortRef.current = controller;
122
-
123
- setIsLoading(true);
124
- setError(null);
125
-
126
- try {
127
- const prompt = [
128
- 'Validate this single form field and return JSON only.',
129
- 'Schema: {"isValid": boolean, "feedback": string, "suggestion": string}',
130
- `Field: ${input.fieldName}`,
131
- `Value: ${input.value}`,
132
- `Rule: ${input.validationRule || 'Use common real-world validation best practices.'}`,
133
- `Other form values: ${JSON.stringify(input.formData || {})}`,
134
- 'If valid, feedback should be short and positive. suggestion can be empty string.',
135
- ].join('\n');
136
-
137
- const data = await sendClaudeRequest(prompt, controller.signal);
138
- const text = extractTextContent(data);
139
-
140
- if (!text) {
141
- throw new Error('No validation content returned from Claude API.');
142
- }
143
-
144
- const parsed = parseJsonFromText<AIFieldValidation>(text);
145
-
146
- if (typeof parsed?.isValid !== 'boolean' || typeof parsed?.feedback !== 'string') {
147
- throw new Error('Invalid validation response format returned by Claude API.');
148
- }
149
-
150
- const result: AIFieldValidation = {
151
- isValid: parsed.isValid,
152
- feedback: parsed.feedback,
153
- suggestion: typeof parsed.suggestion === 'string' ? parsed.suggestion : undefined,
154
- };
155
-
156
- setValidations(prev => ({
157
- ...prev,
158
- [input.fieldName]: result,
159
- }));
160
-
161
- return result;
162
- } catch (err) {
163
- if ((err as Error).name === 'AbortError') {
164
- return null;
165
- }
166
-
167
- const message = (err as Error).message || 'Failed to validate field';
168
- setError(message);
60
+ if (!options.apiKey) {
61
+ setError('Missing API key');
169
62
  return null;
170
- } finally {
171
- validationAbortRef.current = null;
172
- setIsLoading(false);
173
63
  }
174
- },
175
- [sendClaudeRequest],
176
- );
177
-
178
- const autocompleteField = useCallback(
179
- async (input: AutocompleteFieldInput) => {
180
- autocompleteAbortRef.current?.abort();
181
- const controller = new AbortController();
182
- autocompleteAbortRef.current = controller;
183
64
 
184
- setIsLoading(true);
185
65
  setError(null);
66
+ setIsLoading(true);
186
67
 
187
68
  try {
69
+ const schemaText = input.validationSchema ? JSON.stringify(input.validationSchema) : 'Use common validation best practices';
188
70
  const prompt = [
189
- 'Generate form autocomplete suggestions for one field and return JSON only.',
190
- 'Schema: {"suggestions": string[]}',
191
- `Field: ${input.fieldName}`,
192
- `Current value: ${input.value}`,
193
- `Max suggestions: ${input.maxSuggestions ?? 5}`,
194
- `Instruction: ${input.instruction || 'Provide useful, natural completions for this field value.'}`,
195
- `Other form values: ${JSON.stringify(input.formData || {})}`,
196
- 'Suggestions should be unique, concise, and ordered best-first.',
71
+ 'Validate the following form data.',
72
+ 'Return ONLY valid JSON with this schema: {"errors": {"fieldName": "errorMessage"}}',
73
+ 'If all fields are valid, return: {"errors": {}}',
74
+ `Validation schema: ${schemaText}`,
75
+ `Custom instructions: ${input.customInstructions || 'None'}`,
76
+ 'Form data:',
77
+ JSON.stringify(input.formData),
197
78
  ].join('\n');
198
79
 
199
- const data = await sendClaudeRequest(prompt, controller.signal);
200
- const text = extractTextContent(data);
80
+ const aiResponse = await provider.makeRequest({
81
+ prompt,
82
+ options: {
83
+ system:
84
+ options.system ||
85
+ 'You are a form validation assistant. Return ONLY valid JSON responses, no markdown or explanations.',
86
+ temperature: options.temperature ?? 0.2,
87
+ maxTokens: options.maxTokens ?? 800,
88
+ },
89
+ });
90
+
91
+ const parsed = parseJsonFromText<{ errors?: Record<string, string> }>(aiResponse.text);
92
+ const errors = parsed?.errors || {};
93
+ const isValid = Object.keys(errors).length === 0;
94
+
95
+ const result: FormValidationResult = {
96
+ isValid,
97
+ errors,
98
+ raw: parsed,
99
+ };
201
100
 
202
- if (!text) {
203
- throw new Error('No autocomplete content returned from Claude API.');
101
+ if (isMountedRef.current) {
102
+ setValidationResult(result);
204
103
  }
205
-
206
- const parsed = parseJsonFromText<{ suggestions?: unknown }>(text);
207
- const suggestions = Array.isArray(parsed?.suggestions)
208
- ? parsed.suggestions.filter((item): item is string => typeof item === 'string')
209
- : [];
210
-
211
- const limitedSuggestions = suggestions.slice(0, input.maxSuggestions ?? 5);
212
-
213
- setAutocomplete(prev => ({
214
- ...prev,
215
- [input.fieldName]: limitedSuggestions,
216
- }));
217
-
218
- return limitedSuggestions;
104
+ return result;
219
105
  } catch (err) {
220
- if ((err as Error).name === 'AbortError') {
221
- return null;
106
+ if (isMountedRef.current) {
107
+ const message = err instanceof Error ? err.message : 'Failed to validate form';
108
+ setError(message);
222
109
  }
223
-
224
- const message = (err as Error).message || 'Failed to generate autocomplete suggestions';
225
- setError(message);
226
110
  return null;
227
111
  } finally {
228
- autocompleteAbortRef.current = null;
229
- setIsLoading(false);
112
+ if (isMountedRef.current) {
113
+ setIsLoading(false);
114
+ }
230
115
  }
231
116
  },
232
- [sendClaudeRequest],
117
+ [provider, options],
233
118
  );
234
119
 
120
+ useEffect(() => {
121
+ isMountedRef.current = true;
122
+ return () => {
123
+ isMountedRef.current = false;
124
+ abortControllerRef.current?.abort();
125
+ };
126
+ }, []);
127
+
235
128
  return {
236
- validations,
237
- autocomplete,
129
+ validationResult,
238
130
  isLoading,
239
131
  error,
240
- validateField,
241
- autocompleteField,
242
- clearFormAI,
243
- cancelRequests,
132
+ validateForm,
133
+ clearValidation,
244
134
  };
245
135
  }
@@ -1,32 +1,46 @@
1
- import { useCallback, useEffect, useRef, useState } from 'react';
2
-
3
- interface UseAIStreamOptions {
4
- apiKey: string;
5
- model?: string;
6
- system?: string;
7
- maxTokens?: number;
8
- temperature?: number;
9
- }
10
-
11
- interface UseAIStreamReturn {
12
- response: string;
13
- isLoading: boolean;
14
- error: string | null;
15
- streamResponse: (prompt: string) => Promise<void>;
16
- abortStream: () => void;
17
- clearResponse: () => void;
18
- }
19
-
1
+ import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
2
+ import type { UseAIStreamOptions, UseAIStreamReturn } from '../types';
3
+ import { fetchWithRetry } from '../utils/fetchWithRetry';
4
+
5
+ const DEFAULT_MODEL_MAP = {
6
+ anthropic: 'claude-sonnet-4-20250514',
7
+ openai: 'gpt-4',
8
+ gemini: 'gemini-pro',
9
+ };
10
+
11
+ /**
12
+ * Streams AI responses token-by-token and exposes incremental output state.
13
+ *
14
+ * @param options Stream configuration including provider, API key, model, base URL,
15
+ * timeout/retry controls, and generation settings such as system prompt, temperature,
16
+ * and max tokens.
17
+ * @returns Stream state with accumulated response text, loading/error flags, and actions
18
+ * to start streaming, abort an in-flight stream, or clear the current response.
19
+ */
20
20
  export function useAIStream(options: UseAIStreamOptions): UseAIStreamReturn {
21
21
  const [response, setResponse] = useState('');
22
22
  const [isLoading, setIsLoading] = useState(false);
23
23
  const [error, setError] = useState<string | null>(null);
24
+
24
25
  const abortControllerRef = useRef<AbortController | null>(null);
26
+ const isMountedRef = useRef(true);
27
+
28
+ const providerConfig = useMemo(
29
+ () => ({
30
+ provider: options.provider || 'anthropic',
31
+ apiKey: options.apiKey,
32
+ model: options.model || DEFAULT_MODEL_MAP[options.provider || 'anthropic'],
33
+ baseUrl: options.baseUrl,
34
+ }),
35
+ [options],
36
+ );
25
37
 
26
- const abortStream = useCallback(() => {
38
+ const abort = useCallback(() => {
27
39
  abortControllerRef.current?.abort();
28
40
  abortControllerRef.current = null;
29
- setIsLoading(false);
41
+ if (isMountedRef.current) {
42
+ setIsLoading(false);
43
+ }
30
44
  }, []);
31
45
 
32
46
  const clearResponse = useCallback(() => {
@@ -36,7 +50,7 @@ export function useAIStream(options: UseAIStreamOptions): UseAIStreamReturn {
36
50
 
37
51
  const streamResponse = useCallback(
38
52
  async (prompt: string) => {
39
- abortStream();
53
+ abort();
40
54
  setResponse('');
41
55
  setError(null);
42
56
  setIsLoading(true);
@@ -45,43 +59,71 @@ export function useAIStream(options: UseAIStreamOptions): UseAIStreamReturn {
45
59
  abortControllerRef.current = controller;
46
60
 
47
61
  try {
48
- const apiResponse = await fetch('https://api.anthropic.com/v1/messages', {
49
- method: 'POST',
50
- headers: {
51
- 'Content-Type': 'application/json',
52
- 'x-api-key': options.apiKey,
53
- 'anthropic-version': '2023-06-01',
54
- },
55
- body: JSON.stringify({
56
- model: options.model || 'claude-sonnet-4-20250514',
62
+ let url = '';
63
+ let body: Record<string, unknown> = {};
64
+ let headers: Record<string, string> = {
65
+ 'Content-Type': 'application/json',
66
+ };
67
+
68
+ if (providerConfig.provider === 'anthropic') {
69
+ url = `${providerConfig.baseUrl || 'https://api.anthropic.com'}/v1/messages`;
70
+ headers['x-api-key'] = options.apiKey;
71
+ headers['anthropic-version'] = '2023-06-01';
72
+ body = {
73
+ model: providerConfig.model,
74
+ max_tokens: options.maxTokens ?? 1024,
75
+ temperature: options.temperature ?? 0.7,
76
+ stream: true,
77
+ system: options.system,
78
+ messages: [{ role: 'user', content: prompt }],
79
+ };
80
+ } else if (providerConfig.provider === 'openai') {
81
+ url = `${providerConfig.baseUrl || 'https://api.openai.com'}/v1/chat/completions`;
82
+ headers.Authorization = `Bearer ${options.apiKey}`;
83
+ body = {
84
+ model: providerConfig.model,
57
85
  max_tokens: options.maxTokens ?? 1024,
58
86
  temperature: options.temperature ?? 0.7,
59
87
  stream: true,
60
88
  system: options.system,
61
89
  messages: [{ role: 'user', content: prompt }],
62
- }),
63
- signal: controller.signal,
64
- });
90
+ };
91
+ } else {
92
+ throw new Error(`Streaming not supported for provider: ${providerConfig.provider}`);
93
+ }
94
+
95
+ const apiResponse = await fetchWithRetry(
96
+ url,
97
+ {
98
+ method: 'POST',
99
+ headers,
100
+ body: JSON.stringify(body),
101
+ signal: controller.signal,
102
+ },
103
+ {
104
+ timeout: options.timeout || 30000,
105
+ maxRetries: options.maxRetries ?? 3,
106
+ },
107
+ );
65
108
 
66
109
  if (!apiResponse.ok) {
67
110
  const errorText = await apiResponse.text();
68
- throw new Error(errorText || `Claude API error: ${apiResponse.status}`);
111
+ throw new Error(errorText || `API error: ${apiResponse.status}`);
69
112
  }
70
113
 
71
114
  if (!apiResponse.body) {
72
- throw new Error('Streaming is not supported in this environment.');
115
+ throw new Error('Streaming not supported in this environment');
73
116
  }
74
117
 
75
118
  const reader = apiResponse.body.getReader();
76
119
  const decoder = new TextDecoder();
77
120
  let buffer = '';
78
121
 
122
+ // eslint-disable-next-line no-constant-condition
79
123
  while (true) {
80
124
  const { done, value } = await reader.read();
81
125
 
82
- if (done) {
83
- break;
84
- }
126
+ if (done) break;
85
127
 
86
128
  buffer += decoder.decode(value, { stream: true });
87
129
  const lines = buffer.split('\n');
@@ -89,44 +131,58 @@ export function useAIStream(options: UseAIStreamOptions): UseAIStreamReturn {
89
131
 
90
132
  for (const rawLine of lines) {
91
133
  const line = rawLine.trim();
92
-
93
- if (!line || !line.startsWith('data:')) {
94
- continue;
95
- }
134
+ if (!line || !line.startsWith('data:')) continue;
96
135
 
97
136
  const payload = line.slice(5).trim();
98
- if (!payload || payload === '[DONE]') {
99
- continue;
100
- }
137
+ if (!payload || payload === '[DONE]') continue;
101
138
 
102
139
  try {
103
140
  const parsed = JSON.parse(payload);
104
- if (
105
- parsed.type === 'content_block_delta' &&
106
- parsed.delta?.type === 'text_delta' &&
107
- typeof parsed.delta.text === 'string'
108
- ) {
109
- setResponse(prev => prev + parsed.delta.text);
141
+
142
+ if (providerConfig.provider === 'anthropic') {
143
+ if (
144
+ parsed.type === 'content_block_delta' &&
145
+ parsed.delta?.type === 'text_delta' &&
146
+ typeof parsed.delta.text === 'string'
147
+ ) {
148
+ if (isMountedRef.current) {
149
+ setResponse((prev: string) => prev + parsed.delta.text);
150
+ }
151
+ }
152
+ } else if (providerConfig.provider === 'openai') {
153
+ if (
154
+ parsed.choices?.[0]?.delta?.content &&
155
+ typeof parsed.choices[0].delta.content === 'string'
156
+ ) {
157
+ if (isMountedRef.current) {
158
+ setResponse((prev: string) => prev + parsed.choices[0].delta.content);
159
+ }
160
+ }
110
161
  }
111
162
  } catch {
112
- // Ignore malformed stream chunks and keep consuming the stream.
163
+ // Ignore malformed stream chunks
113
164
  }
114
165
  }
115
166
  }
116
167
  } catch (err) {
117
- if ((err as Error).name !== 'AbortError') {
118
- setError((err as Error).message || 'Failed to stream response');
168
+ if (isMountedRef.current && (err as Error).name !== 'AbortError') {
169
+ const message = err instanceof Error ? err.message : 'Failed to stream response';
170
+ setError(message);
119
171
  }
120
172
  } finally {
121
173
  abortControllerRef.current = null;
122
- setIsLoading(false);
174
+ if (isMountedRef.current) {
175
+ setIsLoading(false);
176
+ }
123
177
  }
124
178
  },
125
- [abortStream, options],
179
+ [abort, options, providerConfig],
126
180
  );
127
181
 
128
182
  useEffect(() => {
183
+ isMountedRef.current = true;
129
184
  return () => {
185
+ isMountedRef.current = false;
130
186
  abortControllerRef.current?.abort();
131
187
  };
132
188
  }, []);
@@ -136,7 +192,7 @@ export function useAIStream(options: UseAIStreamOptions): UseAIStreamReturn {
136
192
  isLoading,
137
193
  error,
138
194
  streamResponse,
139
- abortStream,
195
+ abort,
140
196
  clearResponse,
141
197
  };
142
198
  }
@@ -63,6 +63,14 @@ function lengthInstruction(length: SummaryLength): string {
63
63
  return 'Produce a balanced summary in 1-2 paragraphs plus a short bullet list of key takeaways.';
64
64
  }
65
65
 
66
+ /**
67
+ * Summarizes long-form text with selectable output length (short, medium, or long).
68
+ *
69
+ * @param options Summarization configuration including API key, model/system behavior,
70
+ * token/temperature controls, and default summary length.
71
+ * @returns Summarization state with current summary text, selected length,
72
+ * loading/error indicators, and actions to set length, generate summaries, or clear output.
73
+ */
66
74
  export function useAISummarize(options: UseAISummarizeOptions): UseAISummarizeReturn {
67
75
  const [summary, setSummary] = useState('');
68
76
  const [length, setLength] = useState<SummaryLength>(options.defaultLength || 'medium');
@@ -62,6 +62,15 @@ function parseJsonFromText<T>(text: string): T {
62
62
  return JSON.parse(candidate) as T;
63
63
  }
64
64
 
65
+ /**
66
+ * Translates source text into a target language with optional automatic debounced translation.
67
+ *
68
+ * @param options Translation configuration including API key, model/system settings,
69
+ * temperature/token controls, initial target language, and auto-translate behavior.
70
+ * @returns Translation state with source/translated text, detected source language,
71
+ * target language, status/error flags, and actions to update text/language, translate,
72
+ * or reset translation state.
73
+ */
65
74
  export function useAITranslate(options: UseAITranslateOptions): UseAITranslateReturn {
66
75
  const [sourceText, setSourceText] = useState('');
67
76
  const [translatedText, setTranslatedText] = useState('');
@@ -58,6 +58,14 @@ function getClaudeTextContent(data: unknown): string {
58
58
  .trim();
59
59
  }
60
60
 
61
+ /**
62
+ * Handles speech-to-text capture and optional AI response generation from transcribed speech.
63
+ *
64
+ * @param options Voice hook configuration including API key, model settings, recognition
65
+ * language, and whether transcription should be auto-sent after recording stops.
66
+ * @returns Voice interaction state with transcription/response values, recording and loading
67
+ * indicators, error state, and actions to start/stop recording, send transcription, and clear state.
68
+ */
61
69
  export function useAIVoice(options: UseAIVoiceOptions): UseAIVoiceReturn {
62
70
  const [transcription, setTranscription] = useState('');
63
71
  const [response, setResponse] = useState('');