@mazees/mark 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +29 -0
- package/README.md +209 -0
- package/bin/mark.js +27 -0
- package/out/renderer/assets/icon-DHo2CYph.svg +1 -0
- package/out/renderer/assets/index-G2fADWO-.js +4768 -0
- package/out/renderer/assets/index-f3mVse3c.css +1 -0
- package/out/renderer/assets/music-cover-CG4vi3kZ.png +0 -0
- package/out/renderer/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-500-normal-BIdkeU1p.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-500-normal-DMPDjHtT.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-600-normal-ClASKHrr.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-600-normal-STEjXBNN.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-700-normal-O-jipLrW.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-700-normal-fHs-vx92.woff +0 -0
- package/out/renderer/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
- package/out/renderer/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
- package/out/renderer/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
- package/out/renderer/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
- package/out/renderer/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-500-normal-CK-6C4Hw.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-500-normal-CgAe2rWW.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-600-normal-CAhIAdZj.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-600-normal-Df5ffKXP.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-700-normal-DctTR6Tg.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-700-normal-cby-RkWa.woff2 +0 -0
- package/out/renderer/favicon.ico +0 -0
- package/out/renderer/icon-256.png +0 -0
- package/out/renderer/icon.svg +1 -0
- package/out/renderer/index.html +25 -0
- package/out/renderer/manifest.json +25 -0
- package/out/renderer/robot-simulator.html +1640 -0
- package/package.json +106 -0
- package/src/cli/index.js +332 -0
- package/src/cli/monitor.js +413 -0
- package/src/cli/theme.js +347 -0
- package/src/launcher/MarkTray.cs +227 -0
- package/src/main/browser-agent.js +1132 -0
- package/src/main/git-service.js +90 -0
- package/src/main/google/google-calendar.js +69 -0
- package/src/main/google/google-drive.js +263 -0
- package/src/main/google/google-gmail.js +131 -0
- package/src/main/google/google-service.js +241 -0
- package/src/main/node-tools.js +23 -0
- package/src/main/pc-agent-scripts/mouse-locker.ps1 +76 -0
- package/src/main/pc-agent-scripts/ocr-region.ps1 +185 -0
- package/src/main/pc-agent-scripts/pc-daemon.ps1 +726 -0
- package/src/main/pc-agent-scripts/pc-overlay.ps1 +253 -0
- package/src/main/pc-agent-scripts/pick-folder.ps1 +33 -0
- package/src/main/pc-agent-scripts/read-ui.ps1 +181 -0
- package/src/main/pc-agent-scripts/take-screenshot.ps1 +24 -0
- package/src/main/pc-agent-scripts/win-action.ps1 +380 -0
- package/src/main/plugins/plugin-loader.js +306 -0
- package/src/main/skills/skill-manager.js +265 -0
- package/src/main/syntax-validator.js +455 -0
- package/src/main/task-daemon.js +117 -0
- package/src/main/telegram/telegram-service.js +835 -0
- package/src/main/tools/browser-tools.js +516 -0
- package/src/main/tools/file-tools.js +1126 -0
- package/src/main/tools/git-tools.js +79 -0
- package/src/main/tools/google-tools.js +566 -0
- package/src/main/tools/index.js +36 -0
- package/src/main/tools/media-tools.js +42 -0
- package/src/main/tools/system-tools.js +295 -0
- package/src/main/tools/task-tools.js +73 -0
- package/src/main/tools/telegram-tools.js +101 -0
- package/src/renderer/index.html +24 -0
- package/src/renderer/public/favicon.ico +0 -0
- package/src/renderer/public/icon-256.png +0 -0
- package/src/renderer/public/icon.svg +1 -0
- package/src/renderer/public/manifest.json +25 -0
- package/src/renderer/public/robot-simulator.html +1640 -0
- package/src/renderer/src/App.jsx +304 -0
- package/src/renderer/src/api/ai/awareness.js +294 -0
- package/src/renderer/src/api/ai/chatSummarizer.js +79 -0
- package/src/renderer/src/api/ai/contextCompactor.js +129 -0
- package/src/renderer/src/api/ai/contextManager.js +1130 -0
- package/src/renderer/src/api/ai/core.js +167 -0
- package/src/renderer/src/api/ai/index.js +4 -0
- package/src/renderer/src/api/ai/memoryGroomer.js +101 -0
- package/src/renderer/src/api/ai/persona.js +115 -0
- package/src/renderer/src/api/ai/planning.js +317 -0
- package/src/renderer/src/api/ai/relationship.js +177 -0
- package/src/renderer/src/api/ai/skillSynthesizer.js +168 -0
- package/src/renderer/src/api/ai/tools.js +167 -0
- package/src/renderer/src/api/ai/utils.js +359 -0
- package/src/renderer/src/api/db.js +1013 -0
- package/src/renderer/src/api/oramaStore.js +592 -0
- package/src/renderer/src/api/ragPipeline.js +87 -0
- package/src/renderer/src/api/subagent/subagentExecutor.js +780 -0
- package/src/renderer/src/api/subagent/subagentPrompt.js +44 -0
- package/src/renderer/src/api/subagent/subagentStore.js +173 -0
- package/src/renderer/src/api/taskExecutor.js +18 -0
- package/src/renderer/src/api/taskStore.js +315 -0
- package/src/renderer/src/api/tools/core-tools.js +707 -0
- package/src/renderer/src/api/tools/index.js +151 -0
- package/src/renderer/src/api/turnPairMigrator.js +200 -0
- package/src/renderer/src/api/vectorMemory.js +136 -0
- package/src/renderer/src/api/wakeWord.js +101 -0
- package/src/renderer/src/api/web-bridge.js +885 -0
- package/src/renderer/src/api/webSpeech.js +178 -0
- package/src/renderer/src/api/workspaceRag.js +76 -0
- package/src/renderer/src/assets/electron.svg +10 -0
- package/src/renderer/src/assets/icon.svg +1 -0
- package/src/renderer/src/assets/main.css +589 -0
- package/src/renderer/src/assets/music-cover.png +0 -0
- package/src/renderer/src/assets/wavy-lines.svg +25 -0
- package/src/renderer/src/components/Chat/ApprovalBubble.jsx +127 -0
- package/src/renderer/src/components/Chat/CodeBlock.jsx +83 -0
- package/src/renderer/src/components/Chat/DurableTaskBubble.jsx +455 -0
- package/src/renderer/src/components/Chat/FileDiffModal.jsx +328 -0
- package/src/renderer/src/components/Chat/GeminiWidgets.jsx +82 -0
- package/src/renderer/src/components/Chat/MemoryFooterBubble.jsx +34 -0
- package/src/renderer/src/components/Chat/MessageBubble.jsx +459 -0
- package/src/renderer/src/components/Chat/PluginExecutionBubble.jsx +41 -0
- package/src/renderer/src/components/Chat/ThinkingBubble.jsx +316 -0
- package/src/renderer/src/components/Chat/YoutubeSearchBubble.jsx +43 -0
- package/src/renderer/src/components/Chat/YoutubeSummaryBubble.jsx +40 -0
- package/src/renderer/src/components/Chat/index.js +11 -0
- package/src/renderer/src/components/ChatList.jsx +434 -0
- package/src/renderer/src/components/GlobalCameraManager.jsx +67 -0
- package/src/renderer/src/components/Versions.jsx +15 -0
- package/src/renderer/src/components/YoutubeMusicPlayer.jsx +375 -0
- package/src/renderer/src/components/camera/CameraPreview.jsx +236 -0
- package/src/renderer/src/components/config/AiEngineSection.jsx +363 -0
- package/src/renderer/src/components/config/CameraSection.jsx +100 -0
- package/src/renderer/src/components/config/PermissionsSection.jsx +130 -0
- package/src/renderer/src/components/config/ShortcutSection.jsx +102 -0
- package/src/renderer/src/components/config/VoiceSection.jsx +110 -0
- package/src/renderer/src/components/config/index.js +5 -0
- package/src/renderer/src/components/core/Avatar.jsx +2197 -0
- package/src/renderer/src/components/core/ConfirmModal.jsx +80 -0
- package/src/renderer/src/components/core/DraggableHoloCard.jsx +125 -0
- package/src/renderer/src/components/core/FloatingMenu.jsx +272 -0
- package/src/renderer/src/components/core/HoloCard.jsx +64 -0
- package/src/renderer/src/components/core/InputBar.jsx +1346 -0
- package/src/renderer/src/components/core/ProcessPanel.jsx +216 -0
- package/src/renderer/src/components/core/ResponseArea.jsx +149 -0
- package/src/renderer/src/components/core/StatusIndicator.jsx +59 -0
- package/src/renderer/src/components/core/ThoughtNeuralFlow.jsx +168 -0
- package/src/renderer/src/components/core/ToolClustersDeck.jsx +210 -0
- package/src/renderer/src/components/core/native-skills.js +43 -0
- package/src/renderer/src/components/neural-core/EmotionalCortexLobe.jsx +359 -0
- package/src/renderer/src/components/neural-core/NodeDissectionModal.jsx +57 -0
- package/src/renderer/src/components/neural-core/ProceduralMatrixLobe.jsx +193 -0
- package/src/renderer/src/components/neural-core/ProceduralSkillModal.jsx +53 -0
- package/src/renderer/src/components/neural-core/ResetAiModal.jsx +198 -0
- package/src/renderer/src/components/neural-core/SynapticMemoryLobe.jsx +204 -0
- package/src/renderer/src/components/neural-core/traitMeta.js +102 -0
- package/src/renderer/src/components/subagent/AgentChatFeed.jsx +603 -0
- package/src/renderer/src/components/subagent/AgentInspector.jsx +224 -0
- package/src/renderer/src/components/subagent/AgentSidebar.jsx +216 -0
- package/src/renderer/src/components/subagent/SubagentIntercom.jsx +572 -0
- package/src/renderer/src/components/subagent/SubagentTopologyMap.jsx +423 -0
- package/src/renderer/src/contexts/ApprovalContext.jsx +425 -0
- package/src/renderer/src/contexts/ChatContext.jsx +61 -0
- package/src/renderer/src/contexts/YoutubeMusicContext.jsx +333 -0
- package/src/renderer/src/data/faqData.js +202 -0
- package/src/renderer/src/hooks/agent/executeAgentTool.js +895 -0
- package/src/renderer/src/hooks/agent/index.js +4 -0
- package/src/renderer/src/hooks/agent/useMarkMusic.js +75 -0
- package/src/renderer/src/hooks/agent/useMarkPlan.js +2739 -0
- package/src/renderer/src/hooks/agent/useMarkState.js +173 -0
- package/src/renderer/src/hooks/agent/useMarkYoutube.js +82 -0
- package/src/renderer/src/hooks/agent/useRelationalGrowth.js +201 -0
- package/src/renderer/src/hooks/telegram/useTelegramBot.js +63 -0
- package/src/renderer/src/hooks/useAwareness.js +289 -0
- package/src/renderer/src/hooks/useChatArchiver.js +52 -0
- package/src/renderer/src/hooks/useConfirm.jsx +68 -0
- package/src/renderer/src/hooks/useMarkAgent.js +454 -0
- package/src/renderer/src/hooks/useMemoryGroomer.js +152 -0
- package/src/renderer/src/hooks/useVAD.js +511 -0
- package/src/renderer/src/hooks/useWebSpeechWakeWord.js +122 -0
- package/src/renderer/src/main.jsx +12 -0
- package/src/renderer/src/pages/ChatStudio.jsx +922 -0
- package/src/renderer/src/pages/Configuration.jsx +360 -0
- package/src/renderer/src/pages/GoogleWorkspace.jsx +293 -0
- package/src/renderer/src/pages/Guidebook.jsx +917 -0
- package/src/renderer/src/pages/Knowledge.jsx +241 -0
- package/src/renderer/src/pages/MarkHome.jsx +491 -0
- package/src/renderer/src/pages/NeuralCore.jsx +786 -0
- package/src/renderer/src/pages/Plugins.jsx +630 -0
- package/src/renderer/src/pages/SkillEditor.jsx +380 -0
- package/src/renderer/src/pages/Skills.jsx +219 -0
- package/src/renderer/src/pages/Subagents.jsx +342 -0
- package/src/renderer/src/pages/TelegramBot.jsx +343 -0
- package/src/renderer/src/utils/chatContentParser.js +203 -0
- package/src/renderer/src/utils/toolClusters.js +201 -0
- package/src/server/bin/sha3_wasm_bg.wasm +0 -0
- package/src/server/config-manager.js +57 -0
- package/src/server/index.js +195 -0
- package/src/server/launcher.js +87 -0
- package/src/server/memory/db-store.js +758 -0
- package/src/server/memory/orama-store.js +177 -0
- package/src/server/memory/vector-engine.js +109 -0
- package/src/server/routes/ai.routes.js +280 -0
- package/src/server/routes/awareness.routes.js +62 -0
- package/src/server/routes/chat.routes.js +61 -0
- package/src/server/routes/config.routes.js +41 -0
- package/src/server/routes/index.js +35 -0
- package/src/server/routes/integrations.routes.js +116 -0
- package/src/server/routes/memory.routes.js +256 -0
- package/src/server/routes/plugins.routes.js +72 -0
- package/src/server/routes/skills.routes.js +139 -0
- package/src/server/routes/subagents.routes.js +70 -0
- package/src/server/routes/tasks.routes.js +175 -0
- package/src/server/routes/workspace.routes.js +127 -0
- package/src/server/services/ai/ai-utils.js +161 -0
- package/src/server/services/ai/openai-provider.js +757 -0
- package/src/server/services/ai/web-provider.js +729 -0
- package/src/server/services/ai-bridge.js +46 -0
- package/src/server/services/deepseek-web.js +956 -0
- package/src/server/services/gemini-web.js +170 -0
- package/src/server/services/notification-service.js +91 -0
- package/src/server/services/updater.js +125 -0
- package/src/server/tools/awareness-tracker.js +292 -0
- package/src/server/tools/group-tools.js +1164 -0
- package/src/server/tools/media-tools.js +244 -0
- package/src/server/tools/pc-agent.js +327 -0
- package/src/server/tools/screen-service.js +51 -0
- package/src/server/ws-hub.js +168 -0
- package/src/shared/parsers/mark-tag-parser.js +275 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
startWebSpeechRecognition,
|
|
4
|
+
stopWebSpeechRecognition,
|
|
5
|
+
isWebSpeechSupported
|
|
6
|
+
} from '../api/webSpeech'
|
|
7
|
+
import { detectWakeWord, cleanSpokenCommand } from '../api/wakeWord'
|
|
8
|
+
import { getAllConfig } from '../api/db'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Memainkan suara beep/chime konfirmasi sci-fi lembut ("tutt-ting")
|
|
12
|
+
* menggunakan Web Audio API murni tanpa load aset eksternal.
|
|
13
|
+
*/
|
|
14
|
+
let sharedAudioContext = null
|
|
15
|
+
|
|
16
|
+
export async function playWakeChime() {
|
|
17
|
+
try {
|
|
18
|
+
const AudioCtx = window.AudioContext || window.webkitAudioContext
|
|
19
|
+
if (!AudioCtx) return
|
|
20
|
+
|
|
21
|
+
if (!sharedAudioContext || sharedAudioContext.state === 'closed') {
|
|
22
|
+
sharedAudioContext = new AudioCtx()
|
|
23
|
+
}
|
|
24
|
+
if (sharedAudioContext.state === 'suspended') {
|
|
25
|
+
await sharedAudioContext.resume()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ctx = sharedAudioContext
|
|
29
|
+
const now = ctx.currentTime
|
|
30
|
+
|
|
31
|
+
// Tone 1 (587.33 Hz - D5)
|
|
32
|
+
const osc1 = ctx.createOscillator()
|
|
33
|
+
const gain1 = ctx.createGain()
|
|
34
|
+
osc1.type = 'sine'
|
|
35
|
+
osc1.frequency.setValueAtTime(587.33, now)
|
|
36
|
+
gain1.gain.setValueAtTime(0, now)
|
|
37
|
+
gain1.gain.linearRampToValueAtTime(0.12, now + 0.02)
|
|
38
|
+
gain1.gain.exponentialRampToValueAtTime(0.001, now + 0.12)
|
|
39
|
+
osc1.connect(gain1)
|
|
40
|
+
gain1.connect(ctx.destination)
|
|
41
|
+
osc1.start(now)
|
|
42
|
+
osc1.stop(now + 0.12)
|
|
43
|
+
|
|
44
|
+
// Tone 2 (880.00 Hz - A5, lebih tinggi dan manis)
|
|
45
|
+
const osc2 = ctx.createOscillator()
|
|
46
|
+
const gain2 = ctx.createGain()
|
|
47
|
+
osc2.type = 'sine'
|
|
48
|
+
osc2.frequency.setValueAtTime(880.0, now + 0.08)
|
|
49
|
+
gain2.gain.setValueAtTime(0, now + 0.08)
|
|
50
|
+
gain2.gain.linearRampToValueAtTime(0.15, now + 0.1)
|
|
51
|
+
gain2.gain.exponentialRampToValueAtTime(0.001, now + 0.26)
|
|
52
|
+
osc2.connect(gain2)
|
|
53
|
+
gain2.connect(ctx.destination)
|
|
54
|
+
osc2.start(now + 0.08)
|
|
55
|
+
osc2.stop(now + 0.26)
|
|
56
|
+
} catch (_) {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const useVAD = ({
|
|
60
|
+
onTranscript // Function to call when STT finishes
|
|
61
|
+
}) => {
|
|
62
|
+
const [isRecording, setIsRecording] = useState(false)
|
|
63
|
+
const [isProcessing, setIsProcessing] = useState(false)
|
|
64
|
+
const [audioIntensity, setAudioIntensity] = useState(0)
|
|
65
|
+
const [toastMessage, setToastMessage] = useState('')
|
|
66
|
+
|
|
67
|
+
const isRecordingRef = useRef(false)
|
|
68
|
+
const isProcessingRef = useRef(false)
|
|
69
|
+
const isWakeListeningRef = useRef(false)
|
|
70
|
+
const wakeRecognitionRef = useRef(null)
|
|
71
|
+
const manualRecognitionRef = useRef(null)
|
|
72
|
+
const silenceTimerRef = useRef(null)
|
|
73
|
+
const currentConfigRef = useRef({})
|
|
74
|
+
const onTranscriptRef = useRef(onTranscript)
|
|
75
|
+
|
|
76
|
+
// Real-time Web Audio Analyser references
|
|
77
|
+
const audioContextRef = useRef(null)
|
|
78
|
+
const mediaStreamRef = useRef(null)
|
|
79
|
+
const analyserRef = useRef(null)
|
|
80
|
+
const animFrameRef = useRef(null)
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
onTranscriptRef.current = onTranscript
|
|
84
|
+
}, [onTranscript])
|
|
85
|
+
|
|
86
|
+
// Muat konfigurasi terbaru
|
|
87
|
+
const refreshConfig = async () => {
|
|
88
|
+
try {
|
|
89
|
+
const data = await getAllConfig()
|
|
90
|
+
if (data && data.length > 0) {
|
|
91
|
+
currentConfigRef.current = data[0] || {}
|
|
92
|
+
}
|
|
93
|
+
} catch (_) {}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
refreshConfig()
|
|
98
|
+
|
|
99
|
+
const handleConfigUpdated = (e) => {
|
|
100
|
+
if (e?.detail) {
|
|
101
|
+
currentConfigRef.current = { ...currentConfigRef.current, ...e.detail }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
window.addEventListener('config-updated', handleConfigUpdated)
|
|
105
|
+
return () => window.removeEventListener('config-updated', handleConfigUpdated)
|
|
106
|
+
}, [])
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Menghentikan audio visualizer stream & context
|
|
110
|
+
*/
|
|
111
|
+
const stopAudioAnalyser = useCallback(() => {
|
|
112
|
+
if (animFrameRef.current) {
|
|
113
|
+
cancelAnimationFrame(animFrameRef.current)
|
|
114
|
+
animFrameRef.current = null
|
|
115
|
+
}
|
|
116
|
+
if (analyserRef.current) {
|
|
117
|
+
analyserRef.current = null
|
|
118
|
+
}
|
|
119
|
+
if (mediaStreamRef.current) {
|
|
120
|
+
mediaStreamRef.current.getTracks().forEach((t) => t.stop())
|
|
121
|
+
mediaStreamRef.current = null
|
|
122
|
+
}
|
|
123
|
+
if (audioContextRef.current) {
|
|
124
|
+
try {
|
|
125
|
+
audioContextRef.current.close()
|
|
126
|
+
} catch (_) {}
|
|
127
|
+
audioContextRef.current = null
|
|
128
|
+
}
|
|
129
|
+
setAudioIntensity(0)
|
|
130
|
+
}, [])
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Memulai audio visualizer stream menggunakan Web Audio API AnalyserNode
|
|
134
|
+
*/
|
|
135
|
+
const startAudioAnalyser = useCallback(async () => {
|
|
136
|
+
try {
|
|
137
|
+
stopAudioAnalyser()
|
|
138
|
+
|
|
139
|
+
const constraints = {
|
|
140
|
+
audio: {
|
|
141
|
+
echoCancellation: true,
|
|
142
|
+
noiseSuppression: true,
|
|
143
|
+
autoGainControl: true
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const stream = await navigator.mediaDevices.getUserMedia(constraints)
|
|
148
|
+
if (!isRecordingRef.current) {
|
|
149
|
+
stream.getTracks().forEach((t) => t.stop())
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
mediaStreamRef.current = stream
|
|
154
|
+
|
|
155
|
+
const AudioCtx = window.AudioContext || window.webkitAudioContext
|
|
156
|
+
if (!AudioCtx) return
|
|
157
|
+
|
|
158
|
+
const ctx = new AudioCtx()
|
|
159
|
+
audioContextRef.current = ctx
|
|
160
|
+
|
|
161
|
+
const source = ctx.createMediaStreamSource(stream)
|
|
162
|
+
const analyser = ctx.createAnalyser()
|
|
163
|
+
analyser.fftSize = 256
|
|
164
|
+
analyser.smoothingTimeConstant = 0.4
|
|
165
|
+
analyserRef.current = analyser
|
|
166
|
+
|
|
167
|
+
source.connect(analyser)
|
|
168
|
+
|
|
169
|
+
const bufferLength = analyser.frequencyBinCount
|
|
170
|
+
const dataArray = new Uint8Array(bufferLength)
|
|
171
|
+
let lastThrottleTime = 0
|
|
172
|
+
|
|
173
|
+
const updateIntensity = () => {
|
|
174
|
+
if (!isRecordingRef.current || !analyserRef.current) {
|
|
175
|
+
setAudioIntensity(0)
|
|
176
|
+
window.dispatchEvent(new CustomEvent('mark-intensity', { detail: 0 }))
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
analyserRef.current.getByteFrequencyData(dataArray)
|
|
181
|
+
let sum = 0
|
|
182
|
+
for (let i = 0; i < bufferLength; i++) {
|
|
183
|
+
sum += dataArray[i]
|
|
184
|
+
}
|
|
185
|
+
const avg = sum / bufferLength
|
|
186
|
+
// Normalisasi 0 - 255 menjadi 0.0 - 1.0 dengan kurva sensitif untuk percakapan
|
|
187
|
+
const normalized = Math.min(1, Math.max(0, (avg - 8) / 60))
|
|
188
|
+
|
|
189
|
+
// Dispatch mark-intensity langsung ke window (didengar oleh Avatar & LiveWaveform tanpa re-render React)
|
|
190
|
+
window.dispatchEvent(new CustomEvent('mark-intensity', { detail: normalized }))
|
|
191
|
+
|
|
192
|
+
// Batasi setAudioIntensity pada ~15 FPS untuk komponen UI yang bergantung pada prop React
|
|
193
|
+
const now = performance.now()
|
|
194
|
+
if (now - lastThrottleTime >= 65) {
|
|
195
|
+
lastThrottleTime = now
|
|
196
|
+
setAudioIntensity(normalized)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
animFrameRef.current = requestAnimationFrame(updateIntensity)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
updateIntensity()
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.warn('[VAD] Audio visualizer analyser failed:', err.message)
|
|
205
|
+
// Fallback tetap memberi sedikit visual bernafas jika mic analyser terblokir
|
|
206
|
+
setAudioIntensity(0.3)
|
|
207
|
+
}
|
|
208
|
+
}, [stopAudioAnalyser])
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Menghentikan rekognisi manual secara bersih
|
|
212
|
+
*/
|
|
213
|
+
const stopManualRecording = useCallback(() => {
|
|
214
|
+
if (silenceTimerRef.current) {
|
|
215
|
+
clearTimeout(silenceTimerRef.current)
|
|
216
|
+
silenceTimerRef.current = null
|
|
217
|
+
}
|
|
218
|
+
isRecordingRef.current = false
|
|
219
|
+
setIsRecording(false)
|
|
220
|
+
stopAudioAnalyser()
|
|
221
|
+
stopWebSpeechRecognition()
|
|
222
|
+
manualRecognitionRef.current = null
|
|
223
|
+
}, [stopAudioAnalyser])
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Memulai perekaman manual suara (Push-to-Talk / Klik Tombol Mic / Shortcut)
|
|
227
|
+
*/
|
|
228
|
+
const startManualRecording = useCallback(async () => {
|
|
229
|
+
if (!isWebSpeechSupported()) {
|
|
230
|
+
setToastMessage('Web Speech API tidak didukung di browser ini.')
|
|
231
|
+
setTimeout(() => setToastMessage(''), 4000)
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Jika wake word background listener sedang aktif, stop sementara
|
|
236
|
+
if (wakeRecognitionRef.current || isWakeListeningRef.current) {
|
|
237
|
+
stopWebSpeechRecognition()
|
|
238
|
+
wakeRecognitionRef.current = null
|
|
239
|
+
isWakeListeningRef.current = false
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (silenceTimerRef.current) {
|
|
243
|
+
clearTimeout(silenceTimerRef.current)
|
|
244
|
+
silenceTimerRef.current = null
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await refreshConfig()
|
|
248
|
+
const lang = currentConfigRef.current?.speechLanguage || 'id-ID'
|
|
249
|
+
const customWakeWords = currentConfigRef.current?.customWakeWords || ''
|
|
250
|
+
|
|
251
|
+
isRecordingRef.current = true
|
|
252
|
+
setIsRecording(true)
|
|
253
|
+
setIsProcessing(false)
|
|
254
|
+
|
|
255
|
+
// Bunyikan chime feedback setiap mic manual mulai aktif
|
|
256
|
+
playWakeChime()
|
|
257
|
+
|
|
258
|
+
// Aktifkan visualizer amplitude mic seketika
|
|
259
|
+
startAudioAnalyser()
|
|
260
|
+
|
|
261
|
+
let accumulatedFinal = ''
|
|
262
|
+
let latestInterim = ''
|
|
263
|
+
|
|
264
|
+
// Buffer jeda hening (1800ms) sebelum rekaman otomatis diselesaikan
|
|
265
|
+
const resetSilenceTimeout = (delayMs = 1800) => {
|
|
266
|
+
if (silenceTimerRef.current) {
|
|
267
|
+
clearTimeout(silenceTimerRef.current)
|
|
268
|
+
}
|
|
269
|
+
silenceTimerRef.current = setTimeout(() => {
|
|
270
|
+
if (!isRecordingRef.current) return
|
|
271
|
+
const textToProcess = (accumulatedFinal || latestInterim || '').trim()
|
|
272
|
+
if (textToProcess) {
|
|
273
|
+
stopManualRecording()
|
|
274
|
+
const cleanText = cleanSpokenCommand(textToProcess, customWakeWords)
|
|
275
|
+
const textToSend = cleanText || textToProcess
|
|
276
|
+
if (textToSend) {
|
|
277
|
+
onTranscriptRef.current(textToSend, { isWakeWord: false, wakePhrase: null })
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}, delayMs)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Pasang timeout awal (8 detik) jika pengguna belum mulai berbicara
|
|
284
|
+
resetSilenceTimeout(8000)
|
|
285
|
+
|
|
286
|
+
const rec = await startWebSpeechRecognition({
|
|
287
|
+
lang,
|
|
288
|
+
continuous: true,
|
|
289
|
+
onInterim: (interim) => {
|
|
290
|
+
if (!isRecordingRef.current) return
|
|
291
|
+
latestInterim = interim
|
|
292
|
+
// Reset jeda hening saat pengguna sedang berbicara
|
|
293
|
+
resetSilenceTimeout(1800)
|
|
294
|
+
},
|
|
295
|
+
onResult: (finalText) => {
|
|
296
|
+
if (!isRecordingRef.current) return
|
|
297
|
+
if (finalText && finalText.trim()) {
|
|
298
|
+
accumulatedFinal = finalText.trim()
|
|
299
|
+
latestInterim = ''
|
|
300
|
+
// Reset jeda hening setelah potongan kalimat selesai
|
|
301
|
+
resetSilenceTimeout(1800)
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
onError: (err) => {
|
|
305
|
+
console.warn('[VAD] Speech recognition error:', err.message)
|
|
306
|
+
if (silenceTimerRef.current) {
|
|
307
|
+
clearTimeout(silenceTimerRef.current)
|
|
308
|
+
silenceTimerRef.current = null
|
|
309
|
+
}
|
|
310
|
+
stopManualRecording()
|
|
311
|
+
},
|
|
312
|
+
onEnd: () => {
|
|
313
|
+
if (silenceTimerRef.current) {
|
|
314
|
+
clearTimeout(silenceTimerRef.current)
|
|
315
|
+
silenceTimerRef.current = null
|
|
316
|
+
}
|
|
317
|
+
if (isRecordingRef.current) {
|
|
318
|
+
stopManualRecording()
|
|
319
|
+
const textToProcess = (accumulatedFinal || latestInterim || '').trim()
|
|
320
|
+
if (textToProcess) {
|
|
321
|
+
const cleanText = cleanSpokenCommand(textToProcess, customWakeWords)
|
|
322
|
+
const textToSend = cleanText || textToProcess
|
|
323
|
+
if (textToSend) {
|
|
324
|
+
onTranscriptRef.current(textToSend, { isWakeWord: false, wakePhrase: null })
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
manualRecognitionRef.current = rec
|
|
332
|
+
}, [startAudioAnalyser, stopManualRecording])
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Toggle manual voice recording
|
|
336
|
+
*/
|
|
337
|
+
const toggleRecording = useCallback(() => {
|
|
338
|
+
if (isRecordingRef.current) {
|
|
339
|
+
stopManualRecording()
|
|
340
|
+
} else {
|
|
341
|
+
startManualRecording()
|
|
342
|
+
}
|
|
343
|
+
}, [startManualRecording, stopManualRecording])
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Background Wake Word Listener Watchdog Loop
|
|
347
|
+
* Berjalan terus-menerus selama component mounted.
|
|
348
|
+
* Secara otomatis mengaktifkan kembali wake word recognition setelah manual mic selesai,
|
|
349
|
+
* setelah Mark selesai berbicara TTS, atau setelah recognition session terputus/cycling.
|
|
350
|
+
*/
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
let isMounted = true
|
|
353
|
+
let isStartingWake = false
|
|
354
|
+
|
|
355
|
+
const checkAndEnsureWakeListener = async () => {
|
|
356
|
+
if (!isMounted || isStartingWake) return
|
|
357
|
+
|
|
358
|
+
const wakeEnabled = currentConfigRef.current?.wakeWordEnabled !== false
|
|
359
|
+
|
|
360
|
+
// Jangan jalankan jika: fitur dimatikan, sedang manual recording, Mark sedang bicara TTS, atau tidak didukung
|
|
361
|
+
if (
|
|
362
|
+
!wakeEnabled ||
|
|
363
|
+
isRecordingRef.current ||
|
|
364
|
+
window.isMarkSpeaking ||
|
|
365
|
+
!isWebSpeechSupported()
|
|
366
|
+
) {
|
|
367
|
+
if (isWakeListeningRef.current) {
|
|
368
|
+
console.log('[WakeWord] Menjeda deteksi wake word latar belakang...')
|
|
369
|
+
isWakeListeningRef.current = false
|
|
370
|
+
stopWebSpeechRecognition()
|
|
371
|
+
wakeRecognitionRef.current = null
|
|
372
|
+
}
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Jika sudah mendengarkan dengan baik, biarkan
|
|
377
|
+
if (isWakeListeningRef.current) return
|
|
378
|
+
|
|
379
|
+
isStartingWake = true
|
|
380
|
+
|
|
381
|
+
const lang = currentConfigRef.current?.speechLanguage || 'id-ID'
|
|
382
|
+
const customWakeWords = currentConfigRef.current?.customWakeWords || ''
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
const rec = await startWebSpeechRecognition({
|
|
386
|
+
lang,
|
|
387
|
+
continuous: true,
|
|
388
|
+
onStart: () => {
|
|
389
|
+
if (!isMounted) return
|
|
390
|
+
isWakeListeningRef.current = true
|
|
391
|
+
console.log('[WakeWord] Standby mendengarkan kata pemicu ("Hey Mark" / "Mark")...', {
|
|
392
|
+
lang,
|
|
393
|
+
customWakeWords
|
|
394
|
+
})
|
|
395
|
+
},
|
|
396
|
+
onInterim: (interim) => {
|
|
397
|
+
if (!interim || window.isMarkSpeaking || isRecordingRef.current) return
|
|
398
|
+
console.log('[WakeWord] Hearing (interim):', interim)
|
|
399
|
+
const check = detectWakeWord(interim, customWakeWords)
|
|
400
|
+
if (check.detected) {
|
|
401
|
+
console.log('[WakeWord] Wake word terdeteksi pada interim:', check)
|
|
402
|
+
if (check.command) {
|
|
403
|
+
stopWebSpeechRecognition()
|
|
404
|
+
isWakeListeningRef.current = false
|
|
405
|
+
playWakeChime()
|
|
406
|
+
console.log('[WakeWord] Menjalankan perintah suara langsung:', check.command)
|
|
407
|
+
onTranscriptRef.current(check.command, {
|
|
408
|
+
isWakeWord: true,
|
|
409
|
+
wakePhrase: check.wakePhrase || 'Mark'
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
onResult: (finalText) => {
|
|
415
|
+
if (!finalText || window.isMarkSpeaking || isRecordingRef.current) return
|
|
416
|
+
console.log('[WakeWord] Heard (final):', finalText)
|
|
417
|
+
const check = detectWakeWord(finalText, customWakeWords)
|
|
418
|
+
if (check.detected) {
|
|
419
|
+
console.log('[WakeWord] Wake word terdeteksi pada final text:', check)
|
|
420
|
+
stopWebSpeechRecognition()
|
|
421
|
+
isWakeListeningRef.current = false
|
|
422
|
+
playWakeChime()
|
|
423
|
+
if (check.command) {
|
|
424
|
+
console.log('[WakeWord] Menjalankan perintah suara langsung:', check.command)
|
|
425
|
+
onTranscriptRef.current(check.command, {
|
|
426
|
+
isWakeWord: true,
|
|
427
|
+
wakePhrase: check.wakePhrase || 'Mark'
|
|
428
|
+
})
|
|
429
|
+
} else {
|
|
430
|
+
console.log(
|
|
431
|
+
'[WakeWord] Nama dipanggil tanpa perintah, otomatis menyalakan mic manual...'
|
|
432
|
+
)
|
|
433
|
+
startManualRecording()
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
onError: (err) => {
|
|
438
|
+
console.error('[WakeWord] Session error:', err?.message || err)
|
|
439
|
+
isWakeListeningRef.current = false
|
|
440
|
+
wakeRecognitionRef.current = null
|
|
441
|
+
},
|
|
442
|
+
onEnd: () => {
|
|
443
|
+
isWakeListeningRef.current = false
|
|
444
|
+
wakeRecognitionRef.current = null
|
|
445
|
+
if (isMounted) {
|
|
446
|
+
setTimeout(() => {
|
|
447
|
+
if (isMounted) {
|
|
448
|
+
checkAndEnsureWakeListener()
|
|
449
|
+
}
|
|
450
|
+
}, 300)
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
wakeRecognitionRef.current = rec
|
|
455
|
+
} catch (err) {
|
|
456
|
+
console.error('[WakeWord] Gagal mengaktifkan session:', err?.message || err)
|
|
457
|
+
isWakeListeningRef.current = false
|
|
458
|
+
wakeRecognitionRef.current = null
|
|
459
|
+
} finally {
|
|
460
|
+
isStartingWake = false
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Polling Watchdog santai: fail-safe setiap 3000ms
|
|
465
|
+
const watchdogInterval = setInterval(() => {
|
|
466
|
+
checkAndEnsureWakeListener()
|
|
467
|
+
}, 3000)
|
|
468
|
+
|
|
469
|
+
// Panggil langsung pada start
|
|
470
|
+
checkAndEnsureWakeListener()
|
|
471
|
+
|
|
472
|
+
return () => {
|
|
473
|
+
isMounted = false
|
|
474
|
+
clearInterval(watchdogInterval)
|
|
475
|
+
if (wakeRecognitionRef.current || isWakeListeningRef.current) {
|
|
476
|
+
console.log('[WakeWord] Mematikan background listener on unmount...')
|
|
477
|
+
isWakeListeningRef.current = false
|
|
478
|
+
stopWebSpeechRecognition()
|
|
479
|
+
wakeRecognitionRef.current = null
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}, [startManualRecording])
|
|
483
|
+
|
|
484
|
+
useEffect(() => {
|
|
485
|
+
const handleTriggerToggle = () => {
|
|
486
|
+
toggleRecording()
|
|
487
|
+
}
|
|
488
|
+
window.addEventListener('trigger-mic-toggle', handleTriggerToggle)
|
|
489
|
+
return () => window.removeEventListener('trigger-mic-toggle', handleTriggerToggle)
|
|
490
|
+
}, [toggleRecording])
|
|
491
|
+
|
|
492
|
+
useEffect(() => {
|
|
493
|
+
window.isVADRecording = isRecording
|
|
494
|
+
}, [isRecording])
|
|
495
|
+
|
|
496
|
+
useEffect(() => {
|
|
497
|
+
return () => {
|
|
498
|
+
stopAudioAnalyser()
|
|
499
|
+
}
|
|
500
|
+
}, [stopAudioAnalyser])
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
isRecording,
|
|
504
|
+
isProcessing,
|
|
505
|
+
audioIntensity,
|
|
506
|
+
toggleRecording,
|
|
507
|
+
startRecording: startManualRecording,
|
|
508
|
+
stopRecording: stopManualRecording,
|
|
509
|
+
toastMessage
|
|
510
|
+
}
|
|
511
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook untuk Passive Wake Word Detection menggunakan Web Speech API bawaan Microsoft Edge / Chromium.
|
|
5
|
+
* Berjalan secara native dengan 0 MB RAM overhead model dan konsumsi CPU minimal.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} params
|
|
8
|
+
* @param {boolean} [params.enabled=true] Status aktif
|
|
9
|
+
* @param {string} [params.wakeWord='mark'] Kata kunci pemicu
|
|
10
|
+
* @param {Function} params.onWakeWord Callback saat kata kunci terdeteksi
|
|
11
|
+
*/
|
|
12
|
+
export function useWebSpeechWakeWord({
|
|
13
|
+
enabled = true,
|
|
14
|
+
wakeWord = 'mark',
|
|
15
|
+
onWakeWord
|
|
16
|
+
}) {
|
|
17
|
+
const [isListening, setIsListening] = useState(false)
|
|
18
|
+
const [lastDetected, setLastDetected] = useState(null)
|
|
19
|
+
const recognitionRef = useRef(null)
|
|
20
|
+
const isEnabledRef = useRef(enabled)
|
|
21
|
+
const restartTimeoutRef = useRef(null)
|
|
22
|
+
|
|
23
|
+
isEnabledRef.current = enabled
|
|
24
|
+
|
|
25
|
+
const startRecognition = useCallback(() => {
|
|
26
|
+
if (typeof window === 'undefined') return
|
|
27
|
+
|
|
28
|
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
|
|
29
|
+
if (!SpeechRecognition) {
|
|
30
|
+
console.warn('[WakeWord] Web Speech API tidak didukung di browser ini.')
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (recognitionRef.current) {
|
|
35
|
+
try {
|
|
36
|
+
recognitionRef.current.abort()
|
|
37
|
+
} catch (_) {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const recognition = new SpeechRecognition()
|
|
42
|
+
recognition.continuous = true
|
|
43
|
+
recognition.interimResults = true
|
|
44
|
+
recognition.lang = 'id-ID'
|
|
45
|
+
recognition.maxAlternatives = 1
|
|
46
|
+
|
|
47
|
+
recognition.onstart = () => {
|
|
48
|
+
setIsListening(true)
|
|
49
|
+
console.log('[WakeWord] Mendengarkan wake word native (id-ID)...')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
recognition.onresult = (event) => {
|
|
53
|
+
for (let i = event.resultIndex; i < event.results.length; ++i) {
|
|
54
|
+
const transcript = event.results[i][0]?.transcript?.trim().toLowerCase() || ''
|
|
55
|
+
if (!transcript) continue
|
|
56
|
+
|
|
57
|
+
const target = wakeWord.toLowerCase()
|
|
58
|
+
const regex = new RegExp(`\\b(${target}|mak|makh|marg|mart|marck|marc)\\b`, 'i')
|
|
59
|
+
|
|
60
|
+
if (regex.test(transcript)) {
|
|
61
|
+
console.log(`[WakeWord] Kata kunci terdeteksi: "${transcript}"`)
|
|
62
|
+
setLastDetected(Date.now())
|
|
63
|
+
if (onWakeWord) {
|
|
64
|
+
onWakeWord(transcript)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
recognition.onerror = (event) => {
|
|
71
|
+
if (event.error !== 'no-speech' && event.error !== 'aborted') {
|
|
72
|
+
console.warn('[WakeWord] Recognition error:', event.error)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
recognition.onend = () => {
|
|
77
|
+
setIsListening(false)
|
|
78
|
+
// Auto-restart jika masih aktif (agar tetap mendengarkan secara kontinu)
|
|
79
|
+
if (isEnabledRef.current) {
|
|
80
|
+
if (restartTimeoutRef.current) clearTimeout(restartTimeoutRef.current)
|
|
81
|
+
restartTimeoutRef.current = setTimeout(() => {
|
|
82
|
+
if (isEnabledRef.current) {
|
|
83
|
+
startRecognition()
|
|
84
|
+
}
|
|
85
|
+
}, 300)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
recognition.start()
|
|
90
|
+
recognitionRef.current = recognition
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error('[WakeWord] Gagal memulai recognition:', err.message)
|
|
93
|
+
}
|
|
94
|
+
}, [wakeWord, onWakeWord])
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (enabled) {
|
|
98
|
+
startRecognition()
|
|
99
|
+
} else {
|
|
100
|
+
if (recognitionRef.current) {
|
|
101
|
+
try {
|
|
102
|
+
recognitionRef.current.abort()
|
|
103
|
+
} catch (_) {}
|
|
104
|
+
}
|
|
105
|
+
setIsListening(false)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
if (restartTimeoutRef.current) clearTimeout(restartTimeoutRef.current)
|
|
110
|
+
if (recognitionRef.current) {
|
|
111
|
+
try {
|
|
112
|
+
recognitionRef.current.abort()
|
|
113
|
+
} catch (_) {}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}, [enabled, startRecognition])
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
isListening,
|
|
120
|
+
lastDetected
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import './assets/main.css'
|
|
2
|
+
import './api/web-bridge.js'
|
|
3
|
+
|
|
4
|
+
import { StrictMode } from 'react'
|
|
5
|
+
import { createRoot } from 'react-dom/client'
|
|
6
|
+
import App from './App'
|
|
7
|
+
|
|
8
|
+
createRoot(document.getElementById('root')).render(
|
|
9
|
+
<StrictMode>
|
|
10
|
+
<App />
|
|
11
|
+
</StrictMode>
|
|
12
|
+
)
|