@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.
- package/README.md +7 -2
- package/dist/components/group-switcher.js +3 -4
- package/dist/components/voice-playback.d.ts +15 -0
- package/dist/components/voice-playback.js +86 -0
- package/dist/components/voice-subscribe-events.d.ts +0 -4
- package/dist/index.d.ts +10 -2
- package/dist/index.js +23 -2
- package/dist/operator-console/operator-console-state.d.ts +49 -0
- package/dist/operator-console/operator-console-state.js +97 -0
- package/dist/operator-console/operator-console.d.ts +32 -0
- package/dist/operator-console/operator-console.js +88 -0
- package/dist/root-layout/global-overlays.d.ts +37 -0
- package/dist/root-layout/global-overlays.js +37 -0
- package/dist/root-layout/layout-picker.d.ts +9 -0
- package/dist/root-layout/layout-picker.js +22 -0
- package/dist/root-layout/layouts/full-screen-layout.d.ts +12 -0
- package/dist/root-layout/layouts/full-screen-layout.js +60 -0
- package/dist/root-layout/layouts/tabs-with-console-layout.d.ts +20 -0
- package/dist/root-layout/layouts/tabs-with-console-layout.js +158 -0
- package/dist/root-layout/root-layout-host.d.ts +19 -0
- package/dist/root-layout/root-layout-host.js +88 -0
- package/dist/root-layout/root-layout-registry.d.ts +15 -0
- package/dist/root-layout/root-layout-registry.js +50 -0
- package/dist/root-layout/root-layout-state.d.ts +23 -0
- package/dist/root-layout/root-layout-state.js +54 -0
- package/dist/root-layout/root-layout.d.ts +30 -0
- package/dist/root-layout/root-layout.js +12 -0
- package/dist/screens/network-viewer/connection-troubleshooter.js +7 -11
- package/dist/screens/settings/settings-page.js +2 -1
- package/dist/screens/setup-user.js +2 -7
- package/dist/tabs-layout/tabs-layout.d.ts +48 -4
- package/dist/tabs-layout/tabs-layout.js +27 -53
- package/dist/tabs-layout/tabs-state.d.ts +10 -0
- package/dist/tabs-layout/tabs-state.js +33 -1
- package/package.json +3 -3
- package/src/components/group-switcher.tsx +5 -7
- package/src/components/voice-playback.tsx +97 -0
- package/src/components/voice-subscribe-events.ts +0 -4
- package/src/index.tsx +24 -2
- package/src/operator-console/operator-console-state.ts +119 -0
- package/src/operator-console/operator-console.tsx +233 -0
- package/src/root-layout/global-overlays.ts +50 -0
- package/src/root-layout/layout-picker.tsx +47 -0
- package/src/root-layout/layouts/full-screen-layout.tsx +144 -0
- package/src/root-layout/layouts/tabs-with-console-layout.tsx +260 -0
- package/src/root-layout/root-layout-host.tsx +113 -0
- package/src/root-layout/root-layout-registry.ts +51 -0
- package/src/root-layout/root-layout-state.ts +55 -0
- package/src/root-layout/root-layout.ts +32 -0
- package/src/screens/network-viewer/connection-troubleshooter.tsx +10 -14
- package/src/screens/settings/settings-page.tsx +7 -1
- package/src/screens/setup-user.tsx +2 -9
- package/src/tabs-layout/tabs-layout.tsx +28 -74
- package/src/tabs-layout/tabs-state.ts +31 -0
- package/dist/components/chat-overlay.d.ts +0 -14
- package/dist/components/chat-overlay.js +0 -477
- 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;
|