@peers-app/peers-ui 0.21.7 → 0.22.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.
Files changed (39) hide show
  1. package/dist/index.d.ts +0 -2
  2. package/dist/index.js +0 -2
  3. package/dist/screens/packages/package-versions.js +1 -1
  4. package/dist/screens/settings/settings-page.js +0 -5
  5. package/package.json +3 -3
  6. package/src/index.tsx +0 -2
  7. package/src/screens/packages/package-versions.tsx +1 -0
  8. package/src/screens/settings/settings-page.tsx +0 -5
  9. package/tsconfig.json +1 -0
  10. package/dist/components/voice-indicator.d.ts +0 -15
  11. package/dist/components/voice-indicator.js +0 -276
  12. package/dist/components/voice-playback.d.ts +0 -15
  13. package/dist/components/voice-playback.js +0 -86
  14. package/dist/components/voice-subscribe-events.d.ts +0 -28
  15. package/dist/components/voice-subscribe-events.js +0 -2
  16. package/dist/screens/settings/voice-settings-agent.d.ts +0 -2
  17. package/dist/screens/settings/voice-settings-agent.js +0 -39
  18. package/dist/screens/settings/voice-settings-api-keys.d.ts +0 -13
  19. package/dist/screens/settings/voice-settings-api-keys.js +0 -8
  20. package/dist/screens/settings/voice-settings-output.d.ts +0 -16
  21. package/dist/screens/settings/voice-settings-output.js +0 -7
  22. package/dist/screens/settings/voice-settings-providers.d.ts +0 -9
  23. package/dist/screens/settings/voice-settings-providers.js +0 -6
  24. package/dist/screens/settings/voice-settings-types.d.ts +0 -33
  25. package/dist/screens/settings/voice-settings-types.js +0 -55
  26. package/dist/screens/settings/voice-settings-wake-word.d.ts +0 -9
  27. package/dist/screens/settings/voice-settings-wake-word.js +0 -8
  28. package/dist/screens/settings/voice-settings.d.ts +0 -10
  29. package/dist/screens/settings/voice-settings.js +0 -173
  30. package/src/components/voice-indicator.tsx +0 -353
  31. package/src/components/voice-playback.tsx +0 -97
  32. package/src/components/voice-subscribe-events.ts +0 -16
  33. package/src/screens/settings/voice-settings-agent.tsx +0 -73
  34. package/src/screens/settings/voice-settings-api-keys.tsx +0 -91
  35. package/src/screens/settings/voice-settings-output.tsx +0 -126
  36. package/src/screens/settings/voice-settings-providers.tsx +0 -49
  37. package/src/screens/settings/voice-settings-types.ts +0 -94
  38. package/src/screens/settings/voice-settings-wake-word.tsx +0 -94
  39. package/src/screens/settings/voice-settings.tsx +0 -326
package/dist/index.d.ts CHANGED
@@ -6,8 +6,6 @@ export * from "./components/markdown-with-mentions";
6
6
  export * from "./components/sortable-list";
7
7
  export * from "./components/tabs";
8
8
  export { Typeahead, type TypeaheadItem } from "./components/typeahead";
9
- export * from "./components/voice-indicator";
10
- export * from "./components/voice-playback";
11
9
  export { isDesktop, mainContentPath } from "./globals";
12
10
  export * from "./hooks";
13
11
  export * from "./operator-console/operator-console";
package/dist/index.js CHANGED
@@ -24,8 +24,6 @@ __exportStar(require("./components/sortable-list"), exports);
24
24
  __exportStar(require("./components/tabs"), exports);
25
25
  var typeahead_1 = require("./components/typeahead");
26
26
  Object.defineProperty(exports, "Typeahead", { enumerable: true, get: function () { return typeahead_1.Typeahead; } });
27
- __exportStar(require("./components/voice-indicator"), exports);
28
- __exportStar(require("./components/voice-playback"), exports);
29
27
  var globals_1 = require("./globals");
30
28
  Object.defineProperty(exports, "isDesktop", { enumerable: true, get: function () { return globals_1.isDesktop; } });
31
29
  Object.defineProperty(exports, "mainContentPath", { enumerable: true, get: function () { return globals_1.mainContentPath; } });
