@peers-app/peers-ui 0.19.10 → 0.19.13

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 (57) hide show
  1. package/README.md +7 -2
  2. package/dist/components/group-switcher.js +3 -4
  3. package/dist/components/voice-playback.d.ts +15 -0
  4. package/dist/components/voice-playback.js +86 -0
  5. package/dist/components/voice-subscribe-events.d.ts +0 -4
  6. package/dist/index.d.ts +10 -2
  7. package/dist/index.js +23 -2
  8. package/dist/operator-console/operator-console-state.d.ts +49 -0
  9. package/dist/operator-console/operator-console-state.js +97 -0
  10. package/dist/operator-console/operator-console.d.ts +32 -0
  11. package/dist/operator-console/operator-console.js +88 -0
  12. package/dist/root-layout/global-overlays.d.ts +37 -0
  13. package/dist/root-layout/global-overlays.js +37 -0
  14. package/dist/root-layout/layout-picker.d.ts +9 -0
  15. package/dist/root-layout/layout-picker.js +22 -0
  16. package/dist/root-layout/layouts/full-screen-layout.d.ts +12 -0
  17. package/dist/root-layout/layouts/full-screen-layout.js +60 -0
  18. package/dist/root-layout/layouts/tabs-with-console-layout.d.ts +20 -0
  19. package/dist/root-layout/layouts/tabs-with-console-layout.js +158 -0
  20. package/dist/root-layout/root-layout-host.d.ts +19 -0
  21. package/dist/root-layout/root-layout-host.js +88 -0
  22. package/dist/root-layout/root-layout-registry.d.ts +15 -0
  23. package/dist/root-layout/root-layout-registry.js +50 -0
  24. package/dist/root-layout/root-layout-state.d.ts +23 -0
  25. package/dist/root-layout/root-layout-state.js +54 -0
  26. package/dist/root-layout/root-layout.d.ts +30 -0
  27. package/dist/root-layout/root-layout.js +12 -0
  28. package/dist/screens/network-viewer/connection-troubleshooter.js +7 -11
  29. package/dist/screens/settings/settings-page.js +2 -1
  30. package/dist/screens/setup-user.js +2 -7
  31. package/dist/tabs-layout/tabs-layout.d.ts +48 -4
  32. package/dist/tabs-layout/tabs-layout.js +27 -53
  33. package/dist/tabs-layout/tabs-state.d.ts +10 -0
  34. package/dist/tabs-layout/tabs-state.js +33 -1
  35. package/package.json +3 -3
  36. package/src/components/group-switcher.tsx +5 -7
  37. package/src/components/voice-playback.tsx +97 -0
  38. package/src/components/voice-subscribe-events.ts +0 -4
  39. package/src/index.tsx +24 -2
  40. package/src/operator-console/operator-console-state.ts +119 -0
  41. package/src/operator-console/operator-console.tsx +233 -0
  42. package/src/root-layout/global-overlays.ts +50 -0
  43. package/src/root-layout/layout-picker.tsx +47 -0
  44. package/src/root-layout/layouts/full-screen-layout.tsx +144 -0
  45. package/src/root-layout/layouts/tabs-with-console-layout.tsx +260 -0
  46. package/src/root-layout/root-layout-host.tsx +113 -0
  47. package/src/root-layout/root-layout-registry.ts +51 -0
  48. package/src/root-layout/root-layout-state.ts +55 -0
  49. package/src/root-layout/root-layout.ts +32 -0
  50. package/src/screens/network-viewer/connection-troubleshooter.tsx +10 -14
  51. package/src/screens/settings/settings-page.tsx +7 -1
  52. package/src/screens/setup-user.tsx +2 -9
  53. package/src/tabs-layout/tabs-layout.tsx +28 -74
  54. package/src/tabs-layout/tabs-state.ts +31 -0
  55. package/dist/components/chat-overlay.d.ts +0 -14
  56. package/dist/components/chat-overlay.js +0 -477
  57. package/src/components/chat-overlay.tsx +0 -854
