react-voice-action-router 1.0.2 → 1.0.3
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 +0 -10
- package/dist/index.d.ts +0 -10
- package/dist/index.js +15 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +15 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -10,16 +10,6 @@ interface VoiceContextValue extends VoiceControlState {
|
|
|
10
10
|
declare const VoiceControlProvider: React.FC<VoiceProviderProps>;
|
|
11
11
|
declare const useVoiceContext: () => VoiceContextValue;
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* The Developer Hook
|
|
15
|
-
* Wraps the lifecycle logic so the developer doesn't have to.
|
|
16
|
-
* * Usage:
|
|
17
|
-
* useVoiceCommand({
|
|
18
|
-
* id: 'nav_home',
|
|
19
|
-
* description: 'Go to home',
|
|
20
|
-
* action: () => navigate('/')
|
|
21
|
-
* });
|
|
22
|
-
*/
|
|
23
13
|
declare const useVoiceCommand: (command: VoiceCommand) => void;
|
|
24
14
|
|
|
25
15
|
export { VoiceCommand, VoiceControlProvider, VoiceControlState, useVoiceCommand, useVoiceContext };
|
package/dist/index.d.ts
CHANGED
|
@@ -10,16 +10,6 @@ interface VoiceContextValue extends VoiceControlState {
|
|
|
10
10
|
declare const VoiceControlProvider: React.FC<VoiceProviderProps>;
|
|
11
11
|
declare const useVoiceContext: () => VoiceContextValue;
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* The Developer Hook
|
|
15
|
-
* Wraps the lifecycle logic so the developer doesn't have to.
|
|
16
|
-
* * Usage:
|
|
17
|
-
* useVoiceCommand({
|
|
18
|
-
* id: 'nav_home',
|
|
19
|
-
* description: 'Go to home',
|
|
20
|
-
* action: () => navigate('/')
|
|
21
|
-
* });
|
|
22
|
-
*/
|
|
23
13
|
declare const useVoiceCommand: (command: VoiceCommand) => void;
|
|
24
14
|
|
|
25
15
|
export { VoiceCommand, VoiceControlProvider, VoiceControlState, useVoiceCommand, useVoiceContext };
|
package/dist/index.js
CHANGED
|
@@ -70,13 +70,24 @@ var useVoiceContext = () => {
|
|
|
70
70
|
};
|
|
71
71
|
var useVoiceCommand = (command) => {
|
|
72
72
|
const { register, unregister } = useVoiceContext();
|
|
73
|
-
const
|
|
73
|
+
const commandRef = react.useRef(command);
|
|
74
74
|
react.useEffect(() => {
|
|
75
|
-
|
|
75
|
+
commandRef.current = command;
|
|
76
|
+
});
|
|
77
|
+
react.useEffect(() => {
|
|
78
|
+
const proxyCommand = {
|
|
79
|
+
id: command.id,
|
|
80
|
+
description: command.description,
|
|
81
|
+
phrase: command.phrase,
|
|
82
|
+
action: () => {
|
|
83
|
+
commandRef.current.action();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
register(proxyCommand);
|
|
76
87
|
return () => {
|
|
77
|
-
unregister(
|
|
88
|
+
unregister(proxyCommand.id);
|
|
78
89
|
};
|
|
79
|
-
}, [register, unregister, command]);
|
|
90
|
+
}, [register, unregister, command.id]);
|
|
80
91
|
};
|
|
81
92
|
|
|
82
93
|
// src/core/prompt.ts
|
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\n/**\n * The Developer Hook\n * Wraps the lifecycle logic so the developer doesn't have to.\n * * Usage:\n * useVoiceCommand({\n * id: 'nav_home',\n * description: 'Go to home',\n * action: () => navigate('/')\n * });\n */\nexport const useVoiceCommand = (command: VoiceCommand) => {\n const { register, unregister } = useVoiceContext();\n\n // Use a ref to keep the ID stable across renders\n const idRef = useRef(command.id);\n\n useEffect(() => {\n // 1. Mount: Register the tool\n register(command);\n\n // 2. Unmount: Remove the tool\n return () => {\n unregister(idRef.current);\n };\n }, [register, unregister, command]); // Re-register if command definition changes\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"],"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};"]}
|
package/dist/index.mjs
CHANGED
|
@@ -68,13 +68,24 @@ var useVoiceContext = () => {
|
|
|
68
68
|
};
|
|
69
69
|
var useVoiceCommand = (command) => {
|
|
70
70
|
const { register, unregister } = useVoiceContext();
|
|
71
|
-
const
|
|
71
|
+
const commandRef = useRef(command);
|
|
72
72
|
useEffect(() => {
|
|
73
|
-
|
|
73
|
+
commandRef.current = command;
|
|
74
|
+
});
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const proxyCommand = {
|
|
77
|
+
id: command.id,
|
|
78
|
+
description: command.description,
|
|
79
|
+
phrase: command.phrase,
|
|
80
|
+
action: () => {
|
|
81
|
+
commandRef.current.action();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
register(proxyCommand);
|
|
74
85
|
return () => {
|
|
75
|
-
unregister(
|
|
86
|
+
unregister(proxyCommand.id);
|
|
76
87
|
};
|
|
77
|
-
}, [register, unregister, command]);
|
|
88
|
+
}, [register, unregister, command.id]);
|
|
78
89
|
};
|
|
79
90
|
|
|
80
91
|
// src/core/prompt.ts
|
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":";;;;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;ACjFO,IAAM,eAAA,GAAkB,CAAC,OAAA,KAA0B;AACtD,EAAA,MAAM,EAAE,QAAA,EAAU,UAAA,EAAW,GAAI,eAAA,EAAgB;AAGjD,EAAA,MAAM,KAAA,GAAQA,MAAAA,CAAO,OAAA,CAAQ,EAAE,CAAA;AAE/B,EAAA,SAAA,CAAU,MAAM;AAEZ,IAAA,QAAA,CAAS,OAAO,CAAA;AAGhB,IAAA,OAAO,MAAM;AACT,MAAA,UAAA,CAAW,MAAM,OAAO,CAAA;AAAA,IAC5B,CAAA;AAAA,EACJ,CAAA,EAAG,CAAC,QAAA,EAAU,UAAA,EAAY,OAAO,CAAC,CAAA;AACtC;;;ACvBO,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 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\n/**\n * The Developer Hook\n * Wraps the lifecycle logic so the developer doesn't have to.\n * * Usage:\n * useVoiceCommand({\n * id: 'nav_home',\n * description: 'Go to home',\n * action: () => navigate('/')\n * });\n */\nexport const useVoiceCommand = (command: VoiceCommand) => {\n const { register, unregister } = useVoiceContext();\n\n // Use a ref to keep the ID stable across renders\n const idRef = useRef(command.id);\n\n useEffect(() => {\n // 1. Mount: Register the tool\n register(command);\n\n // 2. Unmount: Remove the tool\n return () => {\n unregister(idRef.current);\n };\n }, [register, unregister, command]); // Re-register if command definition changes\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"],"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};"]}
|