@@ -256,5 +256,5 @@ function VersionRow(props) {
256
256
  commitVersion();
257
257
  if (e.key === "Escape")
258
258
  setEditingVersion(false);
259
- } })) : ((0, jsx_runtime_1.jsxs)("strong", { className: "me-2", style: { cursor: "pointer" }, onClick: startEditing, title: "Click to edit version", children: ["v", pv.version] })), pv.versionTag && ((0, jsx_runtime_1.jsx)("span", { className: `badge ${tagBadgeClass(pv.versionTag)} me-2`, children: pv.versionTag })), (0, jsx_runtime_1.jsx)("code", { className: "text-muted small me-2", children: pv.packageVersionHash?.substring(0, 8) }), (0, jsx_runtime_1.jsx)("small", { className: "text-muted me-2", children: formatDate(pv.createdAt) }), creatorName && (0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: ["by ", creatorName] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-1 align-items-center", children: [promoteTargets.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "dropdown", children: [(0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-outline-info dropdown-toggle", "data-bs-toggle": "dropdown", title: "Change version tag", children: "Promote" }), (0, jsx_runtime_1.jsx)("ul", { className: "dropdown-menu dropdown-menu-end", children: promoteTargets.map((tag) => ((0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsxs)("button", { className: "dropdown-item", onClick: () => props.onPromote(pv, tag), children: [currentTag, " \u2192 ", tag] }) }, tag))) })] })), isActive ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-secondary", disabled: isPinned || pinning, onClick: () => props.onPin(), title: isPinned ? "Already pinned" : "Pin to this version (disable auto-updates)", children: [pinning ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-pin-fill" })), isPinned ? " Pinned" : " Pin"] }), (0, jsx_runtime_1.jsx)("span", { className: "badge text-bg-info align-self-center", children: "active" })] })) : ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-primary", disabled: activating === pv.packageVersionId, onClick: () => props.onActivate(pv), children: [activating === pv.packageVersionId ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-1" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-check2-circle me-1" })), "Activate"] })), !isActive && ((0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-outline-danger", disabled: deleting === pv.packageVersionId, onClick: () => props.onDelete(pv), children: deleting === pv.packageVersionId ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-trash" })) }))] })] }) }));
259
+ } })) : ((0, jsx_runtime_1.jsxs)("strong", { className: "me-2", style: { cursor: "pointer" }, onClick: startEditing, title: "Click to edit version", children: ["v", pv.version] })), pv.versionTag && ((0, jsx_runtime_1.jsx)("span", { className: `badge ${tagBadgeClass(pv.versionTag)} me-2`, children: pv.versionTag })), (0, jsx_runtime_1.jsx)("code", { className: "text-muted small me-2", children: pv.packageVersionHash?.substring(0, 8) }), (0, jsx_runtime_1.jsx)("small", { className: "text-muted me-2", children: formatDate(pv.createdAt) }), creatorName && (0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: ["by ", creatorName] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-1 align-items-center", children: [promoteTargets.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "dropdown", children: [(0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-outline-info dropdown-toggle", "data-bs-toggle": "dropdown", "data-bs-popper-config": '{"strategy":"fixed"}', title: "Change version tag", children: "Promote" }), (0, jsx_runtime_1.jsx)("ul", { className: "dropdown-menu dropdown-menu-end", children: promoteTargets.map((tag) => ((0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsxs)("button", { className: "dropdown-item", onClick: () => props.onPromote(pv, tag), children: [currentTag, " \u2192 ", tag] }) }, tag))) })] })), isActive ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-secondary", disabled: isPinned || pinning, onClick: () => props.onPin(), title: isPinned ? "Already pinned" : "Pin to this version (disable auto-updates)", children: [pinning ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-pin-fill" })), isPinned ? " Pinned" : " Pin"] }), (0, jsx_runtime_1.jsx)("span", { className: "badge text-bg-info align-self-center", children: "active" })] })) : ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-primary", disabled: activating === pv.packageVersionId, onClick: () => props.onActivate(pv), children: [activating === pv.packageVersionId ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-1" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-check2-circle me-1" })), "Activate"] })), !isActive && ((0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-outline-danger", disabled: deleting === pv.packageVersionId, onClick: () => props.onDelete(pv), children: deleting === pv.packageVersionId ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-trash" })) }))] })] }) }));
260
260
  }
@@ -45,7 +45,6 @@ const hooks_1 = require("../../hooks");
45
45
  const layout_picker_1 = require("../../root-layout/layout-picker");
46
46
  const device_pairing_1 = require("../device-pairing");
47
47
  const color_mode_dropdown_1 = require("./color-mode-dropdown");
48
- const voice_settings_1 = require("./voice-settings");
49
48
  function getAppVersion() {
50
49
  return globalThis.__PEERS_APP_VERSION__ || "unknown";
51
50
  }
