react-voice-action-router 1.0.3 → 1.0.4

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/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
- import { V as VoiceProviderProps, a as VoiceControlState, b as VoiceCommand } from './index-DyZ8YKh9.mjs';
3
- export { L as LLMAdapter, c as createOpenAIAdapter } from './index-DyZ8YKh9.mjs';
2
+ import { V as VoiceProviderProps, a as VoiceControlState, b as VoiceCommand, L as LLMAdapter } from './index-DyZ8YKh9.mjs';
3
+ export { c as createOpenAIAdapter } from './index-DyZ8YKh9.mjs';
4
4
 
5
5
  interface VoiceContextValue extends VoiceControlState {
6
6
  register: (cmd: VoiceCommand) => void;
@@ -12,4 +12,31 @@ declare const useVoiceContext: () => VoiceContextValue;
12
12
 
13
13
  declare const useVoiceCommand: (command: VoiceCommand) => void;
14
14
 
15
- export { VoiceCommand, VoiceControlProvider, VoiceControlState, useVoiceCommand, useVoiceContext };
15
+ interface GeminiConfig {
16
+ apiKey: string;
17
+ /**
18
+ * @default "gemini-1.5-flash"
19
+ */
20
+ model?: string;
21
+ /**
22
+ * System instruction to guide the style of response (Optional)
23
+ */
24
+ systemInstruction?: string;
25
+ }
26
+ declare const createGeminiAdapter: (config: GeminiConfig) => LLMAdapter;
27
+
28
+ interface ClaudeConfig {
29
+ apiKey: string;
30
+ /**
31
+ * @default "claude-3-5-sonnet-20240620"
32
+ */
33
+ model?: string;
34
+ /**
35
+ * Optional custom endpoint if using a proxy (Recommended for CORS)
36
+ * @default "https://api.anthropic.com/v1/messages"
37
+ */
38
+ endpoint?: string;
39
+ }
40
+ declare const createClaudeAdapter: (config: ClaudeConfig) => LLMAdapter;
41
+
42
+ export { LLMAdapter, VoiceCommand, VoiceControlProvider, VoiceControlState, createClaudeAdapter, createGeminiAdapter, useVoiceCommand, useVoiceContext };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
- import { V as VoiceProviderProps, a as VoiceControlState, b as VoiceCommand } from './index-DyZ8YKh9.js';
3
- export { L as LLMAdapter, c as createOpenAIAdapter } from './index-DyZ8YKh9.js';
2
+ import { V as VoiceProviderProps, a as VoiceControlState, b as VoiceCommand, L as LLMAdapter } from './index-DyZ8YKh9.js';
3
+ export { c as createOpenAIAdapter } from './index-DyZ8YKh9.js';
4
4
 
5
5
  interface VoiceContextValue extends VoiceControlState {
6
6
  register: (cmd: VoiceCommand) => void;
@@ -12,4 +12,31 @@ declare const useVoiceContext: () => VoiceContextValue;
12
12
 
13
13
  declare const useVoiceCommand: (command: VoiceCommand) => void;
14
14
 
15
- export { VoiceCommand, VoiceControlProvider, VoiceControlState, useVoiceCommand, useVoiceContext };
15
+ interface GeminiConfig {
16
+ apiKey: string;
17
+ /**
18
+ * @default "gemini-1.5-flash"
19
+ */
20
+ model?: string;
21
+ /**
22
+ * System instruction to guide the style of response (Optional)
23
+ */
24
+ systemInstruction?: string;
25
+ }
26
+ declare const createGeminiAdapter: (config: GeminiConfig) => LLMAdapter;
27
+
28
+ interface ClaudeConfig {
29
+ apiKey: string;
30
+ /**
31
+ * @default "claude-3-5-sonnet-20240620"
32
+ */
33
+ model?: string;
34
+ /**
35
+ * Optional custom endpoint if using a proxy (Recommended for CORS)
36
+ * @default "https://api.anthropic.com/v1/messages"
37
+ */
38
+ endpoint?: string;
39
+ }
40
+ declare const createClaudeAdapter: (config: ClaudeConfig) => LLMAdapter;
41
+
42
+ export { LLMAdapter, VoiceCommand, VoiceControlProvider, VoiceControlState, createClaudeAdapter, createGeminiAdapter, useVoiceCommand, useVoiceContext };
package/dist/index.js CHANGED
@@ -150,7 +150,104 @@ var createOpenAIAdapter = (config) => {
150
150
  };
151
151
  };
152
152
 
153
+ // src/adapters/gemini.ts
154
+ var createGeminiAdapter = (config) => {
155
+ return async (transcript, commands) => {
156
+ var _a, _b, _c, _d, _e;
157
+ const commandList = commands.map((c) => `- "${c.id}": ${c.description}`).join("\n");
158
+ const prompt = `
159
+ You are a voice command router.
160
+ The user said: "${transcript}"
161
+
162
+ Available commands:
163
+ ${commandList}
164
+
165
+ Rules:
166
+ 1. Return ONLY the JSON object. No markdown, no explanation.
167
+ 2. Format: { "commandId": "id_here" } or { "commandId": null } if no match.
168
+ `;
169
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${config.model || "gemini-1.5-flash"}:generateContent?key=${config.apiKey}`;
170
+ try {
171
+ const response = await fetch(url, {
172
+ method: "POST",
173
+ headers: {
174
+ "Content-Type": "application/json"
175
+ },
176
+ body: JSON.stringify({
177
+ contents: [
178
+ {
179
+ parts: [{ text: prompt }]
180
+ }
181
+ ]
182
+ })
183
+ });
184
+ if (!response.ok) {
185
+ throw new Error(`Gemini API Error: ${response.statusText}`);
186
+ }
187
+ const data = await response.json();
188
+ const textResponse = (_e = (_d = (_c = (_b = (_a = data.candidates) == null ? void 0 : _a[0]) == null ? void 0 : _b.content) == null ? void 0 : _c.parts) == null ? void 0 : _d[0]) == null ? void 0 : _e.text;
189
+ if (!textResponse) return { commandId: null };
190
+ const cleanJson = textResponse.replace(/```json|```/g, "").trim();
191
+ return JSON.parse(cleanJson);
192
+ } catch (error) {
193
+ console.error("Error routing with Gemini:", error);
194
+ return { commandId: null };
195
+ }
196
+ };
197
+ };
198
+
199
+ // src/adapters/claude.ts
200
+ var createClaudeAdapter = (config) => {
201
+ return async (transcript, commands) => {
202
+ var _a, _b;
203
+ const endpoint = config.endpoint || "https://api.anthropic.com/v1/messages";
204
+ const model = config.model || "claude-3-5-sonnet-20240620";
205
+ const commandList = commands.map((c) => `- "${c.id}": ${c.description}`).join("\n");
206
+ const systemPrompt = `
207
+ You are a voice command router.
208
+ Available commands:
209
+ ${commandList}
210
+
211
+ Rules:
212
+ 1. Analyze the user's transcript.
213
+ 2. Return a JSON object: { "commandId": "id" } or { "commandId": null }.
214
+ 3. No markdown, no conversational text. ONLY JSON.
215
+ `;
216
+ try {
217
+ const response = await fetch(endpoint, {
218
+ method: "POST",
219
+ headers: {
220
+ "x-api-key": config.apiKey,
221
+ "anthropic-version": "2023-06-01",
222
+ "content-type": "application/json"
223
+ // 'dangerously-allow-browser': 'true' // Anthropic client SDK uses this, but fetch doesn't need it.
224
+ },
225
+ body: JSON.stringify({
226
+ model,
227
+ max_tokens: 100,
228
+ system: systemPrompt,
229
+ messages: [{ role: "user", content: transcript }]
230
+ })
231
+ });
232
+ if (!response.ok) {
233
+ const err = await response.text();
234
+ throw new Error(`Claude API Error: ${response.status} - ${err}`);
235
+ }
236
+ const data = await response.json();
237
+ const textResponse = (_b = (_a = data.content) == null ? void 0 : _a[0]) == null ? void 0 : _b.text;
238
+ if (!textResponse) return { commandId: null };
239
+ const cleanJson = textResponse.replace(/```json|```/g, "").trim();
240
+ return JSON.parse(cleanJson);
241
+ } catch (error) {
242
+ console.error("Error routing with Claude:", error);
243
+ return { commandId: null };
244
+ }
245
+ };
246
+ };
247
+
153
248
  exports.VoiceControlProvider = VoiceControlProvider;
249
+ exports.createClaudeAdapter = createClaudeAdapter;
250
+ exports.createGeminiAdapter = createGeminiAdapter;
154
251
  exports.createOpenAIAdapter = createOpenAIAdapter;
155
252
  exports.useVoiceCommand = useVoiceCommand;
156
253
  exports.useVoiceContext = useVoiceContext;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/VoiceContext.tsx","../src/hooks/useVoiceCommand.ts","../src/core/prompt.ts","../src/adapters/openai.ts"],"names":["createContext","useRef","useState","useCallback","jsx","useContext","useEffect"],"mappings":"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","file":"index.js","sourcesContent":["import React, { createContext, useContext, useState, useCallback, useRef } from 'react';\nimport { VoiceCommand, VoiceControlState, VoiceProviderProps } from '../types';\n\ninterface VoiceContextValue extends VoiceControlState {\n register: (cmd: VoiceCommand) => void;\n unregister: (id: string) => void;\n processTranscript: (text: string) => Promise<void>;\n}\n\nconst VoiceContext = createContext<VoiceContextValue | null>(null);\n\nexport const VoiceControlProvider: React.FC<VoiceProviderProps> = ({ \n children, \n adapter \n}) => {\n // THE REGISTRY: A Map ensures O(1) lookups and prevents duplicate IDs\n const commandsRef = useRef<Map<string, VoiceCommand>>(new Map());\n \n // UI STATE\n const [state, setState] = useState<VoiceControlState>({\n isListening: false,\n isProcessing: false,\n lastTranscript: null,\n activeCommands: [],\n });\n\n // 1. REGISTRATION LOGIC\n const register = useCallback((cmd: VoiceCommand) => {\n commandsRef.current.set(cmd.id, cmd);\n // Update generic state for debugging UI\n setState(prev => ({ ...prev, activeCommands: Array.from(commandsRef.current.values()) }));\n }, []);\n\n const unregister = useCallback((id: string) => {\n commandsRef.current.delete(id);\n setState(prev => ({ ...prev, activeCommands: Array.from(commandsRef.current.values()) }));\n }, []);\n\n // 2. THE ROUTER LOGIC\n const processTranscript = useCallback(async (transcript: string) => {\n const cleanText = transcript.trim().toLowerCase();\n setState(prev => ({ ...prev, isProcessing: true, lastTranscript: cleanText }));\n\n const allCommands = Array.from(commandsRef.current.values());\n\n // PHASE 1: EXACT MATCH (0ms Latency)\n const exactMatch = allCommands.find(c => c.phrase?.toLowerCase() === cleanText);\n \n if (exactMatch) {\n console.log(`⚡ Instant Match: \"${cleanText}\" -> ${exactMatch.id}`);\n exactMatch.action();\n setState(prev => ({ ...prev, isProcessing: false }));\n return;\n }\n\n // PHASE 2: FUZZY MATCH (AI Adapter)\n try {\n console.log(`🤖 AI Routing: \"${cleanText}\"...`);\n \n // We strip the 'action' function before sending to the AI\n const commandListForAI = allCommands.map(({ id, description, phrase }) => ({ \n id, \n description, \n phrase \n }));\n\n const result = await adapter(cleanText, commandListForAI);\n\n if (result.commandId) {\n const cmd = commandsRef.current.get(result.commandId);\n if (cmd) {\n console.log(`✅ Matched: ${cmd.id}`);\n cmd.action();\n } else {\n console.warn(`⚠️ Adapter returned unknown ID: ${result.commandId}`);\n }\n }\n } catch (error) {\n console.error(\"Adapter Error:\", error);\n } finally {\n setState(prev => ({ ...prev, isProcessing: false }));\n }\n }, [adapter]);\n\n return (\n <VoiceContext.Provider value={{ ...state, register, unregister, processTranscript }}>\n {children}\n </VoiceContext.Provider>\n );\n};\n\nexport const useVoiceContext = () => {\n const ctx = useContext(VoiceContext);\n if (!ctx) throw new Error(\"useVoiceContext must be used within a VoiceControlProvider\");\n return ctx;\n};","import { useEffect, useRef } from \"react\";\nimport { useVoiceContext } from \"../components/VoiceContext\";\nimport { VoiceCommand } from \"../types\";\n\nexport const useVoiceCommand = (command: VoiceCommand) => {\n const { register, unregister } = useVoiceContext();\n\n // 1. Keep a \"Ref\" to the latest command.\n // This allows the action function to change (e.g. updating state)\n // WITHOUT forcing us to unregister/re-register the command.\n const commandRef = useRef(command);\n\n // Always update the ref on every render\n useEffect(() => {\n commandRef.current = command;\n });\n\n useEffect(() => {\n // 2. Register a \"Proxy Command\"\n // Instead of registering the raw command, we register a proxy\n // that always calls the LATEST version stored in the ref.\n const proxyCommand: VoiceCommand = {\n id: command.id,\n description: command.description,\n phrase: command.phrase,\n action: () => {\n // When triggered, execute whatever the current action is\n commandRef.current.action();\n },\n };\n\n register(proxyCommand);\n\n return () => {\n unregister(proxyCommand.id);\n };\n // 3. Only re-register if the ID changes.\n // We intentionally ignore 'command' or 'command.action' changes here.\n }, [register, unregister, command.id]);\n};\n","import { VoiceCommand } from '../types';\n\n/**\n * Generates the System Prompt for the LLM.\n * This ensures consistent behavior across different AI providers.\n */\nexport const createSystemPrompt = (commands: Omit<VoiceCommand, 'action'>[]) => {\n // Format the list of commands for the AI to read\n const commandList = commands.map(cmd =>\n `- ID: \"${cmd.id}\" | Description: \"${cmd.description}\"`\n ).join('\\n');\n\n return `\nYou are a precise Voice Command Router.\nYour goal is to map the user's spoken input to the correct Command ID from the list below.\n\nRULES:\n1. Analyze the user's input and find the intent.\n2. Match it to the command with the most relevant \"Description\".\n3. Use fuzzy matching (e.g., \"Dark mode\" matches \"Toggle Theme\").\n4. If NO command matches the intent, return null.\n5. IMPORTANT: Output ONLY valid JSON. Do not include markdown formatting.\n\nAVAILABLE COMMANDS:\n${commandList}\n\nRESPONSE FORMAT:\n{ \"commandId\": \"string_id_or_null\" }\n`;\n};\n\n/**\n * Standardizes how the user's voice transcript is presented to the AI.\n */\nexport const createUserPrompt = (transcript: string) => {\n return `User Input: \"${transcript}\"`;\n};","import { LLMAdapter } from '../types';\nimport { createSystemPrompt } from '../core/prompt';\n\ninterface OpenAIConfig {\n apiKey: string;\n /** @default \"gpt-4o-mini\" */\n model?: string;\n}\n\n/**\n * A Factory that creates an Adapter for OpenAI.\n * Users call this: createOpenAIAdapter({ apiKey: '...' })\n */\nexport const createOpenAIAdapter = (config: OpenAIConfig): LLMAdapter => {\n return async (transcript, commands) => {\n\n // 1. Generate the optimized system instructions\n const systemPrompt = createSystemPrompt(commands);\n\n // 2. Call the API\n const response = await fetch('https://api.openai.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${config.apiKey}`\n },\n body: JSON.stringify({\n model: config.model || \"gpt-4o-mini\",\n messages: [\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: transcript }\n ],\n temperature: 0, // Deterministic results\n response_format: { type: \"json_object\" } // Force JSON mode\n })\n });\n\n if (!response.ok) {\n throw new Error(`OpenAI Adapter Failed: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n // 3. Parse the result\n try {\n const parsed = JSON.parse(data.choices[0].message.content);\n return { commandId: parsed.commandId };\n } catch (e) {\n console.error(\"Failed to parse LLM response\", e);\n return { commandId: null };\n }\n };\n};"]}
1
+ {"version":3,"sources":["../src/components/VoiceContext.tsx","../src/hooks/useVoiceCommand.ts","../src/core/prompt.ts","../src/adapters/openai.ts","../src/adapters/gemini.ts","../src/adapters/claude.ts"],"names":["createContext","useRef","useState","useCallback","jsx","useContext","useEffect"],"mappings":"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","file":"index.js","sourcesContent":["import React, { createContext, useContext, useState, useCallback, useRef } from 'react';\nimport { VoiceCommand, VoiceControlState, VoiceProviderProps } from '../types';\n\ninterface VoiceContextValue extends VoiceControlState {\n register: (cmd: VoiceCommand) => void;\n unregister: (id: string) => void;\n processTranscript: (text: string) => Promise<void>;\n}\n\nconst VoiceContext = createContext<VoiceContextValue | null>(null);\n\nexport const VoiceControlProvider: React.FC<VoiceProviderProps> = ({ \n children, \n adapter \n}) => {\n // THE REGISTRY: A Map ensures O(1) lookups and prevents duplicate IDs\n const commandsRef = useRef<Map<string, VoiceCommand>>(new Map());\n \n // UI STATE\n const [state, setState] = useState<VoiceControlState>({\n isListening: false,\n isProcessing: false,\n lastTranscript: null,\n activeCommands: [],\n });\n\n // 1. REGISTRATION LOGIC\n const register = useCallback((cmd: VoiceCommand) => {\n commandsRef.current.set(cmd.id, cmd);\n // Update generic state for debugging UI\n setState(prev => ({ ...prev, activeCommands: Array.from(commandsRef.current.values()) }));\n }, []);\n\n const unregister = useCallback((id: string) => {\n commandsRef.current.delete(id);\n setState(prev => ({ ...prev, activeCommands: Array.from(commandsRef.current.values()) }));\n }, []);\n\n // 2. THE ROUTER LOGIC\n const processTranscript = useCallback(async (transcript: string) => {\n const cleanText = transcript.trim().toLowerCase();\n setState(prev => ({ ...prev, isProcessing: true, lastTranscript: cleanText }));\n\n const allCommands = Array.from(commandsRef.current.values());\n\n // PHASE 1: EXACT MATCH (0ms Latency)\n const exactMatch = allCommands.find(c => c.phrase?.toLowerCase() === cleanText);\n \n if (exactMatch) {\n console.log(`⚡ Instant Match: \"${cleanText}\" -> ${exactMatch.id}`);\n exactMatch.action();\n setState(prev => ({ ...prev, isProcessing: false }));\n return;\n }\n\n // PHASE 2: FUZZY MATCH (AI Adapter)\n try {\n console.log(`🤖 AI Routing: \"${cleanText}\"...`);\n \n // We strip the 'action' function before sending to the AI\n const commandListForAI = allCommands.map(({ id, description, phrase }) => ({ \n id, \n description, \n phrase \n }));\n\n const result = await adapter(cleanText, commandListForAI);\n\n if (result.commandId) {\n const cmd = commandsRef.current.get(result.commandId);\n if (cmd) {\n console.log(`✅ Matched: ${cmd.id}`);\n cmd.action();\n } else {\n console.warn(`⚠️ Adapter returned unknown ID: ${result.commandId}`);\n }\n }\n } catch (error) {\n console.error(\"Adapter Error:\", error);\n } finally {\n setState(prev => ({ ...prev, isProcessing: false }));\n }\n }, [adapter]);\n\n return (\n <VoiceContext.Provider value={{ ...state, register, unregister, processTranscript }}>\n {children}\n </VoiceContext.Provider>\n );\n};\n\nexport const useVoiceContext = () => {\n const ctx = useContext(VoiceContext);\n if (!ctx) throw new Error(\"useVoiceContext must be used within a VoiceControlProvider\");\n return ctx;\n};","import { useEffect, useRef } from \"react\";\nimport { useVoiceContext } from \"../components/VoiceContext\";\nimport { VoiceCommand } from \"../types\";\n\nexport const useVoiceCommand = (command: VoiceCommand) => {\n const { register, unregister } = useVoiceContext();\n\n // 1. Keep a \"Ref\" to the latest command.\n // This allows the action function to change (e.g. updating state)\n // WITHOUT forcing us to unregister/re-register the command.\n const commandRef = useRef(command);\n\n // Always update the ref on every render\n useEffect(() => {\n commandRef.current = command;\n });\n\n useEffect(() => {\n // 2. Register a \"Proxy Command\"\n // Instead of registering the raw command, we register a proxy\n // that always calls the LATEST version stored in the ref.\n const proxyCommand: VoiceCommand = {\n id: command.id,\n description: command.description,\n phrase: command.phrase,\n action: () => {\n // When triggered, execute whatever the current action is\n commandRef.current.action();\n },\n };\n\n register(proxyCommand);\n\n return () => {\n unregister(proxyCommand.id);\n };\n // 3. Only re-register if the ID changes.\n // We intentionally ignore 'command' or 'command.action' changes here.\n }, [register, unregister, command.id]);\n};\n","import { VoiceCommand } from '../types';\n\n/**\n * Generates the System Prompt for the LLM.\n * This ensures consistent behavior across different AI providers.\n */\nexport const createSystemPrompt = (commands: Omit<VoiceCommand, 'action'>[]) => {\n // Format the list of commands for the AI to read\n const commandList = commands.map(cmd =>\n `- ID: \"${cmd.id}\" | Description: \"${cmd.description}\"`\n ).join('\\n');\n\n return `\nYou are a precise Voice Command Router.\nYour goal is to map the user's spoken input to the correct Command ID from the list below.\n\nRULES:\n1. Analyze the user's input and find the intent.\n2. Match it to the command with the most relevant \"Description\".\n3. Use fuzzy matching (e.g., \"Dark mode\" matches \"Toggle Theme\").\n4. If NO command matches the intent, return null.\n5. IMPORTANT: Output ONLY valid JSON. Do not include markdown formatting.\n\nAVAILABLE COMMANDS:\n${commandList}\n\nRESPONSE FORMAT:\n{ \"commandId\": \"string_id_or_null\" }\n`;\n};\n\n/**\n * Standardizes how the user's voice transcript is presented to the AI.\n */\nexport const createUserPrompt = (transcript: string) => {\n return `User Input: \"${transcript}\"`;\n};","import { LLMAdapter } from '../types';\nimport { createSystemPrompt } from '../core/prompt';\n\ninterface OpenAIConfig {\n apiKey: string;\n /** @default \"gpt-4o-mini\" */\n model?: string;\n}\n\n/**\n * A Factory that creates an Adapter for OpenAI.\n * Users call this: createOpenAIAdapter({ apiKey: '...' })\n */\nexport const createOpenAIAdapter = (config: OpenAIConfig): LLMAdapter => {\n return async (transcript, commands) => {\n\n // 1. Generate the optimized system instructions\n const systemPrompt = createSystemPrompt(commands);\n\n // 2. Call the API\n const response = await fetch('https://api.openai.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${config.apiKey}`\n },\n body: JSON.stringify({\n model: config.model || \"gpt-4o-mini\",\n messages: [\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: transcript }\n ],\n temperature: 0, // Deterministic results\n response_format: { type: \"json_object\" } // Force JSON mode\n })\n });\n\n if (!response.ok) {\n throw new Error(`OpenAI Adapter Failed: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n // 3. Parse the result\n try {\n const parsed = JSON.parse(data.choices[0].message.content);\n return { commandId: parsed.commandId };\n } catch (e) {\n console.error(\"Failed to parse LLM response\", e);\n return { commandId: null };\n }\n };\n};","import { LLMAdapter } from \"../types\";\n\ninterface GeminiConfig {\n apiKey: string;\n /**\n * @default \"gemini-1.5-flash\"\n */\n model?: string;\n /**\n * System instruction to guide the style of response (Optional)\n */\n systemInstruction?: string;\n}\n\nexport const createGeminiAdapter = (config: GeminiConfig): LLMAdapter => {\n return async (transcript, commands) => {\n // 1. Prepare the System Prompt\n // We explain the task to Gemini and list the available commands\n const commandList = commands\n .map((c) => `- \"${c.id}\": ${c.description}`)\n .join(\"\\n\");\n\n const prompt = `\n You are a voice command router.\n The user said: \"${transcript}\"\n\n Available commands:\n ${commandList}\n\n Rules:\n 1. Return ONLY the JSON object. No markdown, no explanation.\n 2. Format: { \"commandId\": \"id_here\" } or { \"commandId\": null } if no match.\n `;\n\n // 2. Call Google Gemini API\n const url = `https://generativelanguage.googleapis.com/v1beta/models/${\n config.model || \"gemini-1.5-flash\"\n }:generateContent?key=${config.apiKey}`;\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n contents: [\n {\n parts: [{ text: prompt }],\n },\n ],\n }),\n });\n\n if (!response.ok) {\n throw new Error(`Gemini API Error: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n // 3. Parse Response\n // Gemini returns nested objects: candidates[0].content.parts[0].text\n const textResponse = data.candidates?.[0]?.content?.parts?.[0]?.text;\n\n if (!textResponse) return { commandId: null };\n\n // Clean up markdown code blocks if Gemini adds them (e.g. ```json ... ```)\n const cleanJson = textResponse.replace(/```json|```/g, \"\").trim();\n\n return JSON.parse(cleanJson);\n } catch (error) {\n console.error(\"Error routing with Gemini:\", error);\n return { commandId: null };\n }\n };\n};\n","import { LLMAdapter } from \"../types\";\n\ninterface ClaudeConfig {\n apiKey: string;\n /**\n * @default \"claude-3-5-sonnet-20240620\"\n */\n model?: string;\n /**\n * Optional custom endpoint if using a proxy (Recommended for CORS)\n * @default \"https://api.anthropic.com/v1/messages\"\n */\n endpoint?: string;\n}\n\nexport const createClaudeAdapter = (config: ClaudeConfig): LLMAdapter => {\n return async (transcript, commands) => {\n const endpoint = config.endpoint || \"https://api.anthropic.com/v1/messages\";\n const model = config.model || \"claude-3-5-sonnet-20240620\";\n\n // 1. Prepare Command List\n const commandList = commands\n .map((c) => `- \"${c.id}\": ${c.description}`)\n .join(\"\\n\");\n\n const systemPrompt = `\n You are a voice command router.\n Available commands:\n ${commandList}\n\n Rules:\n 1. Analyze the user's transcript.\n 2. Return a JSON object: { \"commandId\": \"id\" } or { \"commandId\": null }.\n 3. No markdown, no conversational text. ONLY JSON.\n `;\n\n try {\n // 2. Call Anthropic API\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers: {\n \"x-api-key\": config.apiKey,\n \"anthropic-version\": \"2023-06-01\",\n \"content-type\": \"application/json\",\n // 'dangerously-allow-browser': 'true' // Anthropic client SDK uses this, but fetch doesn't need it.\n },\n body: JSON.stringify({\n model: model,\n max_tokens: 100,\n system: systemPrompt,\n messages: [{ role: \"user\", content: transcript }],\n }),\n });\n\n if (!response.ok) {\n const err = await response.text();\n throw new Error(`Claude API Error: ${response.status} - ${err}`);\n }\n\n const data = await response.json();\n\n // 3. Parse Response\n // Claude returns: content: [ { type: 'text', text: '...' } ]\n const textResponse = data.content?.[0]?.text;\n\n if (!textResponse) return { commandId: null };\n\n // Clean up potential markdown formatting\n const cleanJson = textResponse.replace(/```json|```/g, \"\").trim();\n return JSON.parse(cleanJson);\n } catch (error) {\n console.error(\"Error routing with Claude:\", error);\n return { commandId: null };\n }\n };\n};\n"]}
package/dist/index.mjs CHANGED
@@ -148,6 +148,101 @@ var createOpenAIAdapter = (config) => {
148
148
  };
149
149
  };
