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/.claude/hooks/hook-check.sh +15 -0
- package/.claude/hooks/post-tool-voice-hook.sh +10 -0
- package/.claude/hooks/pre-tool-hook.sh +30 -0
- package/.claude/hooks/stop-hook.sh +29 -0
- package/.mcp.json +12 -0
- package/README.md +147 -0
- package/bin/cli.js +186 -0
- package/dist/chunk-IYGM5COW.js +12 -0
- package/dist/chunk-IYGM5COW.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/dist/unified-server.d.ts +1 -0
- package/dist/unified-server.js +352 -0
- package/dist/unified-server.js.map +1 -0
- package/mcp-voice-hooks-1.0.0.tgz +0 -0
- package/package.json +66 -0
- package/public/app.js +321 -0
- package/public/index.html +322 -0
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>
|