@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.
Files changed (227) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +209 -0
  3. package/bin/mark.js +27 -0
  4. package/out/renderer/assets/icon-DHo2CYph.svg +1 -0
  5. package/out/renderer/assets/index-G2fADWO-.js +4768 -0
  6. package/out/renderer/assets/index-f3mVse3c.css +1 -0
  7. package/out/renderer/assets/music-cover-CG4vi3kZ.png +0 -0
  8. package/out/renderer/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
  9. package/out/renderer/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
  10. package/out/renderer/assets/poppins-devanagari-500-normal-BIdkeU1p.woff2 +0 -0
  11. package/out/renderer/assets/poppins-devanagari-500-normal-DMPDjHtT.woff +0 -0
  12. package/out/renderer/assets/poppins-devanagari-600-normal-ClASKHrr.woff +0 -0
  13. package/out/renderer/assets/poppins-devanagari-600-normal-STEjXBNN.woff2 +0 -0
  14. package/out/renderer/assets/poppins-devanagari-700-normal-O-jipLrW.woff2 +0 -0
  15. package/out/renderer/assets/poppins-devanagari-700-normal-fHs-vx92.woff +0 -0
  16. package/out/renderer/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
  17. package/out/renderer/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
  18. package/out/renderer/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
  19. package/out/renderer/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
  20. package/out/renderer/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
  21. package/out/renderer/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
  22. package/out/renderer/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
  23. package/out/renderer/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
  24. package/out/renderer/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
  25. package/out/renderer/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
  26. package/out/renderer/assets/poppins-latin-ext-500-normal-CK-6C4Hw.woff2 +0 -0
  27. package/out/renderer/assets/poppins-latin-ext-500-normal-CgAe2rWW.woff +0 -0
  28. package/out/renderer/assets/poppins-latin-ext-600-normal-CAhIAdZj.woff2 +0 -0
  29. package/out/renderer/assets/poppins-latin-ext-600-normal-Df5ffKXP.woff +0 -0
  30. package/out/renderer/assets/poppins-latin-ext-700-normal-DctTR6Tg.woff +0 -0
  31. package/out/renderer/assets/poppins-latin-ext-700-normal-cby-RkWa.woff2 +0 -0
  32. package/out/renderer/favicon.ico +0 -0
  33. package/out/renderer/icon-256.png +0 -0
  34. package/out/renderer/icon.svg +1 -0
  35. package/out/renderer/index.html +25 -0
  36. package/out/renderer/manifest.json +25 -0
  37. package/out/renderer/robot-simulator.html +1640 -0
  38. package/package.json +106 -0
  39. package/src/cli/index.js +332 -0
  40. package/src/cli/monitor.js +413 -0
  41. package/src/cli/theme.js +347 -0
  42. package/src/launcher/MarkTray.cs +227 -0
  43. package/src/main/browser-agent.js +1132 -0
  44. package/src/main/git-service.js +90 -0
  45. package/src/main/google/google-calendar.js +69 -0
  46. package/src/main/google/google-drive.js +263 -0
  47. package/src/main/google/google-gmail.js +131 -0
  48. package/src/main/google/google-service.js +241 -0
  49. package/src/main/node-tools.js +23 -0
  50. package/src/main/pc-agent-scripts/mouse-locker.ps1 +76 -0
  51. package/src/main/pc-agent-scripts/ocr-region.ps1 +185 -0
  52. package/src/main/pc-agent-scripts/pc-daemon.ps1 +726 -0
  53. package/src/main/pc-agent-scripts/pc-overlay.ps1 +253 -0
  54. package/src/main/pc-agent-scripts/pick-folder.ps1 +33 -0
  55. package/src/main/pc-agent-scripts/read-ui.ps1 +181 -0
  56. package/src/main/pc-agent-scripts/take-screenshot.ps1 +24 -0
  57. package/src/main/pc-agent-scripts/win-action.ps1 +380 -0
  58. package/src/main/plugins/plugin-loader.js +306 -0
  59. package/src/main/skills/skill-manager.js +265 -0
  60. package/src/main/syntax-validator.js +455 -0
  61. package/src/main/task-daemon.js +117 -0
  62. package/src/main/telegram/telegram-service.js +835 -0
  63. package/src/main/tools/browser-tools.js +516 -0
  64. package/src/main/tools/file-tools.js +1126 -0
  65. package/src/main/tools/git-tools.js +79 -0
  66. package/src/main/tools/google-tools.js +566 -0
  67. package/src/main/tools/index.js +36 -0
  68. package/src/main/tools/media-tools.js +42 -0
  69. package/src/main/tools/system-tools.js +295 -0
  70. package/src/main/tools/task-tools.js +73 -0
  71. package/src/main/tools/telegram-tools.js +101 -0
  72. package/src/renderer/index.html +24 -0
  73. package/src/renderer/public/favicon.ico +0 -0
  74. package/src/renderer/public/icon-256.png +0 -0
  75. package/src/renderer/public/icon.svg +1 -0
  76. package/src/renderer/public/manifest.json +25 -0
  77. package/src/renderer/public/robot-simulator.html +1640 -0
  78. package/src/renderer/src/App.jsx +304 -0
  79. package/src/renderer/src/api/ai/awareness.js +294 -0
  80. package/src/renderer/src/api/ai/chatSummarizer.js +79 -0
  81. package/src/renderer/src/api/ai/contextCompactor.js +129 -0
  82. package/src/renderer/src/api/ai/contextManager.js +1130 -0
  83. package/src/renderer/src/api/ai/core.js +167 -0
  84. package/src/renderer/src/api/ai/index.js +4 -0
  85. package/src/renderer/src/api/ai/memoryGroomer.js +101 -0
  86. package/src/renderer/src/api/ai/persona.js +115 -0
  87. package/src/renderer/src/api/ai/planning.js +317 -0
  88. package/src/renderer/src/api/ai/relationship.js +177 -0
  89. package/src/renderer/src/api/ai/skillSynthesizer.js +168 -0
  90. package/src/renderer/src/api/ai/tools.js +167 -0
  91. package/src/renderer/src/api/ai/utils.js +359 -0
  92. package/src/renderer/src/api/db.js +1013 -0
  93. package/src/renderer/src/api/oramaStore.js +592 -0
  94. package/src/renderer/src/api/ragPipeline.js +87 -0
  95. package/src/renderer/src/api/subagent/subagentExecutor.js +780 -0
  96. package/src/renderer/src/api/subagent/subagentPrompt.js +44 -0
  97. package/src/renderer/src/api/subagent/subagentStore.js +173 -0
  98. package/src/renderer/src/api/taskExecutor.js +18 -0
  99. package/src/renderer/src/api/taskStore.js +315 -0
  100. package/src/renderer/src/api/tools/core-tools.js +707 -0
  101. package/src/renderer/src/api/tools/index.js +151 -0
  102. package/src/renderer/src/api/turnPairMigrator.js +200 -0
  103. package/src/renderer/src/api/vectorMemory.js +136 -0
  104. package/src/renderer/src/api/wakeWord.js +101 -0
  105. package/src/renderer/src/api/web-bridge.js +885 -0
  106. package/src/renderer/src/api/webSpeech.js +178 -0
  107. package/src/renderer/src/api/workspaceRag.js +76 -0
  108. package/src/renderer/src/assets/electron.svg +10 -0
  109. package/src/renderer/src/assets/icon.svg +1 -0
  110. package/src/renderer/src/assets/main.css +589 -0
  111. package/src/renderer/src/assets/music-cover.png +0 -0
  112. package/src/renderer/src/assets/wavy-lines.svg +25 -0
  113. package/src/renderer/src/components/Chat/ApprovalBubble.jsx +127 -0
  114. package/src/renderer/src/components/Chat/CodeBlock.jsx +83 -0
  115. package/src/renderer/src/components/Chat/DurableTaskBubble.jsx +455 -0
  116. package/src/renderer/src/components/Chat/FileDiffModal.jsx +328 -0
  117. package/src/renderer/src/components/Chat/GeminiWidgets.jsx +82 -0
  118. package/src/renderer/src/components/Chat/MemoryFooterBubble.jsx +34 -0
  119. package/src/renderer/src/components/Chat/MessageBubble.jsx +459 -0
  120. package/src/renderer/src/components/Chat/PluginExecutionBubble.jsx +41 -0
  121. package/src/renderer/src/components/Chat/ThinkingBubble.jsx +316 -0
  122. package/src/renderer/src/components/Chat/YoutubeSearchBubble.jsx +43 -0
  123. package/src/renderer/src/components/Chat/YoutubeSummaryBubble.jsx +40 -0
  124. package/src/renderer/src/components/Chat/index.js +11 -0
  125. package/src/renderer/src/components/ChatList.jsx +434 -0
  126. package/src/renderer/src/components/GlobalCameraManager.jsx +67 -0
  127. package/src/renderer/src/components/Versions.jsx +15 -0
  128. package/src/renderer/src/components/YoutubeMusicPlayer.jsx +375 -0
  129. package/src/renderer/src/components/camera/CameraPreview.jsx +236 -0
  130. package/src/renderer/src/components/config/AiEngineSection.jsx +363 -0
  131. package/src/renderer/src/components/config/CameraSection.jsx +100 -0
  132. package/src/renderer/src/components/config/PermissionsSection.jsx +130 -0
  133. package/src/renderer/src/components/config/ShortcutSection.jsx +102 -0
  134. package/src/renderer/src/components/config/VoiceSection.jsx +110 -0
  135. package/src/renderer/src/components/config/index.js +5 -0
  136. package/src/renderer/src/components/core/Avatar.jsx +2197 -0
  137. package/src/renderer/src/components/core/ConfirmModal.jsx +80 -0
  138. package/src/renderer/src/components/core/DraggableHoloCard.jsx +125 -0
  139. package/src/renderer/src/components/core/FloatingMenu.jsx +272 -0
  140. package/src/renderer/src/components/core/HoloCard.jsx +64 -0
  141. package/src/renderer/src/components/core/InputBar.jsx +1346 -0
  142. package/src/renderer/src/components/core/ProcessPanel.jsx +216 -0
  143. package/src/renderer/src/components/core/ResponseArea.jsx +149 -0
  144. package/src/renderer/src/components/core/StatusIndicator.jsx +59 -0
  145. package/src/renderer/src/components/core/ThoughtNeuralFlow.jsx +168 -0
  146. package/src/renderer/src/components/core/ToolClustersDeck.jsx +210 -0
  147. package/src/renderer/src/components/core/native-skills.js +43 -0
  148. package/src/renderer/src/components/neural-core/EmotionalCortexLobe.jsx +359 -0
  149. package/src/renderer/src/components/neural-core/NodeDissectionModal.jsx +57 -0
  150. package/src/renderer/src/components/neural-core/ProceduralMatrixLobe.jsx +193 -0
  151. package/src/renderer/src/components/neural-core/ProceduralSkillModal.jsx +53 -0
  152. package/src/renderer/src/components/neural-core/ResetAiModal.jsx +198 -0
  153. package/src/renderer/src/components/neural-core/SynapticMemoryLobe.jsx +204 -0
  154. package/src/renderer/src/components/neural-core/traitMeta.js +102 -0
  155. package/src/renderer/src/components/subagent/AgentChatFeed.jsx +603 -0
  156. package/src/renderer/src/components/subagent/AgentInspector.jsx +224 -0
  157. package/src/renderer/src/components/subagent/AgentSidebar.jsx +216 -0
  158. package/src/renderer/src/components/subagent/SubagentIntercom.jsx +572 -0
  159. package/src/renderer/src/components/subagent/SubagentTopologyMap.jsx +423 -0
  160. package/src/renderer/src/contexts/ApprovalContext.jsx +425 -0
  161. package/src/renderer/src/contexts/ChatContext.jsx +61 -0
  162. package/src/renderer/src/contexts/YoutubeMusicContext.jsx +333 -0
  163. package/src/renderer/src/data/faqData.js +202 -0
  164. package/src/renderer/src/hooks/agent/executeAgentTool.js +895 -0
  165. package/src/renderer/src/hooks/agent/index.js +4 -0
  166. package/src/renderer/src/hooks/agent/useMarkMusic.js +75 -0
  167. package/src/renderer/src/hooks/agent/useMarkPlan.js +2739 -0
  168. package/src/renderer/src/hooks/agent/useMarkState.js +173 -0
  169. package/src/renderer/src/hooks/agent/useMarkYoutube.js +82 -0
  170. package/src/renderer/src/hooks/agent/useRelationalGrowth.js +201 -0
  171. package/src/renderer/src/hooks/telegram/useTelegramBot.js +63 -0
  172. package/src/renderer/src/hooks/useAwareness.js +289 -0
  173. package/src/renderer/src/hooks/useChatArchiver.js +52 -0
  174. package/src/renderer/src/hooks/useConfirm.jsx +68 -0
  175. package/src/renderer/src/hooks/useMarkAgent.js +454 -0
  176. package/src/renderer/src/hooks/useMemoryGroomer.js +152 -0
  177. package/src/renderer/src/hooks/useVAD.js +511 -0
  178. package/src/renderer/src/hooks/useWebSpeechWakeWord.js +122 -0
  179. package/src/renderer/src/main.jsx +12 -0
  180. package/src/renderer/src/pages/ChatStudio.jsx +922 -0
  181. package/src/renderer/src/pages/Configuration.jsx +360 -0
  182. package/src/renderer/src/pages/GoogleWorkspace.jsx +293 -0
  183. package/src/renderer/src/pages/Guidebook.jsx +917 -0
  184. package/src/renderer/src/pages/Knowledge.jsx +241 -0
  185. package/src/renderer/src/pages/MarkHome.jsx +491 -0
  186. package/src/renderer/src/pages/NeuralCore.jsx +786 -0
  187. package/src/renderer/src/pages/Plugins.jsx +630 -0
  188. package/src/renderer/src/pages/SkillEditor.jsx +380 -0
  189. package/src/renderer/src/pages/Skills.jsx +219 -0
  190. package/src/renderer/src/pages/Subagents.jsx +342 -0
  191. package/src/renderer/src/pages/TelegramBot.jsx +343 -0
  192. package/src/renderer/src/utils/chatContentParser.js +203 -0
  193. package/src/renderer/src/utils/toolClusters.js +201 -0
  194. package/src/server/bin/sha3_wasm_bg.wasm +0 -0
  195. package/src/server/config-manager.js +57 -0
  196. package/src/server/index.js +195 -0
  197. package/src/server/launcher.js +87 -0
  198. package/src/server/memory/db-store.js +758 -0
  199. package/src/server/memory/orama-store.js +177 -0
  200. package/src/server/memory/vector-engine.js +109 -0
  201. package/src/server/routes/ai.routes.js +280 -0
  202. package/src/server/routes/awareness.routes.js +62 -0
  203. package/src/server/routes/chat.routes.js +61 -0
  204. package/src/server/routes/config.routes.js +41 -0
  205. package/src/server/routes/index.js +35 -0
  206. package/src/server/routes/integrations.routes.js +116 -0
  207. package/src/server/routes/memory.routes.js +256 -0
  208. package/src/server/routes/plugins.routes.js +72 -0
  209. package/src/server/routes/skills.routes.js +139 -0
  210. package/src/server/routes/subagents.routes.js +70 -0
  211. package/src/server/routes/tasks.routes.js +175 -0
  212. package/src/server/routes/workspace.routes.js +127 -0
  213. package/src/server/services/ai/ai-utils.js +161 -0
  214. package/src/server/services/ai/openai-provider.js +757 -0
  215. package/src/server/services/ai/web-provider.js +729 -0
  216. package/src/server/services/ai-bridge.js +46 -0
  217. package/src/server/services/deepseek-web.js +956 -0
  218. package/src/server/services/gemini-web.js +170 -0
  219. package/src/server/services/notification-service.js +91 -0
  220. package/src/server/services/updater.js +125 -0
  221. package/src/server/tools/awareness-tracker.js +292 -0
  222. package/src/server/tools/group-tools.js +1164 -0
  223. package/src/server/tools/media-tools.js +244 -0
  224. package/src/server/tools/pc-agent.js +327 -0
  225. package/src/server/tools/screen-service.js +51 -0
  226. package/src/server/ws-hub.js +168 -0
  227. 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
+ )