mcp-voice-hooks 1.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/public/app.js ADDED
@@ -0,0 +1,321 @@
1
+ class VoiceHooksClient {
2
+ constructor() {
3
+ this.baseUrl = 'http://localhost:3000';
4
+ this.debug = localStorage.getItem('voiceHooksDebug') === 'true';
5
+ this.utteranceInput = document.getElementById('utteranceInput');
6
+ this.sendBtn = document.getElementById('sendBtn');
7
+ this.refreshBtn = document.getElementById('refreshBtn');
8
+ this.clearAllBtn = document.getElementById('clearAllBtn');
9
+ this.utterancesList = document.getElementById('utterancesList');
10
+ this.totalCount = document.getElementById('totalCount');
11
+ this.pendingCount = document.getElementById('pendingCount');
12
+ this.deliveredCount = document.getElementById('deliveredCount');
13
+
14
+ // Voice controls
15
+ this.listenBtn = document.getElementById('listenBtn');
16
+ this.listenBtnText = document.getElementById('listenBtnText');
17
+ this.listeningIndicator = document.getElementById('listeningIndicator');
18
+ this.interimText = document.getElementById('interimText');
19
+
20
+ // Speech recognition
21
+ this.recognition = null;
22
+ this.isListening = false;
23
+ this.initializeSpeechRecognition();
24
+
25
+ this.setupEventListeners();
26
+ this.loadData();
27
+
28
+ // Auto-refresh every 2 seconds
29
+ setInterval(() => this.loadData(), 2000);
30
+ }
31
+
32
+ initializeSpeechRecognition() {
33
+ // Check for browser support
34
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
35
+
36
+ if (!SpeechRecognition) {
37
+ console.error('Speech recognition not supported in this browser');
38
+ this.listenBtn.disabled = true;
39
+ this.listenBtnText.textContent = 'Not Supported';
40
+ return;
41
+ }
42
+
43
+ this.recognition = new SpeechRecognition();
44
+ this.recognition.continuous = true;
45
+ this.recognition.interimResults = true;
46
+ this.recognition.lang = 'en-US';
47
+
48
+ // Handle results
49
+ this.recognition.onresult = (event) => {
50
+ let interimTranscript = '';
51
+
52
+ for (let i = event.resultIndex; i < event.results.length; i++) {
53
+ const transcript = event.results[i][0].transcript;
54
+
55
+ if (event.results[i].isFinal) {
56
+ // User paused - send as complete utterance
57
+ this.sendVoiceUtterance(transcript);
58
+ // Clear interim text
59
+ this.interimText.textContent = '';
60
+ this.interimText.classList.remove('active');
61
+ } else {
62
+ // Still speaking - show interim results
63
+ interimTranscript += transcript;
64
+ }
65
+ }
66
+
67
+ if (interimTranscript) {
68
+ this.interimText.textContent = interimTranscript;
69
+ this.interimText.classList.add('active');
70
+ }
71
+ };
72
+
73
+ // Handle errors
74
+ this.recognition.onerror = (event) => {
75
+ console.error('Speech recognition error:', event.error);
76
+
77
+ if (event.error === 'no-speech') {
78
+ // Continue listening
79
+ return;
80
+ }
81
+
82
+ if (event.error === 'not-allowed') {
83
+ alert('Microphone access denied. Please allow microphone access to use voice input.');
84
+ } else {
85
+ alert(`Speech recognition error: ${event.error}`);
86
+ }
87
+
88
+ this.stopListening();
89
+ };
90
+
91
+ // Handle end
92
+ this.recognition.onend = () => {
93
+ if (this.isListening) {
94
+ // Restart recognition to continue listening
95
+ try {
96
+ this.recognition.start();
97
+ } catch (e) {
98
+ console.error('Failed to restart recognition:', e);
99
+ this.stopListening();
100
+ }
101
+ }
102
+ };
103
+ }
104
+
105
+ setupEventListeners() {
106
+ this.sendBtn.addEventListener('click', () => this.sendUtterance());
107
+ this.refreshBtn.addEventListener('click', () => this.loadData());
108
+ this.clearAllBtn.addEventListener('click', () => this.clearAllUtterances());
109
+ this.listenBtn.addEventListener('click', () => this.toggleListening());
110
+
111
+ this.utteranceInput.addEventListener('keypress', (e) => {
112
+ if (e.key === 'Enter') {
113
+ this.sendUtterance();
114
+ }
115
+ });
116
+ }
117
+
118
+ async sendUtterance() {
119
+ const text = this.utteranceInput.value.trim();
120
+ if (!text) return;
121
+
122
+ this.sendBtn.disabled = true;
123
+ this.sendBtn.textContent = 'Sending...';
124
+
125
+ try {
126
+ const response = await fetch(`${this.baseUrl}/api/potential-utterances`, {
127
+ method: 'POST',
128
+ headers: {
129
+ 'Content-Type': 'application/json',
130
+ },
131
+ body: JSON.stringify({
132
+ text: text,
133
+ timestamp: new Date().toISOString()
134
+ }),
135
+ });
136
+
137
+ if (response.ok) {
138
+ this.utteranceInput.value = '';
139
+ this.loadData(); // Refresh the list
140
+ } else {
141
+ const error = await response.json();
142
+ alert(`Error: ${error.error || 'Failed to send utterance'}`);
143
+ }
144
+ } catch (error) {
145
+ console.error('Failed to send utterance:', error);
146
+ alert('Failed to send utterance. Make sure the server is running.');
147
+ } finally {
148
+ this.sendBtn.disabled = false;
149
+ this.sendBtn.textContent = 'Send';
150
+ }
151
+ }
152
+
153
+ async loadData() {
154
+ try {
155
+ // Load status
156
+ const statusResponse = await fetch(`${this.baseUrl}/api/utterances/status`);
157
+ if (statusResponse.ok) {
158
+ const status = await statusResponse.json();
159
+ this.updateStatus(status);
160
+ }
161
+
162
+ // Load utterances
163
+ const utterancesResponse = await fetch(`${this.baseUrl}/api/utterances?limit=20`);
164
+ if (utterancesResponse.ok) {
165
+ const data = await utterancesResponse.json();
166
+ this.updateUtterancesList(data.utterances);
167
+ }
168
+ } catch (error) {
169
+ console.error('Failed to load data:', error);
170
+ }
171
+ }
172
+
173
+ updateStatus(status) {
174
+ this.totalCount.textContent = status.total;
175
+ this.pendingCount.textContent = status.pending;
176
+ this.deliveredCount.textContent = status.delivered;
177
+ }
178
+
179
+ updateUtterancesList(utterances) {
180
+ if (utterances.length === 0) {
181
+ this.utterancesList.innerHTML = '<div class="empty-state">No utterances yet. Type something above to get started!</div>';
182
+ return;
183
+ }
184
+
185
+ this.utterancesList.innerHTML = utterances.map(utterance => `
186
+ <div class="utterance-item">
187
+ <div class="utterance-text">${this.escapeHtml(utterance.text)}</div>
188
+ <div class="utterance-meta">
189
+ <div>${this.formatTimestamp(utterance.timestamp)}</div>
190
+ <div class="utterance-status status-${utterance.status}">
191
+ ${utterance.status.toUpperCase()}
192
+ </div>
193
+ </div>
194
+ </div>
195
+ `).join('');
196
+ }
197
+
198
+ formatTimestamp(timestamp) {
199
+ const date = new Date(timestamp);
200
+ return date.toLocaleTimeString();
201
+ }
202
+
203
+ escapeHtml(text) {
204
+ const div = document.createElement('div');
205
+ div.textContent = text;
206
+ return div.innerHTML;
207
+ }
208
+
209
+ toggleListening() {
210
+ if (this.isListening) {
211
+ this.stopListening();
212
+ } else {
213
+ this.startListening();
214
+ }
215
+ }
216
+
217
+ startListening() {
218
+ if (!this.recognition) {
219
+ alert('Speech recognition not supported in this browser');
220
+ return;
221
+ }
222
+
223
+ try {
224
+ this.recognition.start();
225
+ this.isListening = true;
226
+ this.listenBtn.classList.add('listening');
227
+ this.listenBtnText.textContent = 'Stop Listening';
228
+ this.listeningIndicator.classList.add('active');
229
+ this.debugLog('Started listening');
230
+ } catch (e) {
231
+ console.error('Failed to start recognition:', e);
232
+ alert('Failed to start speech recognition. Please try again.');
233
+ }
234
+ }
235
+
236
+ stopListening() {
237
+ if (this.recognition) {
238
+ this.isListening = false;
239
+ this.recognition.stop();
240
+ this.listenBtn.classList.remove('listening');
241
+ this.listenBtnText.textContent = 'Start Listening';
242
+ this.listeningIndicator.classList.remove('active');
243
+ this.interimText.textContent = '';
244
+ this.interimText.classList.remove('active');
245
+ this.debugLog('Stopped listening');
246
+ }
247
+ }
248
+
249
+ async sendVoiceUtterance(text) {
250
+ const trimmedText = text.trim();
251
+ if (!trimmedText) return;
252
+
253
+ this.debugLog('Sending voice utterance:', trimmedText);
254
+
255
+ try {
256
+ const response = await fetch(`${this.baseUrl}/api/potential-utterances`, {
257
+ method: 'POST',
258
+ headers: {
259
+ 'Content-Type': 'application/json',
260
+ },
261
+ body: JSON.stringify({
262
+ text: trimmedText,
263
+ timestamp: new Date().toISOString()
264
+ }),
265
+ });
266
+
267
+ if (response.ok) {
268
+ this.loadData(); // Refresh the list
269
+ } else {
270
+ const error = await response.json();
271
+ console.error('Error sending voice utterance:', error);
272
+ }
273
+ } catch (error) {
274
+ console.error('Failed to send voice utterance:', error);
275
+ }
276
+ }
277
+
278
+ async clearAllUtterances() {
279
+ if (!confirm('Are you sure you want to clear all utterances?')) {
280
+ return;
281
+ }
282
+
283
+ this.clearAllBtn.disabled = true;
284
+ this.clearAllBtn.textContent = 'Clearing...';
285
+
286
+ try {
287
+ const response = await fetch(`${this.baseUrl}/api/utterances`, {
288
+ method: 'DELETE',
289
+ headers: {
290
+ 'Content-Type': 'application/json',
291
+ }
292
+ });
293
+
294
+ if (response.ok) {
295
+ const result = await response.json();
296
+ this.loadData(); // Refresh the list
297
+ this.debugLog('Cleared all utterances:', result);
298
+ } else {
299
+ const error = await response.json();
300
+ alert(`Error: ${error.error || 'Failed to clear utterances'}`);
301
+ }
302
+ } catch (error) {
303
+ console.error('Failed to clear utterances:', error);
304
+ alert('Failed to clear utterances. Make sure the server is running.');
305
+ } finally {
306
+ this.clearAllBtn.disabled = false;
307
+ this.clearAllBtn.textContent = 'Clear All';
308
+ }
309
+ }
310
+
311
+ debugLog(...args) {
312
+ if (this.debug) {
313
+ console.log(...args);
314
+ }
315
+ }
316
+ }
317
+
318
+ // Initialize the client when the page loads
319
+ document.addEventListener('DOMContentLoaded', () => {
320
+ new VoiceHooksClient();
321
+ });
@@ -0,0 +1,322 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Voice Hooks - MCP POC</title>
7
+ <style>
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10
+ max-width: 800px;
11
+ margin: 0 auto;
12
+ padding: 20px;
13
+ background-color: #f5f5f5;
14
+ }
15
+
16
+ .container {
17
+ background: white;
18
+ border-radius: 12px;
19
+ padding: 24px;
20
+ box-shadow: 0 2px 12px rgba(0,0,0,0.1);
21
+ }
22
+
23
+ h1 {
24
+ color: #333;
25
+ margin-bottom: 8px;
26
+ }
27
+
28
+ .subtitle {
29
+ color: #666;
30
+ margin-bottom: 24px;
31
+ }
32
+
33
+ .input-section {
34
+ margin-bottom: 24px;
35
+ }
36
+
37
+ .input-group {
38
+ display: flex;
39
+ gap: 12px;
40
+ margin-bottom: 16px;
41
+ }
42
+
43
+ #utteranceInput {
44
+ flex: 1;
45
+ padding: 12px;
46
+ border: 2px solid #ddd;
47
+ border-radius: 8px;
48
+ font-size: 16px;
49
+ }
50
+
51
+ #utteranceInput:focus {
52
+ outline: none;
53
+ border-color: #007AFF;
54
+ }
55
+
56
+ #sendBtn {
57
+ background: #007AFF;
58
+ color: white;
59
+ border: none;
60
+ padding: 12px 24px;
61
+ border-radius: 8px;
62
+ font-size: 16px;
63
+ cursor: pointer;
64
+ }
65
+
66
+ #sendBtn:hover {
67
+ background: #0056CC;
68
+ }
69
+
70
+ #sendBtn:disabled {
71
+ background: #ccc;
72
+ cursor: not-allowed;
73
+ }
74
+
75
+ .status {
76
+ display: flex;
77
+ gap: 24px;
78
+ margin-bottom: 24px;
79
+ }
80
+
81
+ .status-item {
82
+ padding: 12px;
83
+ background: #f8f9fa;
84
+ border-radius: 8px;
85
+ text-align: center;
86
+ flex: 1;
87
+ }
88
+
89
+ .status-number {
90
+ font-size: 24px;
91
+ font-weight: bold;
92
+ color: #007AFF;
93
+ }
94
+
95
+ .status-label {
96
+ font-size: 14px;
97
+ color: #666;
98
+ }
99
+
100
+ .utterances-section h3 {
101
+ color: #333;
102
+ margin-bottom: 16px;
103
+ }
104
+
105
+ .utterances-list {
106
+ max-height: 400px;
107
+ overflow-y: auto;
108
+ border: 1px solid #ddd;
109
+ border-radius: 8px;
110
+ }
111
+
112
+ .utterance-item {
113
+ padding: 12px 16px;
114
+ border-bottom: 1px solid #eee;
115
+ display: flex;
116
+ justify-content: space-between;
117
+ align-items: center;
118
+ }
119
+
120
+ .utterance-item:last-child {
121
+ border-bottom: none;
122
+ }
123
+
124
+ .utterance-text {
125
+ flex: 1;
126
+ margin-right: 12px;
127
+ }
128
+
129
+ .utterance-meta {
130
+ font-size: 12px;
131
+ color: #666;
132
+ text-align: right;
133
+ }
134
+
135
+ .utterance-status {
136
+ display: inline-block;
137
+ padding: 2px 8px;
138
+ border-radius: 12px;
139
+ font-size: 11px;
140
+ font-weight: bold;
141
+ margin-top: 4px;
142
+ }
143
+
144
+ .status-pending {
145
+ background: #FFF3CD;
146
+ color: #856404;
147
+ }
148
+
149
+ .status-delivered {
150
+ background: #D1ECF1;
151
+ color: #0C5460;
152
+ }
153
+
154
+ .refresh-btn {
155
+ background: #6C757D;
156
+ color: white;
157
+ border: none;
158
+ padding: 8px 16px;
159
+ border-radius: 6px;
160
+ font-size: 14px;
161
+ cursor: pointer;
162
+ margin-left: 12px;
163
+ }
164
+
165
+ .refresh-btn:hover {
166
+ background: #545B62;
167
+ }
168
+
169
+ .empty-state {
170
+ text-align: center;
171
+ color: #666;
172
+ padding: 40px 20px;
173
+ font-style: italic;
174
+ }
175
+
176
+ .voice-controls {
177
+ display: flex;
178
+ gap: 12px;
179
+ align-items: center;
180
+ margin-bottom: 16px;
181
+ }
182
+
183
+ #listenBtn {
184
+ background: #28A745;
185
+ color: white;
186
+ border: none;
187
+ padding: 12px 24px;
188
+ border-radius: 8px;
189
+ font-size: 16px;
190
+ cursor: pointer;
191
+ display: flex;
192
+ align-items: center;
193
+ gap: 8px;
194
+ }
195
+
196
+ #listenBtn:hover {
197
+ background: #218838;
198
+ }
199
+
200
+ #listenBtn.listening {
201
+ background: #DC3545;
202
+ }
203
+
204
+ #listenBtn.listening:hover {
205
+ background: #C82333;
206
+ }
207
+
208
+ #listenBtn:disabled {
209
+ background: #ccc;
210
+ cursor: not-allowed;
211
+ }
212
+
213
+ .listening-indicator {
214
+ display: none;
215
+ align-items: center;
216
+ gap: 8px;
217
+ color: #DC3545;
218
+ font-weight: 500;
219
+ }
220
+
221
+ .listening-indicator.active {
222
+ display: flex;
223
+ }
224
+
225
+ .listening-dot {
226
+ width: 8px;
227
+ height: 8px;
228
+ background: #DC3545;
229
+ border-radius: 50%;
230
+ animation: pulse 1.5s infinite;
231
+ }
232
+
233
+ @keyframes pulse {
234
+ 0% { opacity: 1; }
235
+ 50% { opacity: 0.3; }
236
+ 100% { opacity: 1; }
237
+ }
238
+
239
+ .interim-text {
240
+ display: none;
241
+ padding: 12px;
242
+ background: #F8F9FA;
243
+ border: 1px solid #DEE2E6;
244
+ border-radius: 8px;
245
+ margin-bottom: 16px;
246
+ font-style: italic;
247
+ color: #6C757D;
248
+ }
249
+
250
+ .interim-text.active {
251
+ display: block;
252
+ }
253
+
254
+ .mic-icon {
255
+ width: 16px;
256
+ height: 16px;
257
+ fill: currentColor;
258
+ }
259
+ </style>
260
+ </head>
261
+ <body>
262
+ <div class="container">
263
+ <h1>Voice Hooks - MCP POC</h1>
264
+ <p class="subtitle">Speak or type utterances to test the MCP voice interaction system</p>
265
+
266
+ <div class="input-section">
267
+ <div class="voice-controls">
268
+ <button id="listenBtn">
269
+ <svg class="mic-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
270
+ <path d="M12 1C10.34 1 9 2.34 9 4V12C9 13.66 10.34 15 12 15C13.66 15 15 13.66 15 12V4C15 2.34 13.66 1 12 1ZM19 12C19 15.53 16.39 18.44 13 18.93V22H11V18.93C7.61 18.44 5 15.53 5 12H7C7 14.76 9.24 17 12 17C14.76 17 17 14.76 17 12H19Z"/>
271
+ </svg>
272
+ <span id="listenBtnText">Start Listening</span>
273
+ </button>
274
+ <div class="listening-indicator" id="listeningIndicator">
275
+ <div class="listening-dot"></div>
276
+ <span>Listening...</span>
277
+ </div>
278
+ </div>
279
+
280
+ <div class="interim-text" id="interimText"></div>
281
+
282
+ <div class="input-group">
283
+ <input
284
+ type="text"
285
+ id="utteranceInput"
286
+ placeholder="Type your utterance here..."
287
+ autofocus
288
+ >
289
+ <button id="sendBtn">Send</button>
290
+ </div>
291
+ </div>
292
+
293
+ <div class="status">
294
+ <div class="status-item">
295
+ <div class="status-number" id="totalCount">0</div>
296
+ <div class="status-label">Total</div>
297
+ </div>
298
+ <div class="status-item">
299
+ <div class="status-number" id="pendingCount">0</div>
300
+ <div class="status-label">Pending</div>
301
+ </div>
302
+ <div class="status-item">
303
+ <div class="status-number" id="deliveredCount">0</div>
304
+ <div class="status-label">Delivered</div>
305
+ </div>
306
+ </div>
307
+
308
+ <div class="utterances-section">
309
+ <h3>
310
+ Recent Utterances
311
+ <button class="refresh-btn" id="refreshBtn">Refresh</button>
312
+ <button class="refresh-btn" id="clearAllBtn" style="background: #DC3545; margin-left: 8px;">Clear All</button>
313
+ </h3>
314
+ <div class="utterances-list" id="utterancesList">
315
+ <div class="empty-state">No utterances yet. Type something above to get started!</div>
316
+ </div>
317
+ </div>
318
+ </div>
319
+
320
+ <script src="app.js"></script>
321
+ </body>
322
+ </html>