@@ -1,477 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ChatOverlay = void 0;
4
- const jsx_runtime_1 = require("react/jsx-runtime");
5
- /**
6
- * Chat Overlay Component
7
- *
8
- * Floating UI element combining voice input with chat functionality.
9
- * Shows minimized voice button that expands to full chat overlay.
10
- * Supports both voice and text input in the same thread.
11
- */
12
- const peers_sdk_1 = require("@peers-app/peers-sdk");
13
- const lodash_1 = require("lodash");
14
- const react_1 = require("react");
15
- const globals_1 = require("../globals");
16
- const message_compose_1 = require("./messages/message-compose");
17
- const message_display_1 = require("./messages/message-display");
18
- const DEFAULT_VOICE_SETTINGS = {
19
- enabled: false,
20
- voiceOutputEnabled: true,
21
- wakeWord: "COMPUTER",
22
- wakeWordSensitivity: 0.5,
23
- speechSensitivity: 0.3,
24
- sttProvider: "auto",
25
- ttsProvider: "browser",
26
- ttsSpeed: 1.0,
27
- };
28
- // Persistent var for tracking the chat overlay thread (synced across devices)
29
- let chatOverlayThreadPvar = null;
30
- function getChatOverlayThreadPvar() {
31
- if (!chatOverlayThreadPvar) {
32
- chatOverlayThreadPvar = (0, peers_sdk_1.groupUserVar)("chatOverlayThreadId", {
33
- defaultValue: null,
34
- });
35
- }
36
- return chatOverlayThreadPvar;
37
- }
38
- const voiceHubActive = (0, peers_sdk_1.deviceVar)("voiceHub:active", { defaultValue: false });
39
- // Voice settings pvar (shared with voice-settings.tsx and voice-service.ts)
40
- const voiceSettingsPvar = (0, peers_sdk_1.userVar)("voiceSettings", {
41
- defaultValue: DEFAULT_VOICE_SETTINGS,
42
- });
43
- const DEFAULT_POSITION = { x: 20, y: 20 };
44
- const overlayPositionPvar = (0, peers_sdk_1.deviceVar)("chatOverlayPosition", {
45
- defaultValue: DEFAULT_POSITION,
46
- });
47
- const ChatOverlay = ({ position: _position = "bottom-right", }) => {
48
- const [voiceState, setVoiceState] = (0, react_1.useState)("disabled");
49
- const [_transcription, setTranscription] = (0, react_1.useState)("");
50
- const [volumeLevel, setVolumeLevel] = (0, react_1.useState)(0);
51
- const [error, setError] = (0, react_1.useState)(null);
52
- const [isExpanded, setIsExpanded] = (0, react_1.useState)(false);
53
- const [showSettings, setShowSettings] = (0, react_1.useState)(false);
54
- const [threadId, setThreadId] = (0, react_1.useState)(null);
55
- const [messages, setMessages] = (0, react_1.useState)([]);
56
- const [parentMessage, setParentMessage] = (0, react_1.useState)(null);
57
- const [voiceSettings, setVoiceSettings] = (0, react_1.useState)(DEFAULT_VOICE_SETTINGS);
58
- const [overlayPosition, setOverlayPosition] = (0, react_1.useState)(DEFAULT_POSITION);
59
- const [isDragging, setIsDragging] = (0, react_1.useState)(false);
60
- const [pendingInitialMessage, setPendingInitialMessage] = (0, react_1.useState)(undefined);
61
- const messagesEndRef = (0, react_1.useRef)(null);
62
- const messagesContainerRef = (0, react_1.useRef)(null);
63
- const currentAudioRef = (0, react_1.useRef)(null);
64
- const dragStartRef = (0, react_1.useRef)(null);
65
- const scrollToBottom = (0, react_1.useCallback)(() => {
66
- setTimeout(() => {
67
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
68
- }, 100);
69
- }, []);
70
- // Load thread from pvar on mount
71
- (0, react_1.useEffect)(() => {
72
- const pvar = getChatOverlayThreadPvar();
73
- pvar.loadingPromise.then(() => {
74
- const savedThreadId = pvar();
75
- if (savedThreadId) {
76
- setThreadId(savedThreadId);
77
- // Also sync to voice service
78
- peers_sdk_1.rpcServerCalls.voiceSetThreadId?.(savedThreadId).catch(() => { });
79
- }
80
- });
81
- // Subscribe to pvar changes (from other devices)
82
- const sub = pvar.subscribe((newThreadId) => {
83
- if (newThreadId !== threadId) {
84
- setThreadId(newThreadId);
85
- peers_sdk_1.rpcServerCalls.voiceSetThreadId?.(newThreadId).catch(() => { });
86
- }
87
- });
88
- return () => sub.dispose();
89
- }, [threadId]);
90
- // Load voice settings from pvar
91
- (0, react_1.useEffect)(() => {
92
- voiceSettingsPvar.loadingPromise.then(() => {
93
- const saved = voiceSettingsPvar();
94
- if (saved) {
95
- setVoiceSettings(saved);
96
- }
97
- });
98
- // Subscribe to settings changes
99
- const sub = voiceSettingsPvar.subscribe((newSettings) => {
100
- if (newSettings) {
101
- setVoiceSettings(newSettings);
102
- }
103
- });
104
- return () => sub.dispose();
105
- }, []);
106
- // Load overlay position from pvar
107
- (0, react_1.useEffect)(() => {
108
- overlayPositionPvar.loadingPromise.then(() => {
109
- const saved = overlayPositionPvar();
110
- if (saved) {
111
- setOverlayPosition(saved);
112
- }
113
- });
114
- }, []);
115
- // Drag handlers
116
- const handleDragStart = (0, react_1.useCallback)((e) => {
117
- // Only start drag on middle-click or when holding shift
118
- if (e.button === 1 || e.shiftKey) {
119
- e.preventDefault();
120
- setIsDragging(true);
121
- dragStartRef.current = {
122
- mouseX: e.clientX,
123
- mouseY: e.clientY,
124
- posX: overlayPosition.x,
125
- posY: overlayPosition.y,
126
- };
127
- }
128
- }, [overlayPosition]);
129
- (0, react_1.useEffect)(() => {
130
- if (!isDragging)
131
- return;
132
- const handleMouseMove = (e) => {
133
- if (!dragStartRef.current)
134
- return;
135
- const deltaX = dragStartRef.current.mouseX - e.clientX;
136
- const deltaY = dragStartRef.current.mouseY - e.clientY;
137
- const newX = Math.max(10, Math.min(window.innerWidth - 80, dragStartRef.current.posX + deltaX));
138
- const newY = Math.max(10, Math.min(window.innerHeight - 80, dragStartRef.current.posY + deltaY));
139
- setOverlayPosition({ x: newX, y: newY });
140
- };
141
- const handleMouseUp = () => {
142
- setIsDragging(false);
143
- dragStartRef.current = null;
144
- // Save position
145
- overlayPositionPvar(overlayPosition);
146
- };
147
- document.addEventListener("mousemove", handleMouseMove);
148
- document.addEventListener("mouseup", handleMouseUp);
149
- return () => {
150
- document.removeEventListener("mousemove", handleMouseMove);
151
- document.removeEventListener("mouseup", handleMouseUp);
152
- };
153
- }, [isDragging, overlayPosition]);
154
- // Scroll to bottom when expanded or messages change
155
- (0, react_1.useEffect)(() => {
156
- if (isExpanded && messagesContainerRef.current) {
157
- // Use setTimeout to ensure DOM has updated
158
- setTimeout(() => {
159
- if (messagesContainerRef.current) {
160
- messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
161
- }
162
- }, 50);
163
- }
164
- }, [isExpanded]);
165
- // Subscribe to voice events
166
- (0, react_1.useEffect)(() => {
167
- const subscriptions = [
168
- (0, peers_sdk_1.subscribe)("chat:openWithMessage", (event) => {
169
- const { message } = event.data || {};
170
- if (message) {
171
- const text = typeof message === "string" ? message : message.message;
172
- setPendingInitialMessage(text);
173
- }
174
- setIsExpanded(true);
175
- setShowSettings(false);
176
- }),
177
- (0, peers_sdk_1.subscribe)("voice:stateChanged", (event) => {
178
- setVoiceState(event.data.state);
179
- if (event.data.state === "idle") {
180
- setTranscription("");
181
- setError(null);
182
- }
183
- if (event.data.state === "recording" && !voiceHubActive()) {
184
- setIsExpanded(true);
185
- }
186
- }),
187
- (0, peers_sdk_1.subscribe)("voice:transcription", (event) => {
188
- setTranscription(event.data.text);
189
- }),
190
- (0, peers_sdk_1.subscribe)("voice:volumeLevel", (event) => {
191
- setVolumeLevel(event.data.level);
192
- }),
193
- (0, peers_sdk_1.subscribe)("voice:error", (event) => {
194
- setError(event.data.message);
195
- }),
196
- (0, peers_sdk_1.subscribe)("voice:threadChanged", (event) => {
197
- const newThreadId = event.data.threadId;
198
- setThreadId(newThreadId);
199
- // Sync to pvar
200
- const pvar = getChatOverlayThreadPvar();
201
- pvar(newThreadId);
202
- }),
203
- ];
204
- // Load initial state
205
- peers_sdk_1.rpcServerCalls
206
- .voiceGetState()
207
- .then(({ state }) => {
208
- setVoiceState(state);
209
- })
210
- .catch(() => { });
211
- // Load initial thread from voice service
212
- peers_sdk_1.rpcServerCalls
213
- .voiceGetThreadId?.()
214
- .then((id) => {
215
- if (id) {
216
- setThreadId(id);
217
- const pvar = getChatOverlayThreadPvar();
218
- pvar(id);
219
- }
220
- })
221
- .catch(() => { });
222
- return () => {
223
- for (const sub of subscriptions) {
224
- sub.unsubscribe();
225
- }
226
- };
227
- }, []);
228
- // Handle browser TTS events
229
- (0, react_1.useEffect)(() => {
230
- const speakHandler = (0, peers_sdk_1.subscribe)("voice:speakText", (event) => {
231
- const { text, voice, rate } = event.data;
232
- if ("speechSynthesis" in window) {
233
- const utterance = new SpeechSynthesisUtterance(text);
234
- if (voice) {
235
- const voices = speechSynthesis.getVoices();
236
- const selectedVoice = voices.find((v) => v.name === voice);
237
- if (selectedVoice)
238
- utterance.voice = selectedVoice;
239
- }
240
- if (rate)
241
- utterance.rate = rate;
242
- utterance.onend = () => {
243
- peers_sdk_1.rpcServerCalls.voiceNotifyPlaybackComplete?.().catch(() => { });
244
- };
245
- utterance.onerror = () => {
246
- peers_sdk_1.rpcServerCalls.voiceNotifyPlaybackComplete?.().catch(() => { });
247
- };
248
- speechSynthesis.speak(utterance);
249
- }
250
- else {
251
- peers_sdk_1.rpcServerCalls.voiceNotifyPlaybackComplete?.().catch(() => { });
252
- }
253
- });
254
- const stopHandler = (0, peers_sdk_1.subscribe)("voice:stopSpeaking", () => {
255
- if ("speechSynthesis" in window) {
256
- speechSynthesis.cancel();
257
- }
258
- if (currentAudioRef.current) {
259
- currentAudioRef.current.pause();
260
- currentAudioRef.current = null;
261
- }
262
- });
263
- const playHandler = (0, peers_sdk_1.subscribe)("voice:playAudio", async (event) => {
264
- const { audioBase64, mimeType } = event.data;
265
- try {
266
- const audioData = Uint8Array.from(atob(audioBase64), (c) => c.charCodeAt(0));
267
- const blob = new Blob([audioData], { type: mimeType });
268
- const url = URL.createObjectURL(blob);
269
- const audio = new Audio(url);
270
- currentAudioRef.current = audio;
271
- audio.onended = () => {
272
- URL.revokeObjectURL(url);
273
- currentAudioRef.current = null;
274
- peers_sdk_1.rpcServerCalls.voiceNotifyPlaybackComplete?.().catch(() => { });
275
- };
276
- await audio.play();
277
- }
278
- catch (e) {
279
- console.error("Failed to play audio:", e);
280
- currentAudioRef.current = null;
281
- peers_sdk_1.rpcServerCalls.voiceNotifyPlaybackComplete?.().catch(() => { });
282
- }
283
- });
284
- return () => {
285
- speakHandler.unsubscribe();
286
- stopHandler.unsubscribe();
287
- playHandler.unsubscribe();
288
- };
289
- }, []);
290
- // Load messages when threadId changes
291
- (0, react_1.useEffect)(() => {
292
- if (!threadId) {
293
- setMessages([]);
294
- setParentMessage(null);
295
- return;
296
- }
297
- const currentThreadId = threadId;
298
- async function loadMessages() {
299
- const parent = await (0, peers_sdk_1.Messages)().get(currentThreadId);
300
- setParentMessage(parent || null);
301
- const msgs = await (0, peers_sdk_1.Messages)().list({ messageParentId: currentThreadId }, { sortBy: ["createdAt", "messageId"] });
302
- setMessages(msgs);
303
- scrollToBottom();
304
- }
305
- void loadMessages();
306
- // Subscribe to message changes
307
- const sub = (0, peers_sdk_1.Messages)().dataChanged.subscribe((evt) => {
308
- if (evt.dataObject.messageParentId === currentThreadId ||
309
- evt.dataObject.messageId === currentThreadId) {
310
- void loadMessages();
311
- }
312
- });
313
- return () => {
314
- sub.unsubscribe();
315
- };
316
- }, [threadId, scrollToBottom]);
317
- const updateVoiceSetting = (0, react_1.useCallback)((key, value) => {
318
- const newSettings = { ...voiceSettings, [key]: value };
319
- setVoiceSettings(newSettings);
320
- voiceSettingsPvar(newSettings);
321
- }, [voiceSettings]);
322
- const handleVoiceClick = (0, react_1.useCallback)(async () => {
323
- try {
324
- if (voiceState === "disabled") {
325
- window.location.hash = "#settings?tab=voice";
326
- return;
327
- }
328
- if (voiceState === "recording") {
329
- await peers_sdk_1.rpcServerCalls.voiceStopRecording();
330
- }
331
- else if (voiceState === "speaking") {
332
- await peers_sdk_1.rpcServerCalls.voiceStopPlayback();
333
- }
334
- else if (voiceState === "idle" || voiceState === "listening") {
335
- await peers_sdk_1.rpcServerCalls.voiceStartRecording();
336
- }
337
- }
338
- catch (e) {
339
- console.error("Voice action failed:", e);
340
- }
341
- }, [voiceState]);
342
- const startNewThread = (0, react_1.useCallback)(() => {
343
- setThreadId(null);
344
- setMessages([]);
345
- setParentMessage(null);
346
- // Clear in voice service and pvar
347
- peers_sdk_1.rpcServerCalls.voiceSetThreadId?.(null).catch(() => { });
348
- getChatOverlayThreadPvar()(null);
349
- }, []);
350
- const handleMessageSubmit = (0, react_1.useCallback)(async (message) => {
351
- // Notify voice service of text activity (resets inactivity timer)
352
- peers_sdk_1.rpcServerCalls.voiceNotifyTextActivity?.().catch(() => { });
353
- // Clear any pending initial message that was pre-filled
354
- setPendingInitialMessage(undefined);
355
- // If no thread yet, this message becomes the thread root
356
- if (!threadId) {
357
- message.messageParentId = undefined;
358
- await (0, peers_sdk_1.Messages)().save(message);
359
- setThreadId(message.messageId);
360
- // Sync to voice service and pvar
361
- peers_sdk_1.rpcServerCalls.voiceSetThreadId?.(message.messageId).catch(() => { });
362
- getChatOverlayThreadPvar()(message.messageId);
363
- }
364
- else {
365
- message.messageParentId = threadId;
366
- await (0, peers_sdk_1.Messages)().save(message);
367
- }
368
- // Add to local state immediately for responsiveness
369
- setMessages((prev) => (0, lodash_1.sortBy)([...prev, message], "createdAt"));
370
- scrollToBottom();
371
- }, [threadId, scrollToBottom]);
372
- const getVoiceIcon = () => {
373
- switch (voiceState) {
374
- case "disabled":
375
- return "bi-mic-mute";
376
- case "idle":
377
- return "bi-mic";
378
- case "listening":
379
- return "bi-ear";
380
- case "recording":
381
- return "bi-mic-fill";
382
- case "processing":
383
- return "bi-hourglass-split";
384
- case "speaking":
385
- return "bi-volume-up-fill";
386
- default:
387
- return "bi-mic";
388
- }
389
- };
390
- const getStateText = () => {
391
- switch (voiceState) {
392
- case "disabled":
393
- return "Voice disabled";
394
- case "idle":
395
- return "Say wake word or click";
396
- case "listening":
397
- return "Listening...";
398
- case "recording":
399
- return "Recording...";
400
- case "processing":
401
- return "Processing...";
402
- case "speaking":
403
- return "Speaking... (click to stop)";
404
- default:
405
- return "";
406
- }
407
- };
408
- const getButtonClass = () => {
409
- const base = "btn rounded-circle shadow";
410
- switch (voiceState) {
411
- case "disabled":
412
- return `${base} btn-secondary`;
413
- case "idle":
414
- return `${base} btn-outline-primary`;
415
- case "listening":
416
- return `${base} btn-info`;
417
- case "recording":
418
- return `${base} btn-danger`;
419
- case "processing":
420
- return `${base} btn-warning`;
421
- case "speaking":
422
- return `${base} btn-success`;
423
- default:
424
- return `${base} btn-secondary`;
425
- }
426
- };
427
- const positionStyles = {
428
- position: "fixed",
429
- zIndex: 1050,
430
- bottom: `${overlayPosition.y}px`,
431
- right: `${overlayPosition.x}px`,
432
- cursor: isDragging ? "grabbing" : undefined,
433
- };
434
- return ((0, jsx_runtime_1.jsxs)("div", { style: positionStyles, children: [isExpanded && ((0, jsx_runtime_1.jsxs)("div", { className: "card shadow mb-2", style: {
435
- width: "400px",
436
- maxHeight: "70vh",
437
- backgroundColor: "var(--bs-body-bg)",
438
- display: "flex",
439
- flexDirection: "column",
440
- }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "card-header py-2 px-3 d-flex justify-content-between align-items-center", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-2", children: [(0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-outline-secondary", onClick: startNewThread, title: "Start new thread", children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-plus-lg" }) }), (0, jsx_runtime_1.jsx)("button", { className: `btn btn-sm ${showSettings ? "btn-secondary" : "btn-outline-secondary"}`, onClick: () => setShowSettings(!showSettings), title: "Voice settings", children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-gear" }) }), (0, jsx_runtime_1.jsx)("small", { className: "text-muted", children: showSettings ? "Settings" : threadId ? "Thread" : "New conversation" })] }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-link p-0", onClick: () => setIsExpanded(false), children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-chevron-down" }) })] }), showSettings && ((0, jsx_runtime_1.jsxs)("div", { className: "card-body p-3", style: { maxHeight: "50vh", overflowY: "auto" }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center justify-content-between mb-2", children: [(0, jsx_runtime_1.jsx)("label", { className: "form-label mb-0", children: "Voice Input" }), (0, jsx_runtime_1.jsxs)("div", { className: "form-check form-switch mb-0", children: [(0, jsx_runtime_1.jsx)("input", { className: "form-check-input", type: "checkbox", id: "voiceEnabledToggle", checked: voiceSettings.enabled, onChange: (e) => updateVoiceSetting("enabled", e.target.checked), role: "switch" }), (0, jsx_runtime_1.jsx)("label", { className: "form-check-label", htmlFor: "voiceEnabledToggle", children: voiceSettings.enabled ? "On" : "Off" })] })] }), voiceSettings.enabled && ((0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center justify-content-between mb-3 ms-2", children: [(0, jsx_runtime_1.jsx)("label", { className: "form-label mb-0 small text-muted", children: "Voice Output" }), (0, jsx_runtime_1.jsxs)("div", { className: "form-check form-switch mb-0", children: [(0, jsx_runtime_1.jsx)("input", { className: "form-check-input", type: "checkbox", id: "voiceOutputEnabledToggle", checked: voiceSettings.voiceOutputEnabled !== false, onChange: (e) => updateVoiceSetting("voiceOutputEnabled", e.target.checked), role: "switch" }), (0, jsx_runtime_1.jsx)("label", { className: "form-check-label", htmlFor: "voiceOutputEnabledToggle", children: voiceSettings.voiceOutputEnabled !== false ? "On" : "Off" })] })] })), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("label", { className: "form-label small mb-1", children: ["Wake Word Sensitivity: ", voiceSettings.wakeWordSensitivity.toFixed(2)] }), (0, jsx_runtime_1.jsx)("input", { type: "range", className: "form-range", min: "0", max: "1", step: "0.05", value: voiceSettings.wakeWordSensitivity, onChange: (e) => updateVoiceSetting("wakeWordSensitivity", parseFloat(e.target.value)) }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between", children: [(0, jsx_runtime_1.jsx)("small", { className: "text-muted", style: { fontSize: "0.7rem" }, children: "Less sensitive" }), (0, jsx_runtime_1.jsx)("small", { className: "text-muted", style: { fontSize: "0.7rem" }, children: "More sensitive" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-2", children: [(0, jsx_runtime_1.jsxs)("label", { className: "form-label small mb-1", children: ["Speech Detection: ", voiceSettings.speechSensitivity.toFixed(2)] }), (0, jsx_runtime_1.jsx)("input", { type: "range", className: "form-range", min: "0", max: "1", step: "0.05", value: voiceSettings.speechSensitivity, onChange: (e) => updateVoiceSetting("speechSensitivity", parseFloat(e.target.value)) }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between", children: [(0, jsx_runtime_1.jsx)("small", { className: "text-muted", style: { fontSize: "0.7rem" }, children: "Quiet (louder speech)" }), (0, jsx_runtime_1.jsx)("small", { className: "text-muted", style: { fontSize: "0.7rem" }, children: "Noisy (softer speech)" })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "text-center mt-3", children: (0, jsx_runtime_1.jsxs)("a", { href: "#settings?tab=voice", className: "small text-muted", children: ["More voice settings ", (0, jsx_runtime_1.jsx)("i", { className: "bi bi-arrow-right" })] }) })] })), !showSettings && ((0, jsx_runtime_1.jsxs)("div", { ref: messagesContainerRef, className: "card-body p-2", style: {
441
- flex: 1,
442
- overflowY: "auto",
443
- maxHeight: "40vh",
444
- minHeight: "100px",
445
- }, children: [parentMessage && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(message_display_1.MessageDisplay, { message: parentMessage, isThreadParent: true }), (0, jsx_runtime_1.jsx)("hr", { className: "my-2" })] })), messages.map((message, index) => ((0, jsx_runtime_1.jsx)(message_display_1.MessageDisplay, { message: message, messageAbove: messages[index - 1] }, message.messageId))), error && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-danger py-1 px-2 small", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-exclamation-triangle me-1" }), error] })), (0, jsx_runtime_1.jsx)("div", { ref: messagesEndRef })] })), !showSettings && voiceState !== "disabled" && voiceState !== "idle" && ((0, jsx_runtime_1.jsx)("div", { className: "px-3 py-1 border-top", children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-2", children: [(0, jsx_runtime_1.jsx)("small", { className: "text-muted", children: getStateText() }), voiceState === "recording" && ((0, jsx_runtime_1.jsx)("div", { className: "progress flex-grow-1", style: { height: "4px" }, children: (0, jsx_runtime_1.jsx)("div", { className: "progress-bar bg-danger", style: { width: `${Math.min(100, volumeLevel * 500)}%` } }) }))] }) })), !showSettings && ((0, jsx_runtime_1.jsx)("div", { className: "card-footer p-2", children: (0, jsx_runtime_1.jsx)(message_compose_1.MessageCompose, { channelId: globals_1.me?.userId || "default", threadId: threadId || (0, peers_sdk_1.newid)(), onMessageSubmit: handleMessageSubmit, initialMessage: pendingInitialMessage }, pendingInitialMessage ? `pre-${pendingInitialMessage.slice(0, 20)}` : "default") }))] })), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2 justify-content-end", onMouseDown: handleDragStart, style: { userSelect: isDragging ? "none" : undefined }, children: [voiceSettings.enabled && isExpanded && ((0, jsx_runtime_1.jsxs)("button", { className: getButtonClass(), style: {
446
- width: "56px",
447
- height: "56px",
448
- fontSize: "24px",
449
- transition: "all 0.2s ease",
450
- position: "relative",
451
- }, onClick: handleVoiceClick, title: getStateText(), children: [(0, jsx_runtime_1.jsx)("i", { className: `bi ${getVoiceIcon()}` }), (voiceState === "listening" || voiceState === "recording") && ((0, jsx_runtime_1.jsx)("span", { className: "position-absolute", style: {
452
- top: 0,
453
- left: 0,
454
- right: 0,
455
- bottom: 0,
456
- borderRadius: "50%",
457
- border: "2px solid currentColor",
458
- animation: "pulse 1.5s ease-out infinite",
459
- } })), voiceState === "processing" && ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm position-absolute", style: { top: "4px", right: "4px" } }))] })), (0, jsx_runtime_1.jsx)("button", { className: `btn rounded-circle shadow ${isExpanded ? "btn-primary" : "btn-light"}`, style: {
460
- width: "56px",
461
- height: "56px",
462
- fontSize: "20px",
463
- }, onClick: () => setIsExpanded(!isExpanded), title: isExpanded ? "Close chat (Shift+drag to move)" : "Open chat (Shift+drag to move)", children: (0, jsx_runtime_1.jsx)("i", { className: `bi ${isExpanded ? "bi-chat-dots-fill" : "bi-chat-dots"}` }) })] }), (0, jsx_runtime_1.jsx)("style", { children: `
464
- @keyframes pulse {
465
- 0% {
466
- transform: scale(1);
467
- opacity: 1;
468
- }
469
- 100% {
470
- transform: scale(1.5);
471
- opacity: 0;
472
- }
473
- }
474
- ` })] }));
475
- };
476
- exports.ChatOverlay = ChatOverlay;
477
- exports.default = exports.ChatOverlay;