react-voice-action-router 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -20
- package/README.md +21 -21
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/index.mjs.map +1 -1
- package/dist/index.d.mts +30 -3
- package/dist/index.d.ts +30 -3
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +96 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -3
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Nouman Ejaz
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nouman Ejaz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/react-voice-action-router)
|
|
6
|
-
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
## Installation
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
npm install react-voice-action-router
|
|
21
|
-
|
|
1
|
+
# React Voice Action Router
|
|
2
|
+
|
|
3
|
+
> Don't build a chatbot. Build a hands-free interface.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/react-voice-action-router)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
[**Read the Documentation**](https://nouman64-cat.github.io/react-voice-action-router/)
|
|
9
|
+
|
|
10
|
+
A headless, latency-first voice intent router for React applications. It bridges natural language speech to your existing React functions using an LLM-Agnostic Adapter Pattern.
|
|
11
|
+
|
|
12
|
+
- **Headless & Zero UI:** We provide the logic hooks; you build the interface (or keep it invisible).
|
|
13
|
+
- **Context-Aware:** The AI only knows about commands on the current screen, reducing costs and hallucinations.
|
|
14
|
+
- **Universal Adapter:** Built-in support for OpenAI, Google Gemini, Anthropic Claude, and local Ollama models.
|
|
15
|
+
- **Latency-First:** Supports instant execution for exact phrases, falling back to AI for fuzzy intents.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install react-voice-action-router
|
|
21
|
+
```
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/prompt.ts","../../src/adapters/openai.ts"],"names":[],"mappings":";;;AAMO,IAAM,kBAAA,GAAqB,CAAC,QAAA,KAA6C;AAE5E,EAAA,MAAM,cAAc,QAAA,CAAS,GAAA;AAAA,IAAI,SAC7B,CAAA,OAAA,EAAU,GAAA,CAAI,EAAE,CAAA,kBAAA,EAAqB,IAAI,WAAW,CAAA,CAAA;AAAA,GACxD,CAAE,KAAK,IAAI,CAAA;AAEX,EAAA,OAAO;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA,EAYT,WAAW;;AAAA;AAAA;AAAA,CAAA;AAKb,CAAA;;;AChBO,IAAM,mBAAA,GAAsB,CAAC,MAAA,KAAqC;AACrE,EAAA,OAAO,OAAO,YAAY,QAAA,KAAa;AAGnC,IAAA,MAAM,YAAA,GAAe,mBAAmB,QAAQ,CAAA;AAGhD,IAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,4CAAA,EAA8C;AAAA,MACvE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACL,cAAA,EAAgB,kBAAA;AAAA,QAChB,eAAA,EAAiB,CAAA,OAAA,EAAU,MAAA,CAAO,MAAM,CAAA;AAAA,OAC5C;AAAA,MACA,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,QACjB,KAAA,EAAO,OAAO,KAAA,IAAS,aAAA;AAAA,QACvB,QAAA,EAAU;AAAA,UACN,EAAE,IAAA,EAAM,QAAA,EAAU,OAAA,EAAS,YAAA,EAAa;AAAA,UACxC,EAAE,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,UAAA;AAAW,SACxC;AAAA,QACA,WAAA,EAAa,CAAA;AAAA;AAAA,QACb,eAAA,EAAiB,EAAE,IAAA,EAAM,aAAA;AAAc;AAAA,OAC1C;AAAA,KACJ,CAAA;AAED,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AACd,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,uBAAA,EAA0B,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,IACnE;AAEA,IAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AAGjC,IAAA,IAAI;AACA,MAAA,MAAM,MAAA,GAAS,KAAK,KAAA,CAAM,IAAA,CAAK,QAAQ,CAAC,CAAA,CAAE,QAAQ,OAAO,CAAA;AACzD,MAAA,OAAO,EAAE,SAAA,EAAW,MAAA,CAAO,SAAA,EAAU;AAAA,IACzC,SAAS,CAAA,EAAG;AACR,MAAA,OAAA,CAAQ,KAAA,CAAM,gCAAgC,CAAC,CAAA;AAC/C,MAAA,OAAO,EAAE,WAAW,IAAA,EAAK;AAAA,IAC7B;AAAA,EACJ,CAAA;AACJ","file":"index.js","sourcesContent":["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/core/prompt.ts","../../src/adapters/openai.ts"],"names":[],"mappings":";;;AAMO,IAAM,kBAAA,GAAqB,CAAC,QAAA,KAA6C;AAE5E,EAAA,MAAM,cAAc,QAAA,CAAS,GAAA;AAAA,IAAI,SAC7B,CAAA,OAAA,EAAU,GAAA,CAAI,EAAE,CAAA,kBAAA,EAAqB,IAAI,WAAW,CAAA,CAAA;AAAA,GACxD,CAAE,KAAK,IAAI,CAAA;AAEX,EAAA,OAAO;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA,EAYT,WAAW;;AAAA;AAAA;AAAA,CAAA;AAKb,CAAA;;;AChBO,IAAM,mBAAA,GAAsB,CAAC,MAAA,KAAqC;AACrE,EAAA,OAAO,OAAO,YAAY,QAAA,KAAa;AAGnC,IAAA,MAAM,YAAA,GAAe,mBAAmB,QAAQ,CAAA;AAGhD,IAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,4CAAA,EAA8C;AAAA,MACvE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACL,cAAA,EAAgB,kBAAA;AAAA,QAChB,eAAA,EAAiB,CAAA,OAAA,EAAU,MAAA,CAAO,MAAM,CAAA;AAAA,OAC5C;AAAA,MACA,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,QACjB,KAAA,EAAO,OAAO,KAAA,IAAS,aAAA;AAAA,QACvB,QAAA,EAAU;AAAA,UACN,EAAE,IAAA,EAAM,QAAA,EAAU,OAAA,EAAS,YAAA,EAAa;AAAA,UACxC,EAAE,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,UAAA;AAAW,SACxC;AAAA,QACA,WAAA,EAAa,CAAA;AAAA;AAAA,QACb,eAAA,EAAiB,EAAE,IAAA,EAAM,aAAA;AAAc;AAAA,OAC1C;AAAA,KACJ,CAAA;AAED,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AACd,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,uBAAA,EAA0B,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,IACnE;AAEA,IAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AAGjC,IAAA,IAAI;AACA,MAAA,MAAM,MAAA,GAAS,KAAK,KAAA,CAAM,IAAA,CAAK,QAAQ,CAAC,CAAA,CAAE,QAAQ,OAAO,CAAA;AACzD,MAAA,OAAO,EAAE,SAAA,EAAW,MAAA,CAAO,SAAA,EAAU;AAAA,IACzC,SAAS,CAAA,EAAG;AACR,MAAA,OAAA,CAAQ,KAAA,CAAM,gCAAgC,CAAC,CAAA;AAC/C,MAAA,OAAO,EAAE,WAAW,IAAA,EAAK;AAAA,IAC7B;AAAA,EACJ,CAAA;AACJ","file":"index.js","sourcesContent":["import { VoiceCommand } from '../types';\r\n\r\n/**\r\n * Generates the System Prompt for the LLM.\r\n * This ensures consistent behavior across different AI providers.\r\n */\r\nexport const createSystemPrompt = (commands: Omit<VoiceCommand, 'action'>[]) => {\r\n // Format the list of commands for the AI to read\r\n const commandList = commands.map(cmd =>\r\n `- ID: \"${cmd.id}\" | Description: \"${cmd.description}\"`\r\n ).join('\\n');\r\n\r\n return `\r\nYou are a precise Voice Command Router.\r\nYour goal is to map the user's spoken input to the correct Command ID from the list below.\r\n\r\nRULES:\r\n1. Analyze the user's input and find the intent.\r\n2. Match it to the command with the most relevant \"Description\".\r\n3. Use fuzzy matching (e.g., \"Dark mode\" matches \"Toggle Theme\").\r\n4. If NO command matches the intent, return null.\r\n5. IMPORTANT: Output ONLY valid JSON. Do not include markdown formatting.\r\n\r\nAVAILABLE COMMANDS:\r\n${commandList}\r\n\r\nRESPONSE FORMAT:\r\n{ \"commandId\": \"string_id_or_null\" }\r\n`;\r\n};\r\n\r\n/**\r\n * Standardizes how the user's voice transcript is presented to the AI.\r\n */\r\nexport const createUserPrompt = (transcript: string) => {\r\n return `User Input: \"${transcript}\"`;\r\n};","import { LLMAdapter } from '../types';\r\nimport { createSystemPrompt } from '../core/prompt';\r\n\r\ninterface OpenAIConfig {\r\n apiKey: string;\r\n /** @default \"gpt-4o-mini\" */\r\n model?: string;\r\n}\r\n\r\n/**\r\n * A Factory that creates an Adapter for OpenAI.\r\n * Users call this: createOpenAIAdapter({ apiKey: '...' })\r\n */\r\nexport const createOpenAIAdapter = (config: OpenAIConfig): LLMAdapter => {\r\n return async (transcript, commands) => {\r\n\r\n // 1. Generate the optimized system instructions\r\n const systemPrompt = createSystemPrompt(commands);\r\n\r\n // 2. Call the API\r\n const response = await fetch('https://api.openai.com/v1/chat/completions', {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'Authorization': `Bearer ${config.apiKey}`\r\n },\r\n body: JSON.stringify({\r\n model: config.model || \"gpt-4o-mini\",\r\n messages: [\r\n { role: \"system\", content: systemPrompt },\r\n { role: \"user\", content: transcript }\r\n ],\r\n temperature: 0, // Deterministic results\r\n response_format: { type: \"json_object\" } // Force JSON mode\r\n })\r\n });\r\n\r\n if (!response.ok) {\r\n throw new Error(`OpenAI Adapter Failed: ${response.statusText}`);\r\n }\r\n\r\n const data = await response.json();\r\n\r\n // 3. Parse the result\r\n try {\r\n const parsed = JSON.parse(data.choices[0].message.content);\r\n return { commandId: parsed.commandId };\r\n } catch (e) {\r\n console.error(\"Failed to parse LLM response\", e);\r\n return { commandId: null };\r\n }\r\n };\r\n};"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/prompt.ts","../../src/adapters/openai.ts"],"names":[],"mappings":";AAMO,IAAM,kBAAA,GAAqB,CAAC,QAAA,KAA6C;AAE5E,EAAA,MAAM,cAAc,QAAA,CAAS,GAAA;AAAA,IAAI,SAC7B,CAAA,OAAA,EAAU,GAAA,CAAI,EAAE,CAAA,kBAAA,EAAqB,IAAI,WAAW,CAAA,CAAA;AAAA,GACxD,CAAE,KAAK,IAAI,CAAA;AAEX,EAAA,OAAO;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA,EAYT,WAAW;;AAAA;AAAA;AAAA,CAAA;AAKb,CAAA;;;AChBO,IAAM,mBAAA,GAAsB,CAAC,MAAA,KAAqC;AACrE,EAAA,OAAO,OAAO,YAAY,QAAA,KAAa;AAGnC,IAAA,MAAM,YAAA,GAAe,mBAAmB,QAAQ,CAAA;AAGhD,IAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,4CAAA,EAA8C;AAAA,MACvE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACL,cAAA,EAAgB,kBAAA;AAAA,QAChB,eAAA,EAAiB,CAAA,OAAA,EAAU,MAAA,CAAO,MAAM,CAAA;AAAA,OAC5C;AAAA,MACA,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,QACjB,KAAA,EAAO,OAAO,KAAA,IAAS,aAAA;AAAA,QACvB,QAAA,EAAU;AAAA,UACN,EAAE,IAAA,EAAM,QAAA,EAAU,OAAA,EAAS,YAAA,EAAa;AAAA,UACxC,EAAE,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,UAAA;AAAW,SACxC;AAAA,QACA,WAAA,EAAa,CAAA;AAAA;AAAA,QACb,eAAA,EAAiB,EAAE,IAAA,EAAM,aAAA;AAAc;AAAA,OAC1C;AAAA,KACJ,CAAA;AAED,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AACd,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,uBAAA,EAA0B,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,IACnE;AAEA,IAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AAGjC,IAAA,IAAI;AACA,MAAA,MAAM,MAAA,GAAS,KAAK,KAAA,CAAM,IAAA,CAAK,QAAQ,CAAC,CAAA,CAAE,QAAQ,OAAO,CAAA;AACzD,MAAA,OAAO,EAAE,SAAA,EAAW,MAAA,CAAO,SAAA,EAAU;AAAA,IACzC,SAAS,CAAA,EAAG;AACR,MAAA,OAAA,CAAQ,KAAA,CAAM,gCAAgC,CAAC,CAAA;AAC/C,MAAA,OAAO,EAAE,WAAW,IAAA,EAAK;AAAA,IAC7B;AAAA,EACJ,CAAA;AACJ","file":"index.mjs","sourcesContent":["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/core/prompt.ts","../../src/adapters/openai.ts"],"names":[],"mappings":";AAMO,IAAM,kBAAA,GAAqB,CAAC,QAAA,KAA6C;AAE5E,EAAA,MAAM,cAAc,QAAA,CAAS,GAAA;AAAA,IAAI,SAC7B,CAAA,OAAA,EAAU,GAAA,CAAI,EAAE,CAAA,kBAAA,EAAqB,IAAI,WAAW,CAAA,CAAA;AAAA,GACxD,CAAE,KAAK,IAAI,CAAA;AAEX,EAAA,OAAO;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA,EAYT,WAAW;;AAAA;AAAA;AAAA,CAAA;AAKb,CAAA;;;AChBO,IAAM,mBAAA,GAAsB,CAAC,MAAA,KAAqC;AACrE,EAAA,OAAO,OAAO,YAAY,QAAA,KAAa;AAGnC,IAAA,MAAM,YAAA,GAAe,mBAAmB,QAAQ,CAAA;AAGhD,IAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,4CAAA,EAA8C;AAAA,MACvE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACL,cAAA,EAAgB,kBAAA;AAAA,QAChB,eAAA,EAAiB,CAAA,OAAA,EAAU,MAAA,CAAO,MAAM,CAAA;AAAA,OAC5C;AAAA,MACA,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,QACjB,KAAA,EAAO,OAAO,KAAA,IAAS,aAAA;AAAA,QACvB,QAAA,EAAU;AAAA,UACN,EAAE,IAAA,EAAM,QAAA,EAAU,OAAA,EAAS,YAAA,EAAa;AAAA,UACxC,EAAE,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,UAAA;AAAW,SACxC;AAAA,QACA,WAAA,EAAa,CAAA;AAAA;AAAA,QACb,eAAA,EAAiB,EAAE,IAAA,EAAM,aAAA;AAAc;AAAA,OAC1C;AAAA,KACJ,CAAA;AAED,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AACd,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,uBAAA,EAA0B,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,IACnE;AAEA,IAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AAGjC,IAAA,IAAI;AACA,MAAA,MAAM,MAAA,GAAS,KAAK,KAAA,CAAM,IAAA,CAAK,QAAQ,CAAC,CAAA,CAAE,QAAQ,OAAO,CAAA;AACzD,MAAA,OAAO,EAAE,SAAA,EAAW,MAAA,CAAO,SAAA,EAAU;AAAA,IACzC,SAAS,CAAA,EAAG;AACR,MAAA,OAAA,CAAQ,KAAA,CAAM,gCAAgC,CAAC,CAAA;AAC/C,MAAA,OAAO,EAAE,WAAW,IAAA,EAAK;AAAA,IAC7B;AAAA,EACJ,CAAA;AACJ","file":"index.mjs","sourcesContent":["import { VoiceCommand } from '../types';\r\n\r\n/**\r\n * Generates the System Prompt for the LLM.\r\n * This ensures consistent behavior across different AI providers.\r\n */\r\nexport const createSystemPrompt = (commands: Omit<VoiceCommand, 'action'>[]) => {\r\n // Format the list of commands for the AI to read\r\n const commandList = commands.map(cmd =>\r\n `- ID: \"${cmd.id}\" | Description: \"${cmd.description}\"`\r\n ).join('\\n');\r\n\r\n return `\r\nYou are a precise Voice Command Router.\r\nYour goal is to map the user's spoken input to the correct Command ID from the list below.\r\n\r\nRULES:\r\n1. Analyze the user's input and find the intent.\r\n2. Match it to the command with the most relevant \"Description\".\r\n3. Use fuzzy matching (e.g., \"Dark mode\" matches \"Toggle Theme\").\r\n4. If NO command matches the intent, return null.\r\n5. IMPORTANT: Output ONLY valid JSON. Do not include markdown formatting.\r\n\r\nAVAILABLE COMMANDS:\r\n${commandList}\r\n\r\nRESPONSE FORMAT:\r\n{ \"commandId\": \"string_id_or_null\" }\r\n`;\r\n};\r\n\r\n/**\r\n * Standardizes how the user's voice transcript is presented to the AI.\r\n */\r\nexport const createUserPrompt = (transcript: string) => {\r\n return `User Input: \"${transcript}\"`;\r\n};","import { LLMAdapter } from '../types';\r\nimport { createSystemPrompt } from '../core/prompt';\r\n\r\ninterface OpenAIConfig {\r\n apiKey: string;\r\n /** @default \"gpt-4o-mini\" */\r\n model?: string;\r\n}\r\n\r\n/**\r\n * A Factory that creates an Adapter for OpenAI.\r\n * Users call this: createOpenAIAdapter({ apiKey: '...' })\r\n */\r\nexport const createOpenAIAdapter = (config: OpenAIConfig): LLMAdapter => {\r\n return async (transcript, commands) => {\r\n\r\n // 1. Generate the optimized system instructions\r\n const systemPrompt = createSystemPrompt(commands);\r\n\r\n // 2. Call the API\r\n const response = await fetch('https://api.openai.com/v1/chat/completions', {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'Authorization': `Bearer ${config.apiKey}`\r\n },\r\n body: JSON.stringify({\r\n model: config.model || \"gpt-4o-mini\",\r\n messages: [\r\n { role: \"system\", content: systemPrompt },\r\n { role: \"user\", content: transcript }\r\n ],\r\n temperature: 0, // Deterministic results\r\n response_format: { type: \"json_object\" } // Force JSON mode\r\n })\r\n });\r\n\r\n if (!response.ok) {\r\n throw new Error(`OpenAI Adapter Failed: ${response.statusText}`);\r\n }\r\n\r\n const data = await response.json();\r\n\r\n // 3. Parse the result\r\n try {\r\n const parsed = JSON.parse(data.choices[0].message.content);\r\n return { commandId: parsed.commandId };\r\n } catch (e) {\r\n console.error(\"Failed to parse LLM response\", e);\r\n return { commandId: null };\r\n }\r\n };\r\n};"]}
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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';\r\nimport { VoiceCommand, VoiceControlState, VoiceProviderProps } from '../types';\r\n\r\ninterface VoiceContextValue extends VoiceControlState {\r\n register: (cmd: VoiceCommand) => void;\r\n unregister: (id: string) => void;\r\n processTranscript: (text: string) => Promise<void>;\r\n}\r\n\r\nconst VoiceContext = createContext<VoiceContextValue | null>(null);\r\n\r\nexport const VoiceControlProvider: React.FC<VoiceProviderProps> = ({ \r\n children, \r\n adapter \r\n}) => {\r\n // THE REGISTRY: A Map ensures O(1) lookups and prevents duplicate IDs\r\n const commandsRef = useRef<Map<string, VoiceCommand>>(new Map());\r\n \r\n // UI STATE\r\n const [state, setState] = useState<VoiceControlState>({\r\n isListening: false,\r\n isProcessing: false,\r\n lastTranscript: null,\r\n activeCommands: [],\r\n });\r\n\r\n // 1. REGISTRATION LOGIC\r\n const register = useCallback((cmd: VoiceCommand) => {\r\n commandsRef.current.set(cmd.id, cmd);\r\n // Update generic state for debugging UI\r\n setState(prev => ({ ...prev, activeCommands: Array.from(commandsRef.current.values()) }));\r\n }, []);\r\n\r\n const unregister = useCallback((id: string) => {\r\n commandsRef.current.delete(id);\r\n setState(prev => ({ ...prev, activeCommands: Array.from(commandsRef.current.values()) }));\r\n }, []);\r\n\r\n // 2. THE ROUTER LOGIC\r\n const processTranscript = useCallback(async (transcript: string) => {\r\n const cleanText = transcript.trim().toLowerCase();\r\n setState(prev => ({ ...prev, isProcessing: true, lastTranscript: cleanText }));\r\n\r\n const allCommands = Array.from(commandsRef.current.values());\r\n\r\n // PHASE 1: EXACT MATCH (0ms Latency)\r\n const exactMatch = allCommands.find(c => c.phrase?.toLowerCase() === cleanText);\r\n \r\n if (exactMatch) {\r\n console.log(`⚡ Instant Match: \"${cleanText}\" -> ${exactMatch.id}`);\r\n exactMatch.action();\r\n setState(prev => ({ ...prev, isProcessing: false }));\r\n return;\r\n }\r\n\r\n // PHASE 2: FUZZY MATCH (AI Adapter)\r\n try {\r\n console.log(`🤖 AI Routing: \"${cleanText}\"...`);\r\n \r\n // We strip the 'action' function before sending to the AI\r\n const commandListForAI = allCommands.map(({ id, description, phrase }) => ({ \r\n id, \r\n description, \r\n phrase \r\n }));\r\n\r\n const result = await adapter(cleanText, commandListForAI);\r\n\r\n if (result.commandId) {\r\n const cmd = commandsRef.current.get(result.commandId);\r\n if (cmd) {\r\n console.log(`✅ Matched: ${cmd.id}`);\r\n cmd.action();\r\n } else {\r\n console.warn(`⚠️ Adapter returned unknown ID: ${result.commandId}`);\r\n }\r\n }\r\n } catch (error) {\r\n console.error(\"Adapter Error:\", error);\r\n } finally {\r\n setState(prev => ({ ...prev, isProcessing: false }));\r\n }\r\n }, [adapter]);\r\n\r\n return (\r\n <VoiceContext.Provider value={{ ...state, register, unregister, processTranscript }}>\r\n {children}\r\n </VoiceContext.Provider>\r\n );\r\n};\r\n\r\nexport const useVoiceContext = () => {\r\n const ctx = useContext(VoiceContext);\r\n if (!ctx) throw new Error(\"useVoiceContext must be used within a VoiceControlProvider\");\r\n return ctx;\r\n};","import { useEffect, useRef } from \"react\";\r\nimport { useVoiceContext } from \"../components/VoiceContext\";\r\nimport { VoiceCommand } from \"../types\";\r\n\r\nexport const useVoiceCommand = (command: VoiceCommand) => {\r\n const { register, unregister } = useVoiceContext();\r\n\r\n // 1. Keep a \"Ref\" to the latest command.\r\n // This allows the action function to change (e.g. updating state)\r\n // WITHOUT forcing us to unregister/re-register the command.\r\n const commandRef = useRef(command);\r\n\r\n // Always update the ref on every render\r\n useEffect(() => {\r\n commandRef.current = command;\r\n });\r\n\r\n useEffect(() => {\r\n // 2. Register a \"Proxy Command\"\r\n // Instead of registering the raw command, we register a proxy\r\n // that always calls the LATEST version stored in the ref.\r\n const proxyCommand: VoiceCommand = {\r\n id: command.id,\r\n description: command.description,\r\n phrase: command.phrase,\r\n action: () => {\r\n // When triggered, execute whatever the current action is\r\n commandRef.current.action();\r\n },\r\n };\r\n\r\n register(proxyCommand);\r\n\r\n return () => {\r\n unregister(proxyCommand.id);\r\n };\r\n // 3. Only re-register if the ID changes.\r\n // We intentionally ignore 'command' or 'command.action' changes here.\r\n }, [register, unregister, command.id]);\r\n};\r\n","import { VoiceCommand } from '../types';\r\n\r\n/**\r\n * Generates the System Prompt for the LLM.\r\n * This ensures consistent behavior across different AI providers.\r\n */\r\nexport const createSystemPrompt = (commands: Omit<VoiceCommand, 'action'>[]) => {\r\n // Format the list of commands for the AI to read\r\n const commandList = commands.map(cmd =>\r\n `- ID: \"${cmd.id}\" | Description: \"${cmd.description}\"`\r\n ).join('\\n');\r\n\r\n return `\r\nYou are a precise Voice Command Router.\r\nYour goal is to map the user's spoken input to the correct Command ID from the list below.\r\n\r\nRULES:\r\n1. Analyze the user's input and find the intent.\r\n2. Match it to the command with the most relevant \"Description\".\r\n3. Use fuzzy matching (e.g., \"Dark mode\" matches \"Toggle Theme\").\r\n4. If NO command matches the intent, return null.\r\n5. IMPORTANT: Output ONLY valid JSON. Do not include markdown formatting.\r\n\r\nAVAILABLE COMMANDS:\r\n${commandList}\r\n\r\nRESPONSE FORMAT:\r\n{ \"commandId\": \"string_id_or_null\" }\r\n`;\r\n};\r\n\r\n/**\r\n * Standardizes how the user's voice transcript is presented to the AI.\r\n */\r\nexport const createUserPrompt = (transcript: string) => {\r\n return `User Input: \"${transcript}\"`;\r\n};","import { LLMAdapter } from '../types';\r\nimport { createSystemPrompt } from '../core/prompt';\r\n\r\ninterface OpenAIConfig {\r\n apiKey: string;\r\n /** @default \"gpt-4o-mini\" */\r\n model?: string;\r\n}\r\n\r\n/**\r\n * A Factory that creates an Adapter for OpenAI.\r\n * Users call this: createOpenAIAdapter({ apiKey: '...' })\r\n */\r\nexport const createOpenAIAdapter = (config: OpenAIConfig): LLMAdapter => {\r\n return async (transcript, commands) => {\r\n\r\n // 1. Generate the optimized system instructions\r\n const systemPrompt = createSystemPrompt(commands);\r\n\r\n // 2. Call the API\r\n const response = await fetch('https://api.openai.com/v1/chat/completions', {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'Authorization': `Bearer ${config.apiKey}`\r\n },\r\n body: JSON.stringify({\r\n model: config.model || \"gpt-4o-mini\",\r\n messages: [\r\n { role: \"system\", content: systemPrompt },\r\n { role: \"user\", content: transcript }\r\n ],\r\n temperature: 0, // Deterministic results\r\n response_format: { type: \"json_object\" } // Force JSON mode\r\n })\r\n });\r\n\r\n if (!response.ok) {\r\n throw new Error(`OpenAI Adapter Failed: ${response.statusText}`);\r\n }\r\n\r\n const data = await response.json();\r\n\r\n // 3. Parse the result\r\n try {\r\n const parsed = JSON.parse(data.choices[0].message.content);\r\n return { commandId: parsed.commandId };\r\n } catch (e) {\r\n console.error(\"Failed to parse LLM response\", e);\r\n return { commandId: null };\r\n }\r\n };\r\n};","import { LLMAdapter } from \"../types\";\r\n\r\ninterface GeminiConfig {\r\n apiKey: string;\r\n /**\r\n * @default \"gemini-1.5-flash\"\r\n */\r\n model?: string;\r\n /**\r\n * System instruction to guide the style of response (Optional)\r\n */\r\n systemInstruction?: string;\r\n}\r\n\r\nexport const createGeminiAdapter = (config: GeminiConfig): LLMAdapter => {\r\n return async (transcript, commands) => {\r\n // 1. Prepare the System Prompt\r\n // We explain the task to Gemini and list the available commands\r\n const commandList = commands\r\n .map((c) => `- \"${c.id}\": ${c.description}`)\r\n .join(\"\\n\");\r\n\r\n const prompt = `\r\n You are a voice command router.\r\n The user said: \"${transcript}\"\r\n\r\n Available commands:\r\n ${commandList}\r\n\r\n Rules:\r\n 1. Return ONLY the JSON object. No markdown, no explanation.\r\n 2. Format: { \"commandId\": \"id_here\" } or { \"commandId\": null } if no match.\r\n `;\r\n\r\n // 2. Call Google Gemini API\r\n const url = `https://generativelanguage.googleapis.com/v1beta/models/${\r\n config.model || \"gemini-1.5-flash\"\r\n }:generateContent?key=${config.apiKey}`;\r\n\r\n try {\r\n const response = await fetch(url, {\r\n method: \"POST\",\r\n headers: {\r\n \"Content-Type\": \"application/json\",\r\n },\r\n body: JSON.stringify({\r\n contents: [\r\n {\r\n parts: [{ text: prompt }],\r\n },\r\n ],\r\n }),\r\n });\r\n\r\n if (!response.ok) {\r\n throw new Error(`Gemini API Error: ${response.statusText}`);\r\n }\r\n\r\n const data = await response.json();\r\n\r\n // 3. Parse Response\r\n // Gemini returns nested objects: candidates[0].content.parts[0].text\r\n const textResponse = data.candidates?.[0]?.content?.parts?.[0]?.text;\r\n\r\n if (!textResponse) return { commandId: null };\r\n\r\n // Clean up markdown code blocks if Gemini adds them (e.g. ```json ... ```)\r\n const cleanJson = textResponse.replace(/```json|```/g, \"\").trim();\r\n\r\n return JSON.parse(cleanJson);\r\n } catch (error) {\r\n console.error(\"Error routing with Gemini:\", error);\r\n return { commandId: null };\r\n }\r\n };\r\n};\r\n","import { LLMAdapter } from \"../types\";\r\n\r\ninterface ClaudeConfig {\r\n apiKey: string;\r\n /**\r\n * @default \"claude-3-5-sonnet-20240620\"\r\n */\r\n model?: string;\r\n /**\r\n * Optional custom endpoint if using a proxy (Recommended for CORS)\r\n * @default \"https://api.anthropic.com/v1/messages\"\r\n */\r\n endpoint?: string;\r\n}\r\n\r\nexport const createClaudeAdapter = (config: ClaudeConfig): LLMAdapter => {\r\n return async (transcript, commands) => {\r\n const endpoint = config.endpoint || \"https://api.anthropic.com/v1/messages\";\r\n const model = config.model || \"claude-3-5-sonnet-20240620\";\r\n\r\n // 1. Prepare Command List\r\n const commandList = commands\r\n .map((c) => `- \"${c.id}\": ${c.description}`)\r\n .join(\"\\n\");\r\n\r\n const systemPrompt = `\r\n You are a voice command router.\r\n Available commands:\r\n ${commandList}\r\n\r\n Rules:\r\n 1. Analyze the user's transcript.\r\n 2. Return a JSON object: { \"commandId\": \"id\" } or { \"commandId\": null }.\r\n 3. No markdown, no conversational text. ONLY JSON.\r\n `;\r\n\r\n try {\r\n // 2. Call Anthropic API\r\n const response = await fetch(endpoint, {\r\n method: \"POST\",\r\n headers: {\r\n \"x-api-key\": config.apiKey,\r\n \"anthropic-version\": \"2023-06-01\",\r\n \"content-type\": \"application/json\",\r\n // 'dangerously-allow-browser': 'true' // Anthropic client SDK uses this, but fetch doesn't need it.\r\n },\r\n body: JSON.stringify({\r\n model: model,\r\n max_tokens: 100,\r\n system: systemPrompt,\r\n messages: [{ role: \"user\", content: transcript }],\r\n }),\r\n });\r\n\r\n if (!response.ok) {\r\n const err = await response.text();\r\n throw new Error(`Claude API Error: ${response.status} - ${err}`);\r\n }\r\n\r\n const data = await response.json();\r\n\r\n // 3. Parse Response\r\n // Claude returns: content: [ { type: 'text', text: '...' } ]\r\n const textResponse = data.content?.[0]?.text;\r\n\r\n if (!textResponse) return { commandId: null };\r\n\r\n // Clean up potential markdown formatting\r\n const cleanJson = textResponse.replace(/```json|```/g, \"\").trim();\r\n return JSON.parse(cleanJson);\r\n } catch (error) {\r\n console.error(\"Error routing with Claude:\", error);\r\n return { commandId: null };\r\n }\r\n };\r\n};\r\n"]}
|
package/dist/index.mjs
CHANGED
|
@@ -148,6 +148,101 @@ var createOpenAIAdapter = (config) => {
|
|
|
148
148
|
};
|
|
149
149
|
};
|
|
150
150
|
|
|
151
|
-
|
|
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
|
package/dist/index.mjs.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":["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":";;;;AASA,IAAM,YAAA,GAAe,cAAwC,IAAI,CAAA;AAE1D,IAAM,uBAAqD,CAAC;AAAA,EACjE,QAAA;AAAA,EACA;AACF,CAAA,KAAM;AAEJ,EAAA,MAAM,WAAA,GAAc,MAAA,iBAAkC,IAAI,GAAA,EAAK,CAAA;AAG/D,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,QAAA,CAA4B;AAAA,IACpD,WAAA,EAAa,KAAA;AAAA,IACb,YAAA,EAAc,KAAA;AAAA,IACd,cAAA,EAAgB,IAAA;AAAA,IAChB,gBAAgB;AAAC,GAClB,CAAA;AAGD,EAAA,MAAM,QAAA,GAAW,WAAA,CAAY,CAAC,GAAA,KAAsB;AAClD,IAAA,WAAA,CAAY,OAAA,CAAQ,GAAA,CAAI,GAAA,CAAI,EAAA,EAAI,GAAG,CAAA;AAEnC,IAAA,QAAA,CAAS,CAAA,IAAA,MAAS,EAAE,GAAG,IAAA,EAAM,cAAA,EAAgB,KAAA,CAAM,IAAA,CAAK,WAAA,CAAY,OAAA,CAAQ,MAAA,EAAQ,CAAA,EAAE,CAAE,CAAA;AAAA,EAC1F,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,UAAA,GAAa,WAAA,CAAY,CAAC,EAAA,KAAe;AAC7C,IAAA,WAAA,CAAY,OAAA,CAAQ,OAAO,EAAE,CAAA;AAC7B,IAAA,QAAA,CAAS,CAAA,IAAA,MAAS,EAAE,GAAG,IAAA,EAAM,cAAA,EAAgB,KAAA,CAAM,IAAA,CAAK,WAAA,CAAY,OAAA,CAAQ,MAAA,EAAQ,CAAA,EAAE,CAAE,CAAA;AAAA,EAC1F,CAAA,EAAG,EAAE,CAAA;AAGL,EAAA,MAAM,iBAAA,GAAoB,WAAA,CAAY,OAAO,UAAA,KAAuB;AAClE,IAAA,MAAM,SAAA,GAAY,UAAA,CAAW,IAAA,EAAK,CAAE,WAAA,EAAY;AAChD,IAAA,QAAA,CAAS,CAAA,IAAA,MAAS,EAAE,GAAG,IAAA,EAAM,cAAc,IAAA,EAAM,cAAA,EAAgB,WAAU,CAAE,CAAA;AAE7E,IAAA,MAAM,cAAc,KAAA,CAAM,IAAA,CAAK,WAAA,CAAY,OAAA,CAAQ,QAAQ,CAAA;AAG3D,IAAA,MAAM,UAAA,GAAa,WAAA,CAAY,IAAA,CAAK,CAAA,CAAA,KAAE;AA9C1C,MAAA,IAAA,EAAA;AA8C6C,MAAA,OAAA,CAAA,CAAA,EAAA,GAAA,CAAA,CAAE,MAAA,KAAF,mBAAU,WAAA,EAAA,MAAkB,SAAA;AAAA,IAAA,CAAS,CAAA;AAE9E,IAAA,IAAI,UAAA,EAAY;AACd,MAAA,OAAA,CAAQ,IAAI,CAAA,uBAAA,EAAqB,SAAS,CAAA,KAAA,EAAQ,UAAA,CAAW,EAAE,CAAA,CAAE,CAAA;AACjE,MAAA,UAAA,CAAW,MAAA,EAAO;AAClB,MAAA,QAAA,CAAS,WAAS,EAAE,GAAG,IAAA,EAAM,YAAA,EAAc,OAAM,CAAE,CAAA;AACnD,MAAA;AAAA,IACF;AAGA,IAAA,IAAI;AACF,MAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,uBAAA,EAAmB,SAAS,CAAA,IAAA,CAAM,CAAA;AAG9C,MAAA,MAAM,gBAAA,GAAmB,YAAY,GAAA,CAAI,CAAC,EAAE,EAAA,EAAI,WAAA,EAAa,QAAO,MAAO;AAAA,QACzE,EAAA;AAAA,QACA,WAAA;AAAA,QACA;AAAA,OACF,CAAE,CAAA;AAEF,MAAA,MAAM,MAAA,GAAS,MAAM,OAAA,CAAQ,SAAA,EAAW,gBAAgB,CAAA;AAExD,MAAA,IAAI,OAAO,SAAA,EAAW;AACpB,QAAA,MAAM,GAAA,GAAM,WAAA,CAAY,OAAA,CAAQ,GAAA,CAAI,OAAO,SAAS,CAAA;AACpD,QAAA,IAAI,GAAA,EAAK;AACP,UAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,gBAAA,EAAc,GAAA,CAAI,EAAE,CAAA,CAAE,CAAA;AAClC,UAAA,GAAA,CAAI,MAAA,EAAO;AAAA,QACb,CAAA,MAAO;AACL,UAAA,OAAA,CAAQ,IAAA,CAAK,CAAA,0CAAA,EAAmC,MAAA,CAAO,SAAS,CAAA,CAAE,CAAA;AAAA,QACpE;AAAA,MACF;AAAA,IACF,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAA,CAAM,kBAAkB,KAAK,CAAA;AAAA,IACvC,CAAA,SAAE;AACA,MAAA,QAAA,CAAS,WAAS,EAAE,GAAG,IAAA,EAAM,YAAA,EAAc,OAAM,CAAE,CAAA;AAAA,IACrD;AAAA,EACF,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAA,uBACE,GAAA,CAAC,YAAA,CAAa,QAAA,EAAb,EAAsB,KAAA,EAAO,EAAE,GAAG,KAAA,EAAO,QAAA,EAAU,UAAA,EAAY,iBAAA,EAAkB,EAC/E,QAAA,EACH,CAAA;AAEJ;AAEO,IAAM,kBAAkB,MAAM;AACnC,EAAA,MAAM,GAAA,GAAM,WAAW,YAAY,CAAA;AACnC,EAAA,IAAI,CAAC,GAAA,EAAK,MAAM,IAAI,MAAM,4DAA4D,CAAA;AACtF,EAAA,OAAO,GAAA;AACT;AC3FO,IAAM,eAAA,GAAkB,CAAC,OAAA,KAA0B;AACxD,EAAA,MAAM,EAAE,QAAA,EAAU,UAAA,EAAW,GAAI,eAAA,EAAgB;AAKjD,EAAA,MAAM,UAAA,GAAaA,OAAO,OAAO,CAAA;AAGjC,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,UAAA,CAAW,OAAA,GAAU,OAAA;AAAA,EACvB,CAAC,CAAA;AAED,EAAA,SAAA,CAAU,MAAM;AAId,IAAA,MAAM,YAAA,GAA6B;AAAA,MACjC,IAAI,OAAA,CAAQ,EAAA;AAAA,MACZ,aAAa,OAAA,CAAQ,WAAA;AAAA,MACrB,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,QAAQ,MAAM;AAEZ,QAAA,UAAA,CAAW,QAAQ,MAAA,EAAO;AAAA,MAC5B;AAAA,KACF;AAEA,IAAA,QAAA,CAAS,YAAY,CAAA;AAErB,IAAA,OAAO,MAAM;AACX,MAAA,UAAA,CAAW,aAAa,EAAE,CAAA;AAAA,IAC5B,CAAA;AAAA,EAGF,GAAG,CAAC,QAAA,EAAU,UAAA,EAAY,OAAA,CAAQ,EAAE,CAAC,CAAA;AACvC;;;ACjCO,IAAM,kBAAA,GAAqB,CAAC,QAAA,KAA6C;AAE5E,EAAA,MAAM,cAAc,QAAA,CAAS,GAAA;AAAA,IAAI,SAC7B,CAAA,OAAA,EAAU,GAAA,CAAI,EAAE,CAAA,kBAAA,EAAqB,IAAI,WAAW,CAAA,CAAA;AAAA,GACxD,CAAE,KAAK,IAAI,CAAA;AAEX,EAAA,OAAO;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA,EAYT,WAAW;;AAAA;AAAA;AAAA,CAAA;AAKb,CAAA;;;AChBO,IAAM,mBAAA,GAAsB,CAAC,MAAA,KAAqC;AACrE,EAAA,OAAO,OAAO,YAAY,QAAA,KAAa;AAGnC,IAAA,MAAM,YAAA,GAAe,mBAAmB,QAAQ,CAAA;AAGhD,IAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,4CAAA,EAA8C;AAAA,MACvE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACL,cAAA,EAAgB,kBAAA;AAAA,QAChB,eAAA,EAAiB,CAAA,OAAA,EAAU,MAAA,CAAO,MAAM,CAAA;AAAA,OAC5C;AAAA,MACA,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,QACjB,KAAA,EAAO,OAAO,KAAA,IAAS,aAAA;AAAA,QACvB,QAAA,EAAU;AAAA,UACN,EAAE,IAAA,EAAM,QAAA,EAAU,OAAA,EAAS,YAAA,EAAa;AAAA,UACxC,EAAE,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,UAAA;AAAW,SACxC;AAAA,QACA,WAAA,EAAa,CAAA;AAAA;AAAA,QACb,eAAA,EAAiB,EAAE,IAAA,EAAM,aAAA;AAAc;AAAA,OAC1C;AAAA,KACJ,CAAA;AAED,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AACd,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,uBAAA,EAA0B,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,IACnE;AAEA,IAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AAGjC,IAAA,IAAI;AACA,MAAA,MAAM,MAAA,GAAS,KAAK,KAAA,CAAM,IAAA,CAAK,QAAQ,CAAC,CAAA,CAAE,QAAQ,OAAO,CAAA;AACzD,MAAA,OAAO,EAAE,SAAA,EAAW,MAAA,CAAO,SAAA,EAAU;AAAA,IACzC,SAAS,CAAA,EAAG;AACR,MAAA,OAAA,CAAQ,KAAA,CAAM,gCAAgC,CAAC,CAAA;AAC/C,MAAA,OAAO,EAAE,WAAW,IAAA,EAAK;AAAA,IAC7B;AAAA,EACJ,CAAA;AACJ;;;ACtCO,IAAM,mBAAA,GAAsB,CAAC,MAAA,KAAqC;AACvE,EAAA,OAAO,OAAO,YAAY,QAAA,KAAa;AAfzC,IAAA,IAAA,EAAA,EAAA,EAAA,EAAA,EAAA,EAAA,EAAA,EAAA,EAAA;AAkBI,IAAA,MAAM,WAAA,GAAc,QAAA,CACjB,GAAA,CAAI,CAAC,MAAM,CAAA,GAAA,EAAM,CAAA,CAAE,EAAE,CAAA,GAAA,EAAM,CAAA,CAAE,WAAW,CAAA,CAAE,CAAA,CAC1C,KAAK,IAAI,CAAA;AAEZ,IAAA,MAAM,MAAA,GAAS;AAAA;AAAA,sBAAA,EAEK,UAAU,CAAA;;AAAA;AAAA,MAAA,EAG1B,WAAW;;AAAA;AAAA;AAAA;AAAA,IAAA,CAAA;AAQf,IAAA,MAAM,MAAM,CAAA,wDAAA,EACV,MAAA,CAAO,SAAS,kBAClB,CAAA,qBAAA,EAAwB,OAAO,MAAM,CAAA,CAAA;AAErC,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,QAChC,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS;AAAA,UACP,cAAA,EAAgB;AAAA,SAClB;AAAA,QACA,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,UACnB,QAAA,EAAU;AAAA,YACR;AAAA,cACE,KAAA,EAAO,CAAC,EAAE,IAAA,EAAM,QAAQ;AAAA;AAC1B;AACF,SACD;AAAA,OACF,CAAA;AAED,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,kBAAA,EAAqB,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MAC5D;AAEA,MAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AAIjC,MAAA,MAAM,YAAA,GAAA,CAAe,EAAA,GAAA,CAAA,EAAA,GAAA,CAAA,EAAA,GAAA,CAAA,EAAA,GAAA,CAAA,EAAA,GAAA,IAAA,CAAK,UAAA,KAAL,IAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAkB,CAAA,CAAA,KAAlB,IAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAsB,OAAA,KAAtB,IAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAA+B,KAAA,KAA/B,IAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAuC,CAAA,CAAA,KAAvC,IAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAA2C,IAAA;AAEhE,MAAA,IAAI,CAAC,YAAA,EAAc,OAAO,EAAE,WAAW,IAAA,EAAK;AAG5C,MAAA,MAAM,YAAY,YAAA,CAAa,OAAA,CAAQ,cAAA,EAAgB,EAAE,EAAE,IAAA,EAAK;AAEhE,MAAA,OAAO,IAAA,CAAK,MAAM,SAAS,CAAA;AAAA,IAC7B,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAA,CAAM,8BAA8B,KAAK,CAAA;AACjD,MAAA,OAAO,EAAE,WAAW,IAAA,EAAK;AAAA,IAC3B;AAAA,EACF,CAAA;AACF;;;AC5DO,IAAM,mBAAA,GAAsB,CAAC,MAAA,KAAqC;AACvE,EAAA,OAAO,OAAO,YAAY,QAAA,KAAa;AAhBzC,IAAA,IAAA,EAAA,EAAA,EAAA;AAiBI,IAAA,MAAM,QAAA,GAAW,OAAO,QAAA,IAAY,uCAAA;AACpC,IAAA,MAAM,KAAA,GAAQ,OAAO,KAAA,IAAS,4BAAA;AAG9B,IAAA,MAAM,WAAA,GAAc,QAAA,CACjB,GAAA,CAAI,CAAC,MAAM,CAAA,GAAA,EAAM,CAAA,CAAE,EAAE,CAAA,GAAA,EAAM,CAAA,CAAE,WAAW,CAAA,CAAE,CAAA,CAC1C,KAAK,IAAI,CAAA;AAEZ,IAAA,MAAM,YAAA,GAAe;AAAA;AAAA;AAAA,MAAA,EAGjB,WAAW;;AAAA;AAAA;AAAA;AAAA;AAAA,IAAA,CAAA;AAQf,IAAA,IAAI;AAEF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,QAAA,EAAU;AAAA,QACrC,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS;AAAA,UACP,aAAa,MAAA,CAAO,MAAA;AAAA,UACpB,mBAAA,EAAqB,YAAA;AAAA,UACrB,cAAA,EAAgB;AAAA;AAAA,SAElB;AAAA,QACA,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,UACnB,KAAA;AAAA,UACA,UAAA,EAAY,GAAA;AAAA,UACZ,MAAA,EAAQ,YAAA;AAAA,UACR,UAAU,CAAC,EAAE,MAAM,MAAA,EAAQ,OAAA,EAAS,YAAY;AAAA,SACjD;AAAA,OACF,CAAA;AAED,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,MAAM,GAAA,GAAM,MAAM,QAAA,CAAS,IAAA,EAAK;AAChC,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,kBAAA,EAAqB,SAAS,MAAM,CAAA,GAAA,EAAM,GAAG,CAAA,CAAE,CAAA;AAAA,MACjE;AAEA,MAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AAIjC,MAAA,MAAM,YAAA,GAAA,CAAe,EAAA,GAAA,CAAA,EAAA,GAAA,IAAA,CAAK,OAAA,KAAL,IAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAe,OAAf,IAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAmB,IAAA;AAExC,MAAA,IAAI,CAAC,YAAA,EAAc,OAAO,EAAE,WAAW,IAAA,EAAK;AAG5C,MAAA,MAAM,YAAY,YAAA,CAAa,OAAA,CAAQ,cAAA,EAAgB,EAAE,EAAE,IAAA,EAAK;AAChE,MAAA,OAAO,IAAA,CAAK,MAAM,SAAS,CAAA;AAAA,IAC7B,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAA,CAAM,8BAA8B,KAAK,CAAA;AACjD,MAAA,OAAO,EAAE,WAAW,IAAA,EAAK;AAAA,IAC3B;AAAA,EACF,CAAA;AACF","file":"index.mjs","sourcesContent":["import React, { createContext, useContext, useState, useCallback, useRef } from 'react';\r\nimport { VoiceCommand, VoiceControlState, VoiceProviderProps } from '../types';\r\n\r\ninterface VoiceContextValue extends VoiceControlState {\r\n register: (cmd: VoiceCommand) => void;\r\n unregister: (id: string) => void;\r\n processTranscript: (text: string) => Promise<void>;\r\n}\r\n\r\nconst VoiceContext = createContext<VoiceContextValue | null>(null);\r\n\r\nexport const VoiceControlProvider: React.FC<VoiceProviderProps> = ({ \r\n children, \r\n adapter \r\n}) => {\r\n // THE REGISTRY: A Map ensures O(1) lookups and prevents duplicate IDs\r\n const commandsRef = useRef<Map<string, VoiceCommand>>(new Map());\r\n \r\n // UI STATE\r\n const [state, setState] = useState<VoiceControlState>({\r\n isListening: false,\r\n isProcessing: false,\r\n lastTranscript: null,\r\n activeCommands: [],\r\n });\r\n\r\n // 1. REGISTRATION LOGIC\r\n const register = useCallback((cmd: VoiceCommand) => {\r\n commandsRef.current.set(cmd.id, cmd);\r\n // Update generic state for debugging UI\r\n setState(prev => ({ ...prev, activeCommands: Array.from(commandsRef.current.values()) }));\r\n }, []);\r\n\r\n const unregister = useCallback((id: string) => {\r\n commandsRef.current.delete(id);\r\n setState(prev => ({ ...prev, activeCommands: Array.from(commandsRef.current.values()) }));\r\n }, []);\r\n\r\n // 2. THE ROUTER LOGIC\r\n const processTranscript = useCallback(async (transcript: string) => {\r\n const cleanText = transcript.trim().toLowerCase();\r\n setState(prev => ({ ...prev, isProcessing: true, lastTranscript: cleanText }));\r\n\r\n const allCommands = Array.from(commandsRef.current.values());\r\n\r\n // PHASE 1: EXACT MATCH (0ms Latency)\r\n const exactMatch = allCommands.find(c => c.phrase?.toLowerCase() === cleanText);\r\n \r\n if (exactMatch) {\r\n console.log(`⚡ Instant Match: \"${cleanText}\" -> ${exactMatch.id}`);\r\n exactMatch.action();\r\n setState(prev => ({ ...prev, isProcessing: false }));\r\n return;\r\n }\r\n\r\n // PHASE 2: FUZZY MATCH (AI Adapter)\r\n try {\r\n console.log(`🤖 AI Routing: \"${cleanText}\"...`);\r\n \r\n // We strip the 'action' function before sending to the AI\r\n const commandListForAI = allCommands.map(({ id, description, phrase }) => ({ \r\n id, \r\n description, \r\n phrase \r\n }));\r\n\r\n const result = await adapter(cleanText, commandListForAI);\r\n\r\n if (result.commandId) {\r\n const cmd = commandsRef.current.get(result.commandId);\r\n if (cmd) {\r\n console.log(`✅ Matched: ${cmd.id}`);\r\n cmd.action();\r\n } else {\r\n console.warn(`⚠️ Adapter returned unknown ID: ${result.commandId}`);\r\n }\r\n }\r\n } catch (error) {\r\n console.error(\"Adapter Error:\", error);\r\n } finally {\r\n setState(prev => ({ ...prev, isProcessing: false }));\r\n }\r\n }, [adapter]);\r\n\r\n return (\r\n <VoiceContext.Provider value={{ ...state, register, unregister, processTranscript }}>\r\n {children}\r\n </VoiceContext.Provider>\r\n );\r\n};\r\n\r\nexport const useVoiceContext = () => {\r\n const ctx = useContext(VoiceContext);\r\n if (!ctx) throw new Error(\"useVoiceContext must be used within a VoiceControlProvider\");\r\n return ctx;\r\n};","import { useEffect, useRef } from \"react\";\r\nimport { useVoiceContext } from \"../components/VoiceContext\";\r\nimport { VoiceCommand } from \"../types\";\r\n\r\nexport const useVoiceCommand = (command: VoiceCommand) => {\r\n const { register, unregister } = useVoiceContext();\r\n\r\n // 1. Keep a \"Ref\" to the latest command.\r\n // This allows the action function to change (e.g. updating state)\r\n // WITHOUT forcing us to unregister/re-register the command.\r\n const commandRef = useRef(command);\r\n\r\n // Always update the ref on every render\r\n useEffect(() => {\r\n commandRef.current = command;\r\n });\r\n\r\n useEffect(() => {\r\n // 2. Register a \"Proxy Command\"\r\n // Instead of registering the raw command, we register a proxy\r\n // that always calls the LATEST version stored in the ref.\r\n const proxyCommand: VoiceCommand = {\r\n id: command.id,\r\n description: command.description,\r\n phrase: command.phrase,\r\n action: () => {\r\n // When triggered, execute whatever the current action is\r\n commandRef.current.action();\r\n },\r\n };\r\n\r\n register(proxyCommand);\r\n\r\n return () => {\r\n unregister(proxyCommand.id);\r\n };\r\n // 3. Only re-register if the ID changes.\r\n // We intentionally ignore 'command' or 'command.action' changes here.\r\n }, [register, unregister, command.id]);\r\n};\r\n","import { VoiceCommand } from '../types';\r\n\r\n/**\r\n * Generates the System Prompt for the LLM.\r\n * This ensures consistent behavior across different AI providers.\r\n */\r\nexport const createSystemPrompt = (commands: Omit<VoiceCommand, 'action'>[]) => {\r\n // Format the list of commands for the AI to read\r\n const commandList = commands.map(cmd =>\r\n `- ID: \"${cmd.id}\" | Description: \"${cmd.description}\"`\r\n ).join('\\n');\r\n\r\n return `\r\nYou are a precise Voice Command Router.\r\nYour goal is to map the user's spoken input to the correct Command ID from the list below.\r\n\r\nRULES:\r\n1. Analyze the user's input and find the intent.\r\n2. Match it to the command with the most relevant \"Description\".\r\n3. Use fuzzy matching (e.g., \"Dark mode\" matches \"Toggle Theme\").\r\n4. If NO command matches the intent, return null.\r\n5. IMPORTANT: Output ONLY valid JSON. Do not include markdown formatting.\r\n\r\nAVAILABLE COMMANDS:\r\n${commandList}\r\n\r\nRESPONSE FORMAT:\r\n{ \"commandId\": \"string_id_or_null\" }\r\n`;\r\n};\r\n\r\n/**\r\n * Standardizes how the user's voice transcript is presented to the AI.\r\n */\r\nexport const createUserPrompt = (transcript: string) => {\r\n return `User Input: \"${transcript}\"`;\r\n};","import { LLMAdapter } from '../types';\r\nimport { createSystemPrompt } from '../core/prompt';\r\n\r\ninterface OpenAIConfig {\r\n apiKey: string;\r\n /** @default \"gpt-4o-mini\" */\r\n model?: string;\r\n}\r\n\r\n/**\r\n * A Factory that creates an Adapter for OpenAI.\r\n * Users call this: createOpenAIAdapter({ apiKey: '...' })\r\n */\r\nexport const createOpenAIAdapter = (config: OpenAIConfig): LLMAdapter => {\r\n return async (transcript, commands) => {\r\n\r\n // 1. Generate the optimized system instructions\r\n const systemPrompt = createSystemPrompt(commands);\r\n\r\n // 2. Call the API\r\n const response = await fetch('https://api.openai.com/v1/chat/completions', {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'Authorization': `Bearer ${config.apiKey}`\r\n },\r\n body: JSON.stringify({\r\n model: config.model || \"gpt-4o-mini\",\r\n messages: [\r\n { role: \"system\", content: systemPrompt },\r\n { role: \"user\", content: transcript }\r\n ],\r\n temperature: 0, // Deterministic results\r\n response_format: { type: \"json_object\" } // Force JSON mode\r\n })\r\n });\r\n\r\n if (!response.ok) {\r\n throw new Error(`OpenAI Adapter Failed: ${response.statusText}`);\r\n }\r\n\r\n const data = await response.json();\r\n\r\n // 3. Parse the result\r\n try {\r\n const parsed = JSON.parse(data.choices[0].message.content);\r\n return { commandId: parsed.commandId };\r\n } catch (e) {\r\n console.error(\"Failed to parse LLM response\", e);\r\n return { commandId: null };\r\n }\r\n };\r\n};","import { LLMAdapter } from \"../types\";\r\n\r\ninterface GeminiConfig {\r\n apiKey: string;\r\n /**\r\n * @default \"gemini-1.5-flash\"\r\n */\r\n model?: string;\r\n /**\r\n * System instruction to guide the style of response (Optional)\r\n */\r\n systemInstruction?: string;\r\n}\r\n\r\nexport const createGeminiAdapter = (config: GeminiConfig): LLMAdapter => {\r\n return async (transcript, commands) => {\r\n // 1. Prepare the System Prompt\r\n // We explain the task to Gemini and list the available commands\r\n const commandList = commands\r\n .map((c) => `- \"${c.id}\": ${c.description}`)\r\n .join(\"\\n\");\r\n\r\n const prompt = `\r\n You are a voice command router.\r\n The user said: \"${transcript}\"\r\n\r\n Available commands:\r\n ${commandList}\r\n\r\n Rules:\r\n 1. Return ONLY the JSON object. No markdown, no explanation.\r\n 2. Format: { \"commandId\": \"id_here\" } or { \"commandId\": null } if no match.\r\n `;\r\n\r\n // 2. Call Google Gemini API\r\n const url = `https://generativelanguage.googleapis.com/v1beta/models/${\r\n config.model || \"gemini-1.5-flash\"\r\n }:generateContent?key=${config.apiKey}`;\r\n\r\n try {\r\n const response = await fetch(url, {\r\n method: \"POST\",\r\n headers: {\r\n \"Content-Type\": \"application/json\",\r\n },\r\n body: JSON.stringify({\r\n contents: [\r\n {\r\n parts: [{ text: prompt }],\r\n },\r\n ],\r\n }),\r\n });\r\n\r\n if (!response.ok) {\r\n throw new Error(`Gemini API Error: ${response.statusText}`);\r\n }\r\n\r\n const data = await response.json();\r\n\r\n // 3. Parse Response\r\n // Gemini returns nested objects: candidates[0].content.parts[0].text\r\n const textResponse = data.candidates?.[0]?.content?.parts?.[0]?.text;\r\n\r\n if (!textResponse) return { commandId: null };\r\n\r\n // Clean up markdown code blocks if Gemini adds them (e.g. ```json ... ```)\r\n const cleanJson = textResponse.replace(/```json|```/g, \"\").trim();\r\n\r\n return JSON.parse(cleanJson);\r\n } catch (error) {\r\n console.error(\"Error routing with Gemini:\", error);\r\n return { commandId: null };\r\n }\r\n };\r\n};\r\n","import { LLMAdapter } from \"../types\";\r\n\r\ninterface ClaudeConfig {\r\n apiKey: string;\r\n /**\r\n * @default \"claude-3-5-sonnet-20240620\"\r\n */\r\n model?: string;\r\n /**\r\n * Optional custom endpoint if using a proxy (Recommended for CORS)\r\n * @default \"https://api.anthropic.com/v1/messages\"\r\n */\r\n endpoint?: string;\r\n}\r\n\r\nexport const createClaudeAdapter = (config: ClaudeConfig): LLMAdapter => {\r\n return async (transcript, commands) => {\r\n const endpoint = config.endpoint || \"https://api.anthropic.com/v1/messages\";\r\n const model = config.model || \"claude-3-5-sonnet-20240620\";\r\n\r\n // 1. Prepare Command List\r\n const commandList = commands\r\n .map((c) => `- \"${c.id}\": ${c.description}`)\r\n .join(\"\\n\");\r\n\r\n const systemPrompt = `\r\n You are a voice command router.\r\n Available commands:\r\n ${commandList}\r\n\r\n Rules:\r\n 1. Analyze the user's transcript.\r\n 2. Return a JSON object: { \"commandId\": \"id\" } or { \"commandId\": null }.\r\n 3. No markdown, no conversational text. ONLY JSON.\r\n `;\r\n\r\n try {\r\n // 2. Call Anthropic API\r\n const response = await fetch(endpoint, {\r\n method: \"POST\",\r\n headers: {\r\n \"x-api-key\": config.apiKey,\r\n \"anthropic-version\": \"2023-06-01\",\r\n \"content-type\": \"application/json\",\r\n // 'dangerously-allow-browser': 'true' // Anthropic client SDK uses this, but fetch doesn't need it.\r\n },\r\n body: JSON.stringify({\r\n model: model,\r\n max_tokens: 100,\r\n system: systemPrompt,\r\n messages: [{ role: \"user\", content: transcript }],\r\n }),\r\n });\r\n\r\n if (!response.ok) {\r\n const err = await response.text();\r\n throw new Error(`Claude API Error: ${response.status} - ${err}`);\r\n }\r\n\r\n const data = await response.json();\r\n\r\n // 3. Parse Response\r\n // Claude returns: content: [ { type: 'text', text: '...' } ]\r\n const textResponse = data.content?.[0]?.text;\r\n\r\n if (!textResponse) return { commandId: null };\r\n\r\n // Clean up potential markdown formatting\r\n const cleanJson = textResponse.replace(/```json|```/g, \"\").trim();\r\n return JSON.parse(cleanJson);\r\n } catch (error) {\r\n console.error(\"Error routing with Claude:\", error);\r\n return { commandId: null };\r\n }\r\n };\r\n};\r\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-voice-action-router",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "A headless voice action router for React",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -27,7 +27,11 @@
|
|
|
27
27
|
"build": "tsup",
|
|
28
28
|
"dev": "tsup --watch",
|
|
29
29
|
"typecheck": "tsc --noEmit",
|
|
30
|
-
"prepublishOnly": "npm run build"
|
|
30
|
+
"prepublishOnly": "npm run build",
|
|
31
|
+
"docs:dev": "vitepress dev docs",
|
|
32
|
+
"docs:build": "vitepress build docs",
|
|
33
|
+
"docs:preview": "vitepress preview docs",
|
|
34
|
+
"docs:deploy": "npm run docs:build && gh-pages -d docs/.vitepress/dist"
|
|
31
35
|
},
|
|
32
36
|
"repository": {
|
|
33
37
|
"type": "git",
|
|
@@ -43,7 +47,9 @@
|
|
|
43
47
|
"devDependencies": {
|
|
44
48
|
"@types/react": "^19.2.7",
|
|
45
49
|
"@types/react-dom": "^19.2.3",
|
|
50
|
+
"gh-pages": "^6.3.0",
|
|
46
51
|
"tsup": "^8.5.1",
|
|
47
|
-
"typescript": "^5.9.3"
|
|
52
|
+
"typescript": "^5.9.3",
|
|
53
|
+
"vitepress": "^1.6.4"
|
|
48
54
|
}
|
|
49
55
|
}
|