@crestapps/ai-chat-ui 2.0.0-preview.154 → 2.0.0-preview.157

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.
@@ -0,0 +1,191 @@
1
+ // Realtime (speech-to-speech) test harness client.
2
+ // Captures microphone audio as PCM16 @ 24 kHz, streams it to /Realtime/Stream over a WebSocket, and plays back the
3
+ // assistant's PCM16 audio. Shared by the MVC and Blazor sample hosts. Requires elements with these ids on the page:
4
+ // rt-start, rt-stop, rt-clear, rt-status, rt-log, rt-deployment, rt-voice, rt-instructions
5
+ (function () {
6
+ function init() {
7
+ var startBtn = document.getElementById('rt-start');
8
+ if (!startBtn || startBtn.dataset.rtInitialized) { return; }
9
+ startBtn.dataset.rtInitialized = '1';
10
+
11
+ var stopBtn = document.getElementById('rt-stop');
12
+ var clearBtn = document.getElementById('rt-clear');
13
+ var statusEl = document.getElementById('rt-status');
14
+ var logEl = document.getElementById('rt-log');
15
+ var deploymentEl = document.getElementById('rt-deployment');
16
+ var voiceEl = document.getElementById('rt-voice');
17
+ var instructionsEl = document.getElementById('rt-instructions');
18
+
19
+ var SAMPLE_RATE = 24000;
20
+
21
+ var ws = null;
22
+ var audioContext = null;
23
+ var micStream = null;
24
+ var sourceNode = null;
25
+ var processorNode = null;
26
+ var zeroGain = null;
27
+ var playHead = 0;
28
+ var scheduledSources = [];
29
+ var assistantLine = null;
30
+
31
+ function setStatus(text, cls) {
32
+ statusEl.textContent = text;
33
+ statusEl.className = 'badge ' + (cls || 'bg-secondary');
34
+ }
35
+
36
+ function appendLine(role, text) {
37
+ var wrapper = document.createElement('div');
38
+ wrapper.className = 'mb-2';
39
+ var label = document.createElement('span');
40
+ var colors = { user: 'text-primary', assistant: 'text-success', error: 'text-danger', system: 'text-muted' };
41
+ label.className = 'fw-bold me-1 ' + (colors[role] || 'text-muted');
42
+ label.textContent = role === 'user' ? 'You:' : role === 'assistant' ? 'Assistant:' : role === 'error' ? 'Error:' : 'System:';
43
+ var body = document.createElement('span');
44
+ body.textContent = text;
45
+ wrapper.appendChild(label);
46
+ wrapper.appendChild(body);
47
+ logEl.appendChild(wrapper);
48
+ logEl.scrollTop = logEl.scrollHeight;
49
+ return body;
50
+ }
51
+
52
+ function logAssistantDelta(text) {
53
+ if (!assistantLine) { assistantLine = appendLine('assistant', ''); }
54
+ assistantLine.textContent += text;
55
+ logEl.scrollTop = logEl.scrollHeight;
56
+ }
57
+
58
+ function startMic() {
59
+ sourceNode = audioContext.createMediaStreamSource(micStream);
60
+ processorNode = audioContext.createScriptProcessor(4096, 1, 1);
61
+ processorNode.onaudioprocess = function (event) {
62
+ if (!ws || ws.readyState !== WebSocket.OPEN) { return; }
63
+ var input = event.inputBuffer.getChannelData(0);
64
+ var pcm = new Int16Array(input.length);
65
+ for (var i = 0; i < input.length; i++) {
66
+ var s = Math.max(-1, Math.min(1, input[i]));
67
+ pcm[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
68
+ }
69
+ ws.send(pcm.buffer);
70
+ };
71
+ zeroGain = audioContext.createGain();
72
+ zeroGain.gain.value = 0; // keep the processor alive without echoing the mic
73
+ sourceNode.connect(processorNode);
74
+ processorNode.connect(zeroGain);
75
+ zeroGain.connect(audioContext.destination);
76
+ }
77
+
78
+ function playPcm(arrayBuffer) {
79
+ if (!audioContext) { return; }
80
+ var pcm = new Int16Array(arrayBuffer);
81
+ if (pcm.length === 0) { return; }
82
+ var f32 = new Float32Array(pcm.length);
83
+ for (var i = 0; i < pcm.length; i++) { f32[i] = pcm[i] / 0x8000; }
84
+ var buffer = audioContext.createBuffer(1, f32.length, SAMPLE_RATE);
85
+ buffer.copyToChannel(f32, 0);
86
+ var src = audioContext.createBufferSource();
87
+ src.buffer = buffer;
88
+ src.connect(audioContext.destination);
89
+ var now = audioContext.currentTime;
90
+ if (playHead < now) { playHead = now; }
91
+ src.start(playHead);
92
+ playHead += buffer.duration;
93
+ scheduledSources.push(src);
94
+ src.onended = function () { scheduledSources = scheduledSources.filter(function (s) { return s !== src; }); };
95
+ }
96
+
97
+ function flushPlayback() {
98
+ scheduledSources.forEach(function (s) { try { s.stop(); } catch (e) { /* already stopped */ } });
99
+ scheduledSources = [];
100
+ if (audioContext) { playHead = audioContext.currentTime; }
101
+ }
102
+
103
+ function handleEvent(msg) {
104
+ if (msg.type === 'transcript') {
105
+ if (msg.role === 'assistant') { logAssistantDelta(msg.text); }
106
+ else { appendLine('user', msg.text); assistantLine = null; }
107
+ } else if (msg.type === 'error') {
108
+ appendLine('error', msg.message);
109
+ setStatus('Error', 'bg-danger');
110
+ } else if (msg.type === 'event' && msg.name === 'speech_started') {
111
+ flushPlayback();
112
+ assistantLine = null;
113
+ } else if (msg.type === 'ready') {
114
+ appendLine('system', 'Connected to "' + msg.deployment + '". Start talking.');
115
+ }
116
+ }
117
+
118
+ function start() {
119
+ startBtn.disabled = true;
120
+ setStatus('Requesting mic…', 'bg-warning');
121
+ navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true, autoGainControl: true } })
122
+ .then(function (stream) {
123
+ micStream = stream;
124
+ var Ctx = window.AudioContext || window.webkitAudioContext;
125
+ audioContext = new Ctx({ sampleRate: SAMPLE_RATE });
126
+ playHead = audioContext.currentTime;
127
+
128
+ var params = new URLSearchParams({
129
+ deploymentName: deploymentEl.value,
130
+ voice: voiceEl.value,
131
+ instructions: instructionsEl.value
132
+ });
133
+ var proto = location.protocol === 'https:' ? 'wss' : 'ws';
134
+ setStatus('Connecting…', 'bg-warning');
135
+ ws = new WebSocket(proto + '://' + location.host + '/Realtime/Stream?' + params.toString());
136
+ ws.binaryType = 'arraybuffer';
137
+
138
+ ws.onopen = function () {
139
+ setStatus('Listening', 'bg-success');
140
+ stopBtn.disabled = false;
141
+ startMic();
142
+ };
143
+ ws.onmessage = function (event) {
144
+ if (typeof event.data === 'string') { handleEvent(JSON.parse(event.data)); }
145
+ else { playPcm(event.data); }
146
+ };
147
+ ws.onerror = function () { setStatus('Connection error', 'bg-danger'); };
148
+ ws.onclose = function (event) {
149
+ if (event && event.code && event.code !== 1000 && event.code !== 1005) {
150
+ appendLine('error', 'Socket closed (code ' + event.code + (event.reason ? ' — ' + event.reason : '') + ').');
151
+ }
152
+ stop(true);
153
+ };
154
+ })
155
+ .catch(function () {
156
+ setStatus('Microphone denied', 'bg-danger');
157
+ startBtn.disabled = false;
158
+ });
159
+ }
160
+
161
+ function stop(fromClose) {
162
+ stopBtn.disabled = true;
163
+ startBtn.disabled = false;
164
+ try { if (processorNode) { processorNode.disconnect(); processorNode.onaudioprocess = null; } } catch (e) { }
165
+ try { if (sourceNode) { sourceNode.disconnect(); } } catch (e) { }
166
+ try { if (zeroGain) { zeroGain.disconnect(); } } catch (e) { }
167
+ try { if (micStream) { micStream.getTracks().forEach(function (t) { t.stop(); }); } } catch (e) { }
168
+ flushPlayback();
169
+ try { if (audioContext) { audioContext.close(); } } catch (e) { }
170
+ audioContext = null;
171
+ if (!fromClose && ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
172
+ try { ws.close(); } catch (e) { }
173
+ }
174
+ if (statusEl.className.indexOf('bg-danger') === -1) { setStatus('Stopped', 'bg-secondary'); }
175
+ }
176
+
177
+ startBtn.addEventListener('click', start);
178
+ stopBtn.addEventListener('click', function () { stop(false); });
179
+ clearBtn.addEventListener('click', function () { logEl.innerHTML = ''; assistantLine = null; });
180
+ }
181
+
182
+ // Exposed so Blazor's interactive render (which mounts the DOM after the circuit connects) can trigger init
183
+ // from OnAfterRenderAsync. Static hosts (MVC) auto-init on DOMContentLoaded.
184
+ window.realtimeTest = { init: init };
185
+
186
+ if (document.readyState === 'loading') {
187
+ document.addEventListener('DOMContentLoaded', init);
188
+ } else {
189
+ init();
190
+ }
191
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crestapps/ai-chat-ui",
3
- "version": "2.0.0-preview.154",
3
+ "version": "2.0.0-preview.157",
4
4
  "description": "Browser-ready JavaScript widgets and CSS for CrestApps AI chat sessions and chat interactions. Supports streaming responses, image generation, Chart.js interactive charts, document uploads, copy-to-clipboard, and feedback buttons.",
5
5
  "license": "MIT",
6
6
  "author": "CrestApps",