@@ -61,10 +60,6 @@ const SettingsPage = () => {
61
60
  name: "Appearance",
62
61
  content: (0, jsx_runtime_1.jsx)(AppearanceSettingsTab, {}),
63
62
  },
64
- {
65
- name: "Voice",
66
- content: (0, jsx_runtime_1.jsx)(voice_settings_1.VoiceSettings, {}),
67
- },
68
63
  {
69
64
  name: "Advanced",
70
65
  content: (0, jsx_runtime_1.jsx)(AdvancedSettingsTab, {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-ui",
3
- "version": "0.21.7",
3
+ "version": "0.22.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-ui.git"
@@ -28,7 +28,7 @@
28
28
  "lint:fix": "biome check --write ."
29
29
  },
30
30
  "peerDependencies": {
31
- "@peers-app/peers-sdk": "^0.21.7",
31
+ "@peers-app/peers-sdk": "^0.22.0",
32
32
  "bootstrap": "^5.3.3",
33
33
  "react": "^18.0.0",
34
34
  "react-dom": "^18.0.0"
@@ -39,7 +39,7 @@
39
39
  "@babel/preset-react": "^7.24.1",
40
40
  "@babel/preset-typescript": "^7.27.1",
41
41
  "@biomejs/biome": "2.3.11",
42
- "@peers-app/peers-sdk": "0.21.7",
42
+ "@peers-app/peers-sdk": "0.22.0",
43
43
  "@testing-library/dom": "^10.4.0",
44
44
  "@testing-library/jest-dom": "^6.6.3",
45
45
  "@testing-library/react": "^16.3.0",
package/src/index.tsx CHANGED
@@ -6,8 +6,6 @@ export * from "./components/markdown-with-mentions";
6
6
  export * from "./components/sortable-list";
7
7
  export * from "./components/tabs";
8
8
  export { Typeahead, type TypeaheadItem } from "./components/typeahead";
9
- export * from "./components/voice-indicator";
10
- export * from "./components/voice-playback";
11
9
  export { isDesktop, mainContentPath } from "./globals";
12
10
  export * from "./hooks";
13
11
  export * from "./operator-console/operator-console";
@@ -368,6 +368,7 @@ function VersionRow(props: {
368
368
  <button
369
369
  className="btn btn-sm btn-outline-info dropdown-toggle"
370
370
  data-bs-toggle="dropdown"
371
+ data-bs-popper-config='{"strategy":"fixed"}'
371
372
  title="Change version tag"
372
373
  >
373
374
  Promote
@@ -21,7 +21,6 @@ import { useObservable, useObservableState, usePromise } from "../../hooks";
21
21
  import { LayoutPicker } from "../../root-layout/layout-picker";
22
22
  import { SourceDevicePairing } from "../device-pairing";
23
23
  import { ColorModeDropdown } from "./color-mode-dropdown";
24
- import { VoiceSettings } from "./voice-settings";
25
24
 
26
25
  /** Electron file picker attaches a filesystem path on `File`. */
27
26
  type FileWithElectronPath = File & { path?: string };
@@ -54,10 +53,6 @@ export const SettingsPage: React.FC = () => {
54
53
  name: "Appearance",
55
54
  content: <AppearanceSettingsTab />,
56
55
  },
57
- {
58
- name: "Voice",
59
- content: <VoiceSettings />,
60
- },
61
56
  {
62
57
  name: "Advanced",
63
58
  content: <AdvancedSettingsTab />,
package/tsconfig.json CHANGED
@@ -5,6 +5,7 @@
5
5
  "jsx": "react-jsx",
6
6
  "declaration": true,
7
7
  "emitDeclarationOnly": false,
8
+ "rootDir": "./src",
8
9
  "outDir": "./dist",
9
10
  "strict": true,
10
11
  "noImplicitAny": true,
@@ -1,15 +0,0 @@
1
- /**
2
- * Voice Indicator Component
3
- *
4
- * Floating UI element showing voice input state.
5
- * Supports manual recording trigger via click.
6
- */
7
- import React from "react";
8
- interface VoiceIndicatorProps {
9
- /** Position of the indicator */
10
- position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
11
- /** Show expanded view with transcription */
12
- showTranscription?: boolean;
13
- }
14
- export declare const VoiceIndicator: React.FC<VoiceIndicatorProps>;
15
- export default VoiceIndicator;
@@ -1,276 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.VoiceIndicator = void 0;
37
- const jsx_runtime_1 = require("react/jsx-runtime");
38
- /**
39
- * Voice Indicator Component
40
- *
41
- * Floating UI element showing voice input state.
42
- * Supports manual recording trigger via click.
43
- */
44
- const peers_sdk_1 = require("@peers-app/peers-sdk");
45
- const react_1 = __importStar(require("react"));
46
- const VoiceIndicator = ({ position = "bottom-right", showTranscription = true, }) => {
47
- const [state, setState] = (0, react_1.useState)("disabled");
48
- const [transcription, setTranscription] = (0, react_1.useState)("");
49
- const [volumeLevel, setVolumeLevel] = (0, react_1.useState)(0);
50
- const [error, setError] = (0, react_1.useState)(null);
51
- const [isExpanded, setIsExpanded] = (0, react_1.useState)(false);
52
- // Track current audio element for cloud TTS so we can stop it
53
- const currentAudioRef = react_1.default.useRef(null);
54
- // Subscribe to voice events
55
- (0, react_1.useEffect)(() => {
56
- const subscriptions = [
57
- (0, peers_sdk_1.subscribe)("voice:stateChanged", (event) => {
58
- setState(event.data.state);
59
- if (event.data.state === "idle") {
60
- setTranscription("");
61
- setError(null);
62
- }
63
- if (event.data.state === "recording") {
64
- setIsExpanded(true);
65
- }
66
- }),
67
- (0, peers_sdk_1.subscribe)("voice:transcription", (event) => {
68
- setTranscription(event.data.text);
69
- }),
70
- (0, peers_sdk_1.subscribe)("voice:volumeLevel", (event) => {
71
- setVolumeLevel(event.data.level);
72
- }),
73
- (0, peers_sdk_1.subscribe)("voice:error", (event) => {
74
- setError(event.data.message);
75
- }),
76
- ];
77
- // Load initial state
78
- peers_sdk_1.rpcServerCalls
79
- .voiceGetState()
80
- .then(({ state }) => {
81
- setState(state);
82
- })
83
- .catch(() => {
84
- // Voice service may not be initialized yet
85
- });
86
- return () => {
87
- for (const sub of subscriptions) {
88
- sub.unsubscribe();
89
- }
90
- };
91
- }, []);
92
- // Handle browser TTS events
93
- (0, react_1.useEffect)(() => {
94
- const speakHandler = (0, peers_sdk_1.subscribe)("voice:speakText", (event) => {
95
- const { text, voice, rate } = event.data;
96
- if ("speechSynthesis" in window) {
97
- const utterance = new SpeechSynthesisUtterance(text);
98
- if (voice) {
99
- const voices = speechSynthesis.getVoices();
100
- const selectedVoice = voices.find((v) => v.name === voice);
101
- if (selectedVoice)
102
- utterance.voice = selectedVoice;
103
- }
104
- if (rate)
105
- utterance.rate = rate;
106
- // Notify server when speech ends
107
- utterance.onend = () => {
108
- peers_sdk_1.rpcServerCalls.voiceNotifyPlaybackComplete?.().catch(() => { });
109
- };
110
- utterance.onerror = () => {
111
- // Still notify on error so we don't hang
112
- peers_sdk_1.rpcServerCalls.voiceNotifyPlaybackComplete?.().catch(() => { });
113
- };
114
- speechSynthesis.speak(utterance);
115
- }
116
- else {
117
- // No speech synthesis available, notify immediately
118
- peers_sdk_1.rpcServerCalls.voiceNotifyPlaybackComplete?.().catch(() => { });
119
- }
120
- });
121
- const stopHandler = (0, peers_sdk_1.subscribe)("voice:stopSpeaking", () => {
122
- // Stop browser TTS
123
- if ("speechSynthesis" in window) {
124
- speechSynthesis.cancel();
125
- }
126
- // Stop cloud TTS audio if playing
127
- if (currentAudioRef.current) {
128
- currentAudioRef.current.pause();
129
- currentAudioRef.current = null;
130
- }
131
- });
132
- const playHandler = (0, peers_sdk_1.subscribe)("voice:playAudio", async (event) => {
133
- const { audioBase64, mimeType } = event.data;
134
- try {
135
- const audioData = Uint8Array.from(atob(audioBase64), (c) => c.charCodeAt(0));
136
- const blob = new Blob([audioData], { type: mimeType });
137
- const url = URL.createObjectURL(blob);
138
- const audio = new Audio(url);
139
- // Track audio element so we can stop it
140
- currentAudioRef.current = audio;
141
- audio.onended = () => {
142
- URL.revokeObjectURL(url);
143
- currentAudioRef.current = null;
144
- // Notify server that audio playback has completed
145
- peers_sdk_1.rpcServerCalls.voiceNotifyPlaybackComplete?.().catch(() => { });
146
- };
147
- await audio.play();
148
- }
149
- catch (e) {
150
- console.error("Failed to play audio:", e);
151
- currentAudioRef.current = null;
152
- // Still notify on error so we don't hang
153
- peers_sdk_1.rpcServerCalls.voiceNotifyPlaybackComplete?.().catch(() => { });
154
- }
155
- });
156
- return () => {
157
- speakHandler.unsubscribe();
158
- stopHandler.unsubscribe();
159
- playHandler.unsubscribe();
160
- };
161
- }, []);
162
- const handleClick = (0, react_1.useCallback)(async () => {
163
- try {
164
- if (state === "disabled") {
165
- // Open settings
166
- window.location.hash = "#/settings/voice";
167
- return;
168
- }
169
- if (state === "recording") {
170
- await peers_sdk_1.rpcServerCalls.voiceStopRecording();
171
- }
172
- else if (state === "speaking") {
173
- await peers_sdk_1.rpcServerCalls.voiceStopPlayback();
174
- }
175
- else if (state === "idle" || state === "listening") {
176
- await peers_sdk_1.rpcServerCalls.voiceStartRecording();
177
- }
178
- }
179
- catch (e) {
180
- console.error("Voice action failed:", e);
181
- }
182
- }, [state]);
183
- const getIcon = () => {
184
- switch (state) {
185
- case "disabled":
186
- return "bi-mic-mute";
187
- case "idle":
188
- return "bi-mic";
189
- case "listening":
190
- return "bi-ear";
191
- case "recording":
192
- return "bi-mic-fill";
193
- case "processing":
194
- return "bi-hourglass-split";
195
- case "speaking":
196
- return "bi-volume-up-fill";
197
- default:
198
- return "bi-mic";
199
- }
200
- };
201
- const getStateText = () => {
202
- switch (state) {
203
- case "disabled":
204
- return "Voice disabled";
205
- case "idle":
206
- return 'Say "Porcupine" or click';
207
- case "listening":
208
- return "Listening...";
209
- case "recording":
210
- return "Recording...";
211
- case "processing":
212
- return "Processing...";
213
- case "speaking":
214
- return "Speaking... (click to stop)";
215
- default:
216
- return "";
217
- }
218
- };
219
- const getButtonClass = () => {
220
- const base = "btn rounded-circle shadow";
221
- switch (state) {
222
- case "disabled":
223
- return `${base} btn-secondary`;
224
- case "idle":
225
- return `${base} btn-outline-primary`;
226
- case "listening":
227
- return `${base} btn-info`;
228
- case "recording":
229
- return `${base} btn-danger`;
230
- case "processing":
231
- return `${base} btn-warning`;
232
- case "speaking":
233
- return `${base} btn-success`;
234
- default:
235
- return `${base} btn-secondary`;
236
- }
237
- };
238
- const positionStyles = {
239
- position: "fixed",
240
- zIndex: 1050,
241
- ...(position.includes("bottom") ? { bottom: "20px" } : { top: "70px" }),
242
- ...(position.includes("right") ? { right: "20px" } : { left: "20px" }),
243
- };
244
- // Don't render if disabled and not showing
245
- // Actually, let's always show so users can click to enable
246
- return ((0, jsx_runtime_1.jsxs)("div", { style: positionStyles, children: [isExpanded && state !== "disabled" && ((0, jsx_runtime_1.jsx)("div", { className: "card shadow mb-2", style: {
247
- width: "280px",
248
- backgroundColor: "var(--bs-body-bg)",
249
- }, children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body py-2 px-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center mb-2", children: [(0, jsx_runtime_1.jsx)("small", { className: "text-muted", children: getStateText() }), (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-x" }) })] }), state === "recording" && ((0, jsx_runtime_1.jsx)("div", { className: "progress mb-2", style: { height: "4px" }, children: (0, jsx_runtime_1.jsx)("div", { className: "progress-bar bg-danger", style: { width: `${Math.min(100, volumeLevel * 500)}%` } }) })), showTranscription && transcription && ((0, jsx_runtime_1.jsxs)("div", { className: "small", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-quote me-1" }), transcription] })), error && ((0, jsx_runtime_1.jsxs)("div", { className: "small text-danger", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-exclamation-triangle me-1" }), error] }))] }) })), (0, jsx_runtime_1.jsxs)("button", { className: getButtonClass(), style: {
250
- width: "56px",
251
- height: "56px",
252
- fontSize: "24px",
253
- transition: "all 0.2s ease",
254
- }, onClick: handleClick, title: getStateText(), children: [(0, jsx_runtime_1.jsx)("i", { className: `bi ${getIcon()}` }), (state === "listening" || state === "recording") && ((0, jsx_runtime_1.jsx)("span", { className: "position-absolute", style: {
255
- top: 0,
256
- left: 0,
257
- right: 0,
258
- bottom: 0,
259
- borderRadius: "50%",
260
- border: "2px solid currentColor",
261
- animation: "pulse 1.5s ease-out infinite",
262
- } })), state === "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)("style", { children: `
263
- @keyframes pulse {
264
- 0% {
265
- transform: scale(1);
266
- opacity: 1;
267
- }
268
- 100% {
269
- transform: scale(1.5);
270
- opacity: 0;
271
- }
272
- }
273
- ` })] }));
274
- };
275
- exports.VoiceIndicator = VoiceIndicator;
276
- exports.default = exports.VoiceIndicator;
@@ -1,15 +0,0 @@
1
- /**
2
- * Voice Playback Component
3
- *
4
- * Headless (renders nothing) component that hosts the browser-side voice TTS
5
- * playback handlers. The voice server (peers-electron `tts-service`) emits
6
- * `voice:speakText` / `voice:playAudio` / `voice:stopSpeaking` events and relies
7
- * on a mounted renderer to actually produce sound and report completion via
8
- * `voiceNotifyPlaybackComplete`. Mount this once near the app root so voice
9
- * output works regardless of which (if any) voice UI is visible.
10
- */
11
- /**
12
- * Subscribes to voice TTS events and plays them in the browser. Renders nothing.
13
- */
14
- export declare const VoicePlayback: React.FC;
15
- export default VoicePlayback;
@@ -1,86 +0,0 @@
1
- "use strict";
2
- /**
3
- * Voice Playback Component
4
- *
5
- * Headless (renders nothing) component that hosts the browser-side voice TTS
6
- * playback handlers. The voice server (peers-electron `tts-service`) emits
7
- * `voice:speakText` / `voice:playAudio` / `voice:stopSpeaking` events and relies
8
- * on a mounted renderer to actually produce sound and report completion via
9
- * `voiceNotifyPlaybackComplete`. Mount this once near the app root so voice
10
- * output works regardless of which (if any) voice UI is visible.
11
- */
12
- Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.VoicePlayback = void 0;
14
- const peers_sdk_1 = require("@peers-app/peers-sdk");
15
- const react_1 = require("react");
16
- /**
17
- * Subscribes to voice TTS events and plays them in the browser. Renders nothing.
18
- */
19
- const VoicePlayback = () => {
20
- // Track the current cloud-TTS audio element so it can be stopped on demand.
21
- const currentAudioRef = (0, react_1.useRef)(null);
22
- (0, react_1.useEffect)(() => {
23
- const speakHandler = (0, peers_sdk_1.subscribe)("voice:speakText", (event) => {
24
- const { text, voice, rate } = event.data;
25
- if ("speechSynthesis" in window) {
26
- const utterance = new SpeechSynthesisUtterance(text);
27
- if (voice) {
28
- const voices = speechSynthesis.getVoices();
29
- const selectedVoice = voices.find((v) => v.name === voice);
30
- if (selectedVoice)
31
- utterance.voice = selectedVoice;
32
- }
33
- if (rate)
34
- utterance.rate = rate;
35
- utterance.onend = () => {
36
- peers_sdk_1.rpcServerCalls.voiceNotifyPlaybackComplete?.().catch(() => { });
37
- };
38
- utterance.onerror = () => {
39
- peers_sdk_1.rpcServerCalls.voiceNotifyPlaybackComplete?.().catch(() => { });
40
- };
41
- speechSynthesis.speak(utterance);
42
- }
43
- else {
44
- peers_sdk_1.rpcServerCalls.voiceNotifyPlaybackComplete?.().catch(() => { });
45
- }
46
- });
47
- const stopHandler = (0, peers_sdk_1.subscribe)("voice:stopSpeaking", () => {
48
- if ("speechSynthesis" in window) {
49
- speechSynthesis.cancel();
50
- }
51
- if (currentAudioRef.current) {
52
- currentAudioRef.current.pause();
53
- currentAudioRef.current = null;
54
- }
55
- });
56
- const playHandler = (0, peers_sdk_1.subscribe)("voice:playAudio", async (event) => {
57
- const { audioBase64, mimeType } = event.data;
58
- try {
59
- const audioData = Uint8Array.from(atob(audioBase64), (c) => c.charCodeAt(0));
60
- const blob = new Blob([audioData], { type: mimeType });
61
- const url = URL.createObjectURL(blob);
62
- const audio = new Audio(url);
63
- currentAudioRef.current = audio;
64
- audio.onended = () => {
65
- URL.revokeObjectURL(url);
66
- currentAudioRef.current = null;
67
- peers_sdk_1.rpcServerCalls.voiceNotifyPlaybackComplete?.().catch(() => { });
68
- };
69
- await audio.play();
70
- }
71
- catch (e) {
72
- console.error("Failed to play audio:", e);
73
- currentAudioRef.current = null;
74
- peers_sdk_1.rpcServerCalls.voiceNotifyPlaybackComplete?.().catch(() => { });
75
- }
76
- });
77
- return () => {
78
- speakHandler.unsubscribe();
79
- stopHandler.unsubscribe();
80
- playHandler.unsubscribe();
81
- };
82
- }, []);
83
- return null;
84
- };
85
- exports.VoicePlayback = VoicePlayback;
86
- exports.default = exports.VoicePlayback;
@@ -1,28 +0,0 @@
1
- /** Shape passed to `subscribe` handlers for voice / chat IPC events */
2
- export type VoiceSubscribeEvent<T> = {
3
- data: T;
4
- };
5
- export type VoiceStatePayload = {
6
- state: string;
7
- };
8
- export type VoiceTranscriptionPayload = {
9
- text: string;
10
- };
11
- export type VoiceVolumePayload = {
12
- level: number;
13
- };
14
- export type VoiceErrorPayload = {
15
- message: string;
16
- };
17
- export type VoiceThreadPayload = {
18
- threadId: string;
19
- };
20
- export type VoiceSpeakPayload = {
21
- text: string;
22
- voice?: string;
23
- rate?: number;
24
- };
25
- export type VoicePlayAudioPayload = {
26
- audioBase64: string;
27
- mimeType: string;
28
- };
@@ -1,2 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,2 +0,0 @@
1
- import type React from "react";
2
- export declare const VoiceSettingsAgent: React.FC;
@@ -1,39 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VoiceSettingsAgent = void 0;
4
- const jsx_runtime_1 = require("react/jsx-runtime");
5
- const peers_sdk_1 = require("@peers-app/peers-sdk");
6
- const react_1 = require("react");
7
- const VOICE_HUB_AGENT_ASSISTANT_ID = "00mh0wlipkdbeaw8vhagent01";
8
- // Must match the pvar names in peers-core/src/data/voice-agent-config.ts and
9
- // peers-electron/src/server/voice/voice-agent-config.ts — pvar instances are
10
- // singletons keyed by name+scope so all three share the same underlying value.
11
- const voiceAgentAssistantIdPvar = (0, peers_sdk_1.userVar)("voice:agentAssistantId", {
12
- defaultValue: VOICE_HUB_AGENT_ASSISTANT_ID,
13
- });
14
- const VoiceSettingsAgent = () => {
15
- const [assistants, setAssistants] = (0, react_1.useState)([]);
16
- const [selectedId, setSelectedId] = (0, react_1.useState)(VOICE_HUB_AGENT_ASSISTANT_ID);
17
- const [saving, setSaving] = (0, react_1.useState)(false);
18
- const [saved, setSaved] = (0, react_1.useState)(false);
19
- (0, react_1.useEffect)(() => {
20
- const load = async () => {
21
- await voiceAgentAssistantIdPvar.loadingPromise;
22
- setSelectedId(voiceAgentAssistantIdPvar() ?? VOICE_HUB_AGENT_ASSISTANT_ID);
23
- const all = await (0, peers_sdk_1.Assistants)().list({});
24
- setAssistants(all);
25
- };
26
- load();
27
- }, []);
28
- const handleSave = (0, react_1.useCallback)(async () => {
29
- setSaving(true);
30
- setSaved(false);
31
- await voiceAgentAssistantIdPvar.loadingPromise;
32
- voiceAgentAssistantIdPvar(selectedId || undefined);
33
- setSaving(false);
34
- setSaved(true);
35
- setTimeout(() => setSaved(false), 2000);
36
- }, [selectedId]);
37
- return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h6", { children: "Voice Agent" }), (0, jsx_runtime_1.jsx)("p", { className: "text-muted small mb-2", children: "When you give a voice command that requires action (e.g. \"add milk to the shopping list\"), it is dispatched to this assistant as a background task." }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-2", children: [(0, jsx_runtime_1.jsx)("select", { className: "form-select form-select-sm", value: selectedId, onChange: (e) => setSelectedId(e.target.value), disabled: saving, children: assistants.map((a) => ((0, jsx_runtime_1.jsxs)("option", { value: a.assistantId, children: [a.name, a.assistantId === VOICE_HUB_AGENT_ASSISTANT_ID ? " (default)" : ""] }, a.assistantId))) }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-primary", onClick: handleSave, disabled: saving, style: { whiteSpace: "nowrap" }, children: saving ? "Saving…" : saved ? "Saved!" : "Save" })] })] }));
38
- };
39
- exports.VoiceSettingsAgent = VoiceSettingsAgent;
@@ -1,13 +0,0 @@
1
- import type React from "react";
2
- import { type PicovoiceKeyError, type VoiceSettings } from "./voice-settings-types";
3
- interface ApiKeysProps {
4
- settings: VoiceSettings;
5
- picovoiceKey: string;
6
- openaiKey: string;
7
- keyError: PicovoiceKeyError | null;
8
- onPicovoiceKeyChange: (value: string) => void;
9
- onOpenaiKeyChange: (value: string) => void;
10
- onDismissKeyError: () => void;
11
- }
12
- export declare const VoiceSettingsApiKeys: React.FC<ApiKeysProps>;
13
- export {};
@@ -1,8 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VoiceSettingsApiKeys = void 0;
4
- const jsx_runtime_1 = require("react/jsx-runtime");
5
- const tooltip_1 = require("../../components/tooltip");
6
- const voice_settings_types_1 = require("./voice-settings-types");
7
- const VoiceSettingsApiKeys = ({ settings, picovoiceKey, openaiKey, keyError, onPicovoiceKeyChange, onOpenaiKeyChange, onDismissKeyError, }) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "mb-4", children: [(0, jsx_runtime_1.jsxs)("label", { className: "form-label", children: ["Picovoice Access Key", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "Required for wake word detection. Get a free key at [console.picovoice.ai](https://console.picovoice.ai)" }), settings.picovoiceAccessKeySet && (0, jsx_runtime_1.jsx)("span", { className: "badge bg-success ms-2", children: "Set" })] }), (0, jsx_runtime_1.jsx)("input", { type: "password", className: "form-control", value: picovoiceKey, onChange: (e) => onPicovoiceKeyChange(e.target.value), placeholder: settings.picovoiceAccessKeySet ? "••••••••••••••••" : "Enter your Picovoice access key" }), (0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: [(0, jsx_runtime_1.jsx)("a", { href: "https://console.picovoice.ai", target: "_blank", rel: "noopener noreferrer", children: "Get a free access key" }), settings.picovoiceAccessKeySet && " (leave blank to keep existing key)"] })] }), keyError && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-warning alert-dismissible mb-4", role: "alert", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-start gap-2", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-exclamation-triangle-fill flex-shrink-0 mt-1" }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: voice_settings_types_1.PICOVOICE_KEY_ERROR_MESSAGES[keyError].heading }), (0, jsx_runtime_1.jsx)("div", { className: "mt-1", children: voice_settings_types_1.PICOVOICE_KEY_ERROR_MESSAGES[keyError].body }), (0, jsx_runtime_1.jsx)("a", { href: "https://console.picovoice.ai", target: "_blank", rel: "noopener noreferrer", className: "alert-link mt-1 d-inline-block", children: "Open console.picovoice.ai" })] })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn-close", onClick: onDismissKeyError })] })), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("label", { className: "form-label", children: ["OpenAI API Key", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "Required for cloud STT (Whisper) and cloud TTS. Leave empty to use local/browser alternatives." }), settings.openaiApiKeySet && (0, jsx_runtime_1.jsx)("span", { className: "badge bg-success ms-2", children: "Set" })] }), (0, jsx_runtime_1.jsx)("input", { type: "password", className: "form-control", value: openaiKey, onChange: (e) => onOpenaiKeyChange(e.target.value), placeholder: settings.openaiApiKeySet ? "••••••••••••••••" : "sk-..." }), settings.openaiApiKeySet && ((0, jsx_runtime_1.jsx)("small", { className: "text-muted", children: "Leave blank to keep existing key" }))] })] }));
8
- exports.VoiceSettingsApiKeys = VoiceSettingsApiKeys;