150
150
 
151
- export { VoiceControlProvider, createOpenAIAdapter, useVoiceCommand, useVoiceContext };
151
+ // src/adapters/gemini.ts
152
+ var createGeminiAdapter = (config) => {
153
+ return async (transcript, commands) => {
154
+ var _a, _b, _c, _d, _e;
155
+ const commandList = commands.map((c) => `- "${c.id}": ${c.description}`).join("\n");
156
+ const prompt = `
157
+ You are a voice command router.
158
+ The user said: "${transcript}"
159
+
160
+ Available commands:
161
+ ${commandList}
162
+
163
+ Rules:
164
+ 1. Return ONLY the JSON object. No markdown, no explanation.
165
+ 2. Format: { "commandId": "id_here" } or { "commandId": null } if no match.
166
+ `;
167
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${config.model || "gemini-1.5-flash"}:generateContent?key=${config.apiKey}`;
168
+ try {
169
+ const response = await fetch(url, {
170
+ method: "POST",
171
+ headers: {
172
+ "Content-Type": "application/json"
173
+ },
174
+ body: JSON.stringify({
175
+ contents: [
176
+ {
177
+ parts: [{ text: prompt }]
178
+ }
179
+ ]
180
+ })
181
+ });
182
+ if (!response.ok) {
183
+ throw new Error(`Gemini API Error: ${response.statusText}`);
184
+ }
185
+ const data = await response.json();
186
+ const textResponse = (_e = (_d = (_c = (_b = (_a = data.candidates) == null ? void 0 : _a[0]) == null ? void 0 : _b.content) == null ? void 0 : _c.parts) == null ? void 0 : _d[0]) == null ? void 0 : _e.text;
187
+ if (!textResponse) return { commandId: null };
188
+ const cleanJson = textResponse.replace(/```json|```/g, "").trim();
189
+ return JSON.parse(cleanJson);
190
+ } catch (error) {
191
+ console.error("Error routing with Gemini:", error);
192
+ return { commandId: null };
193
+ }
194
+ };
195
+ };
196
+
197
+ // src/adapters/claude.ts
198
+ var createClaudeAdapter = (config) => {
199
+ return async (transcript, commands) => {
200
+ var _a, _b;
201
+ const endpoint = config.endpoint || "https://api.anthropic.com/v1/messages";
202
+ const model = config.model || "claude-3-5-sonnet-20240620";
203
+ const commandList = commands.map((c) => `- "${c.id}": ${c.description}`).join("\n");
204
+ const systemPrompt = `
205
+ You are a voice command router.
206
+ Available commands:
207
+ ${commandList}
208
+
209
+ Rules:
210
+ 1. Analyze the user's transcript.
211
+ 2. Return a JSON object: { "commandId": "id" } or { "commandId": null }.
212
+ 3. No markdown, no conversational text. ONLY JSON.
213
+ `;
214
+ try {
215
+ const response = await fetch(endpoint, {
216
+ method: "POST",
217
+ headers: {
218
+ "x-api-key": config.apiKey,
219
+ "anthropic-version": "2023-06-01",
220
+ "content-type": "application/json"
221
+ // 'dangerously-allow-browser': 'true' // Anthropic client SDK uses this, but fetch doesn't need it.
222
+ },
223
+ body: JSON.stringify({
224
+ model,
225
+ max_tokens: 100,
226
+ system: systemPrompt,
227
+ messages: [{ role: "user", content: transcript }]
228
+ })
229
+ });
230
+ if (!response.ok) {
231
+ const err = await response.text();
232
+ throw new Error(`Claude API Error: ${response.status} - ${err}`);
233
+ }
234
+ const data = await response.json();
235
+ const textResponse = (_b = (_a = data.content) == null ? void 0 : _a[0]) == null ? void 0 : _b.text;
236
+ if (!textResponse) return { commandId: null };
237
+ const cleanJson = textResponse.replace(/```json|```/g, "").trim();
238
+ return JSON.parse(cleanJson);
239
+ } catch (error) {
240
+ console.error("Error routing with Claude:", error);
241
+ return { commandId: null };
242
+ }
243
+ };
244
+ };
245
+
246
+ export { VoiceControlProvider, createClaudeAdapter, createGeminiAdapter, createOpenAIAdapter, useVoiceCommand, useVoiceContext };
152
247
  //# sourceMappingURL=index.mjs.map
153
248
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/VoiceContext.tsx","../src/hooks/useVoiceCommand.ts","../src/core/prompt.ts","../src/adapters/openai.ts"],"names":["useRef"],"mappings":"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","file":"index.mjs","sourcesContent":["import React, { createContext, useContext, useState, useCallback, useRef } from 'react';\nimport { VoiceCommand, VoiceControlState, VoiceProviderProps } from '../types';\n\ninterface VoiceContextValue extends VoiceControlState {\n register: (cmd: VoiceCommand) => void;\n unregister: (id: string) => void;\n processTranscript: (text: string) => Promise<void>;\n}\n\nconst VoiceContext = createContext<VoiceContextValue | null>(null);\n\nexport const VoiceControlProvider: React.FC<VoiceProviderProps> = ({ \n children, \n adapter \n}) => {\n // THE REGISTRY: A Map ensures O(1) lookups and prevents duplicate IDs\n const commandsRef = useRef<Map<string, VoiceCommand>>(new Map());\n \n // UI STATE\n const [state, setState] = useState<VoiceControlState>({\n isListening: false,\n isProcessing: false,\n lastTranscript: null,\n activeCommands: [],\n });\n\n // 1. REGISTRATION LOGIC\n const register = useCallback((cmd: VoiceCommand) => {\n commandsRef.current.set(cmd.id, cmd);\n // Update generic state for debugging UI\n setState(prev => ({ ...prev, activeCommands: Array.from(commandsRef.current.values()) }));\n }, []);\n\n const unregister = useCallback((id: string) => {\n commandsRef.current.delete(id);\n setState(prev => ({ ...prev, activeCommands: Array.from(commandsRef.current.values()) }));\n }, []);\n\n // 2. THE ROUTER LOGIC\n const processTranscript = useCallback(async (transcript: string) => {\n const cleanText = transcript.trim().toLowerCase();\n setState(prev => ({ ...prev, isProcessing: true, lastTranscript: cleanText }));\n\n const allCommands = Array.from(commandsRef.current.values());\n\n // PHASE 1: EXACT MATCH (0ms Latency)\n const exactMatch = allCommands.find(c => c.phrase?.toLowerCase() === cleanText);\n \n if (exactMatch) {\n console.log(`⚡ Instant Match: \"${cleanText}\" -> ${exactMatch.id}`);\n exactMatch.action();\n setState(prev => ({ ...prev, isProcessing: false }));\n return;\n }\n\n // PHASE 2: FUZZY MATCH (AI Adapter)\n try {\n console.log(`🤖 AI Routing: \"${cleanText}\"...`);\n \n // We strip the 'action' function before sending to the AI\n const commandListForAI = allCommands.map(({ id, description, phrase }) => ({ \n id, \n description, \n phrase \n }));\n\n const result = await adapter(cleanText, commandListForAI);\n\n if (result.commandId) {\n const cmd = commandsRef.current.get(result.commandId);\n if (cmd) {\n console.log(`✅ Matched: ${cmd.id}`);\n cmd.action();\n } else {\n console.warn(`⚠️ Adapter returned unknown ID: ${result.commandId}`);\n }\n }\n } catch (error) {\n console.error(\"Adapter Error:\", error);\n } finally {\n setState(prev => ({ ...prev, isProcessing: false }));\n }\n }, [adapter]);\n\n return (\n <VoiceContext.Provider value={{ ...state, register, unregister, processTranscript }}>\n {children}\n </VoiceContext.Provider>\n );\n};\n\nexport const useVoiceContext = () => {\n const ctx = useContext(VoiceContext);\n if (!ctx) throw new Error(\"useVoiceContext must be used within a VoiceControlProvider\");\n return ctx;\n};","import { useEffect, useRef } from \"react\";\nimport { useVoiceContext } from \"../components/VoiceContext\";\nimport { VoiceCommand } from \"../types\";\n\nexport const useVoiceCommand = (command: VoiceCommand) => {\n const { register, unregister } = useVoiceContext();\n\n // 1. Keep a \"Ref\" to the latest command.\n // This allows the action function to change (e.g. updating state)\n // WITHOUT forcing us to unregister/re-register the command.\n const commandRef = useRef(command);\n\n // Always update the ref on every render\n useEffect(() => {\n commandRef.current = command;\n });\n\n useEffect(() => {\n // 2. Register a \"Proxy Command\"\n // Instead of registering the raw command, we register a proxy\n // that always calls the LATEST version stored in the ref.\n const proxyCommand: VoiceCommand = {\n id: command.id,\n description: command.description,\n phrase: command.phrase,\n action: () => {\n // When triggered, execute whatever the current action is\n commandRef.current.action();\n },\n };\n\n register(proxyCommand);\n\n return () => {\n unregister(proxyCommand.id);\n };\n // 3. Only re-register if the ID changes.\n // We intentionally ignore 'command' or 'command.action' changes here.\n }, [register, unregister, command.id]);\n};\n","import { VoiceCommand } from '../types';\n\n/**\n * Generates the System Prompt for the LLM.\n * This ensures consistent behavior across different AI providers.\n */\nexport const createSystemPrompt = (commands: Omit<VoiceCommand, 'action'>[]) => {\n // Format the list of commands for the AI to read\n const commandList = commands.map(cmd =>\n `- ID: \"${cmd.id}\" | Description: \"${cmd.description}\"`\n ).join('\\n');\n\n return `\nYou are a precise Voice Command Router.\nYour goal is to map the user's spoken input to the correct Command ID from the list below.\n\nRULES:\n1. Analyze the user's input and find the intent.\n2. Match it to the command with the most relevant \"Description\".\n3. Use fuzzy matching (e.g., \"Dark mode\" matches \"Toggle Theme\").\n4. If NO command matches the intent, return null.\n5. IMPORTANT: Output ONLY valid JSON. Do not include markdown formatting.\n\nAVAILABLE COMMANDS:\n${commandList}\n\nRESPONSE FORMAT:\n{ \"commandId\": \"string_id_or_null\" }\n`;\n};\n\n/**\n * Standardizes how the user's voice transcript is presented to the AI.\n */\nexport const createUserPrompt = (transcript: string) => {\n return `User Input: \"${transcript}\"`;\n};","import { LLMAdapter } from '../types';\nimport { createSystemPrompt } from '../core/prompt';\n\ninterface OpenAIConfig {\n apiKey: string;\n /** @default \"gpt-4o-mini\" */\n model?: string;\n}\n\n/**\n * A Factory that creates an Adapter for OpenAI.\n * Users call this: createOpenAIAdapter({ apiKey: '...' })\n */\nexport const createOpenAIAdapter = (config: OpenAIConfig): LLMAdapter => {\n return async (transcript, commands) => {\n\n // 1. Generate the optimized system instructions\n const systemPrompt = createSystemPrompt(commands);\n\n // 2. Call the API\n const response = await fetch('https://api.openai.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${config.apiKey}`\n },\n body: JSON.stringify({\n model: config.model || \"gpt-4o-mini\",\n messages: [\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: transcript }\n ],\n temperature: 0, // Deterministic results\n response_format: { type: \"json_object\" } // Force JSON mode\n })\n });\n\n if (!response.ok) {\n throw new Error(`OpenAI Adapter Failed: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n // 3. Parse the result\n try {\n const parsed = JSON.parse(data.choices[0].message.content);\n return { commandId: parsed.commandId };\n } catch (e) {\n console.error(\"Failed to parse LLM response\", e);\n return { commandId: null };\n }\n };\n};"]}
1
+ {"version":3,"sources":["../src/components/VoiceContext.tsx","../src/hooks/useVoiceCommand.ts","../src/core/prompt.ts","../src/adapters/openai.ts","../src/adapters/gemini.ts","../src/adapters/claude.ts"],"names":["useRef"],"mappings":"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","file":"index.mjs","sourcesContent":["import React, { createContext, useContext, useState, useCallback, useRef } from 'react';\nimport { VoiceCommand, VoiceControlState, VoiceProviderProps } from '../types';\n\ninterface VoiceContextValue extends VoiceControlState {\n register: (cmd: VoiceCommand) => void;\n unregister: (id: string) => void;\n processTranscript: (text: string) => Promise<void>;\n}\n\nconst VoiceContext = createContext<VoiceContextValue | null>(null);\n\nexport const VoiceControlProvider: React.FC<VoiceProviderProps> = ({ \n children, \n adapter \n}) => {\n // THE REGISTRY: A Map ensures O(1) lookups and prevents duplicate IDs\n const commandsRef = useRef<Map<string, VoiceCommand>>(new Map());\n \n // UI STATE\n const [state, setState] = useState<VoiceControlState>({\n isListening: false,\n isProcessing: false,\n lastTranscript: null,\n activeCommands: [],\n });\n\n // 1. REGISTRATION LOGIC\n const register = useCallback((cmd: VoiceCommand) => {\n commandsRef.current.set(cmd.id, cmd);\n // Update generic state for debugging UI\n setState(prev => ({ ...prev, activeCommands: Array.from(commandsRef.current.values()) }));\n }, []);\n\n const unregister = useCallback((id: string) => {\n commandsRef.current.delete(id);\n setState(prev => ({ ...prev, activeCommands: Array.from(commandsRef.current.values()) }));\n }, []);\n\n // 2. THE ROUTER LOGIC\n const processTranscript = useCallback(async (transcript: string) => {\n const cleanText = transcript.trim().toLowerCase();\n setState(prev => ({ ...prev, isProcessing: true, lastTranscript: cleanText }));\n\n const allCommands = Array.from(commandsRef.current.values());\n\n // PHASE 1: EXACT MATCH (0ms Latency)\n const exactMatch = allCommands.find(c => c.phrase?.toLowerCase() === cleanText);\n \n if (exactMatch) {\n console.log(`⚡ Instant Match: \"${cleanText}\" -> ${exactMatch.id}`);\n exactMatch.action();\n setState(prev => ({ ...prev, isProcessing: false }));\n return;\n }\n\n // PHASE 2: FUZZY MATCH (AI Adapter)\n try {\n console.log(`🤖 AI Routing: \"${cleanText}\"...`);\n \n // We strip the 'action' function before sending to the AI\n const commandListForAI = allCommands.map(({ id, description, phrase }) => ({ \n id, \n description, \n phrase \n }));\n\n const result = await adapter(cleanText, commandListForAI);\n\n if (result.commandId) {\n const cmd = commandsRef.current.get(result.commandId);\n if (cmd) {\n console.log(`✅ Matched: ${cmd.id}`);\n cmd.action();\n } else {\n console.warn(`⚠️ Adapter returned unknown ID: ${result.commandId}`);\n }\n }\n } catch (error) {\n console.error(\"Adapter Error:\", error);\n } finally {\n setState(prev => ({ ...prev, isProcessing: false }));\n }\n }, [adapter]);\n\n return (\n <VoiceContext.Provider value={{ ...state, register, unregister, processTranscript }}>\n {children}\n </VoiceContext.Provider>\n );\n};\n\nexport const useVoiceContext = () => {\n const ctx = useContext(VoiceContext);\n if (!ctx) throw new Error(\"useVoiceContext must be used within a VoiceControlProvider\");\n return ctx;\n};","import { useEffect, useRef } from \"react\";\nimport { useVoiceContext } from \"../components/VoiceContext\";\nimport { VoiceCommand } from \"../types\";\n\nexport const useVoiceCommand = (command: VoiceCommand) => {\n const { register, unregister } = useVoiceContext();\n\n // 1. Keep a \"Ref\" to the latest command.\n // This allows the action function to change (e.g. updating state)\n // WITHOUT forcing us to unregister/re-register the command.\n const commandRef = useRef(command);\n\n // Always update the ref on every render\n useEffect(() => {\n commandRef.current = command;\n });\n\n useEffect(() => {\n // 2. Register a \"Proxy Command\"\n // Instead of registering the raw command, we register a proxy\n // that always calls the LATEST version stored in the ref.\n const proxyCommand: VoiceCommand = {\n id: command.id,\n description: command.description,\n phrase: command.phrase,\n action: () => {\n // When triggered, execute whatever the current action is\n commandRef.current.action();\n },\n };\n\n register(proxyCommand);\n\n return () => {\n unregister(proxyCommand.id);\n };\n // 3. Only re-register if the ID changes.\n // We intentionally ignore 'command' or 'command.action' changes here.\n }, [register, unregister, command.id]);\n};\n","import { VoiceCommand } from '../types';\n\n/**\n * Generates the System Prompt for the LLM.\n * This ensures consistent behavior across different AI providers.\n */\nexport const createSystemPrompt = (commands: Omit<VoiceCommand, 'action'>[]) => {\n // Format the list of commands for the AI to read\n const commandList = commands.map(cmd =>\n `- ID: \"${cmd.id}\" | Description: \"${cmd.description}\"`\n ).join('\\n');\n\n return `\nYou are a precise Voice Command Router.\nYour goal is to map the user's spoken input to the correct Command ID from the list below.\n\nRULES:\n1. Analyze the user's input and find the intent.\n2. Match it to the command with the most relevant \"Description\".\n3. Use fuzzy matching (e.g., \"Dark mode\" matches \"Toggle Theme\").\n4. If NO command matches the intent, return null.\n5. IMPORTANT: Output ONLY valid JSON. Do not include markdown formatting.\n\nAVAILABLE COMMANDS:\n${commandList}\n\nRESPONSE FORMAT:\n{ \"commandId\": \"string_id_or_null\" }\n`;\n};\n\n/**\n * Standardizes how the user's voice transcript is presented to the AI.\n */\nexport const createUserPrompt = (transcript: string) => {\n return `User Input: \"${transcript}\"`;\n};","import { LLMAdapter } from '../types';\nimport { createSystemPrompt } from '../core/prompt';\n\ninterface OpenAIConfig {\n apiKey: string;\n /** @default \"gpt-4o-mini\" */\n model?: string;\n}\n\n/**\n * A Factory that creates an Adapter for OpenAI.\n * Users call this: createOpenAIAdapter({ apiKey: '...' })\n */\nexport const createOpenAIAdapter = (config: OpenAIConfig): LLMAdapter => {\n return async (transcript, commands) => {\n\n // 1. Generate the optimized system instructions\n const systemPrompt = createSystemPrompt(commands);\n\n // 2. Call the API\n const response = await fetch('https://api.openai.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${config.apiKey}`\n },\n body: JSON.stringify({\n model: config.model || \"gpt-4o-mini\",\n messages: [\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: transcript }\n ],\n temperature: 0, // Deterministic results\n response_format: { type: \"json_object\" } // Force JSON mode\n })\n });\n\n if (!response.ok) {\n throw new Error(`OpenAI Adapter Failed: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n // 3. Parse the result\n try {\n const parsed = JSON.parse(data.choices[0].message.content);\n return { commandId: parsed.commandId };\n } catch (e) {\n console.error(\"Failed to parse LLM response\", e);\n return { commandId: null };\n }\n };\n};","import { LLMAdapter } from \"../types\";\n\ninterface GeminiConfig {\n apiKey: string;\n /**\n * @default \"gemini-1.5-flash\"\n */\n model?: string;\n /**\n * System instruction to guide the style of response (Optional)\n */\n systemInstruction?: string;\n}\n\nexport const createGeminiAdapter = (config: GeminiConfig): LLMAdapter => {\n return async (transcript, commands) => {\n // 1. Prepare the System Prompt\n // We explain the task to Gemini and list the available commands\n const commandList = commands\n .map((c) => `- \"${c.id}\": ${c.description}`)\n .join(\"\\n\");\n\n const prompt = `\n You are a voice command router.\n The user said: \"${transcript}\"\n\n Available commands:\n ${commandList}\n\n Rules:\n 1. Return ONLY the JSON object. No markdown, no explanation.\n 2. Format: { \"commandId\": \"id_here\" } or { \"commandId\": null } if no match.\n `;\n\n // 2. Call Google Gemini API\n const url = `https://generativelanguage.googleapis.com/v1beta/models/${\n config.model || \"gemini-1.5-flash\"\n }:generateContent?key=${config.apiKey}`;\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n contents: [\n {\n parts: [{ text: prompt }],\n },\n ],\n }),\n });\n\n if (!response.ok) {\n throw new Error(`Gemini API Error: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n // 3. Parse Response\n // Gemini returns nested objects: candidates[0].content.parts[0].text\n const textResponse = data.candidates?.[0]?.content?.parts?.[0]?.text;\n\n if (!textResponse) return { commandId: null };\n\n // Clean up markdown code blocks if Gemini adds them (e.g. ```json ... ```)\n const cleanJson = textResponse.replace(/```json|```/g, \"\").trim();\n\n return JSON.parse(cleanJson);\n } catch (error) {\n console.error(\"Error routing with Gemini:\", error);\n return { commandId: null };\n }\n };\n};\n","import { LLMAdapter } from \"../types\";\n\ninterface ClaudeConfig {\n apiKey: string;\n /**\n * @default \"claude-3-5-sonnet-20240620\"\n */\n model?: string;\n /**\n * Optional custom endpoint if using a proxy (Recommended for CORS)\n * @default \"https://api.anthropic.com/v1/messages\"\n */\n endpoint?: string;\n}\n\nexport const createClaudeAdapter = (config: ClaudeConfig): LLMAdapter => {\n return async (transcript, commands) => {\n const endpoint = config.endpoint || \"https://api.anthropic.com/v1/messages\";\n const model = config.model || \"claude-3-5-sonnet-20240620\";\n\n // 1. Prepare Command List\n const commandList = commands\n .map((c) => `- \"${c.id}\": ${c.description}`)\n .join(\"\\n\");\n\n const systemPrompt = `\n You are a voice command router.\n Available commands:\n ${commandList}\n\n Rules:\n 1. Analyze the user's transcript.\n 2. Return a JSON object: { \"commandId\": \"id\" } or { \"commandId\": null }.\n 3. No markdown, no conversational text. ONLY JSON.\n `;\n\n try {\n // 2. Call Anthropic API\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers: {\n \"x-api-key\": config.apiKey,\n \"anthropic-version\": \"2023-06-01\",\n \"content-type\": \"application/json\",\n // 'dangerously-allow-browser': 'true' // Anthropic client SDK uses this, but fetch doesn't need it.\n },\n body: JSON.stringify({\n model: model,\n max_tokens: 100,\n system: systemPrompt,\n messages: [{ role: \"user\", content: transcript }],\n }),\n });\n\n if (!response.ok) {\n const err = await response.text();\n throw new Error(`Claude API Error: ${response.status} - ${err}`);\n }\n\n const data = await response.json();\n\n // 3. Parse Response\n // Claude returns: content: [ { type: 'text', text: '...' } ]\n const textResponse = data.content?.[0]?.text;\n\n if (!textResponse) return { commandId: null };\n\n // Clean up potential markdown formatting\n const cleanJson = textResponse.replace(/```json|```/g, \"\").trim();\n return JSON.parse(cleanJson);\n } catch (error) {\n console.error(\"Error routing with Claude:\", error);\n return { commandId: null };\n }\n };\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-voice-action-router",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "A headless voice action router for React",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",