@peers-app/peers-ui 0.21.7 → 0.23.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 (69) hide show
  1. package/dist/components/access-level-field.d.ts +10 -0
  2. package/dist/components/access-level-field.js +28 -0
  3. package/dist/components/access-level-field.test.d.ts +1 -0
  4. package/dist/components/access-level-field.test.js +28 -0
  5. package/dist/components/message-logs/message-logs.d.ts +2 -1
  6. package/dist/components/message-logs/message-logs.js +44 -2
  7. package/dist/components/message-logs/tool-access-approval-card.d.ts +13 -0
  8. package/dist/components/message-logs/tool-access-approval-card.js +81 -0
  9. package/dist/components/message-logs/tool-access-approval-card.test.d.ts +1 -0
  10. package/dist/components/message-logs/tool-access-approval-card.test.js +102 -0
  11. package/dist/index.d.ts +0 -2
  12. package/dist/index.js +0 -2
  13. package/dist/screens/assistants/assistant-info.d.ts +1 -0
  14. package/dist/screens/assistants/assistant-info.js +3 -1
  15. package/dist/screens/assistants/assistant-list.d.ts +1 -0
  16. package/dist/screens/assistants/assistant-list.js +8 -1
  17. package/dist/screens/packages/package-versions.js +1 -1
  18. package/dist/screens/settings/settings-page.js +0 -5
  19. package/dist/screens/tools/tool-access-ipc.d.ts +5 -0
  20. package/dist/screens/tools/tool-access-ipc.js +65 -0
  21. package/dist/screens/tools/tool-access-ipc.test.d.ts +1 -0
  22. package/dist/screens/tools/tool-access-ipc.test.js +40 -0
  23. package/dist/screens/tools/tool-info.d.ts +1 -0
  24. package/dist/screens/tools/tool-info.js +3 -1
  25. package/package.json +3 -3
  26. package/src/components/access-level-field.test.tsx +32 -0
  27. package/src/components/access-level-field.tsx +68 -0
  28. package/src/components/message-logs/message-logs.tsx +58 -1
  29. package/src/components/message-logs/tool-access-approval-card.test.tsx +127 -0
  30. package/src/components/message-logs/tool-access-approval-card.tsx +178 -0
  31. package/src/index.tsx +0 -2
  32. package/src/screens/assistants/assistant-info.tsx +4 -0
  33. package/src/screens/assistants/assistant-list.tsx +29 -2
  34. package/src/screens/packages/package-versions.tsx +1 -0
  35. package/src/screens/settings/settings-page.tsx +0 -5
  36. package/src/screens/tools/tool-access-ipc.test.ts +55 -0
  37. package/src/screens/tools/tool-access-ipc.ts +88 -0
  38. package/src/screens/tools/tool-info.tsx +4 -0
  39. package/tsconfig.json +1 -0
  40. package/dist/components/voice-indicator.d.ts +0 -15
  41. package/dist/components/voice-indicator.js +0 -276
  42. package/dist/components/voice-playback.d.ts +0 -15
  43. package/dist/components/voice-playback.js +0 -86
  44. package/dist/components/voice-subscribe-events.d.ts +0 -28
  45. package/dist/components/voice-subscribe-events.js +0 -2
  46. package/dist/screens/settings/voice-settings-agent.d.ts +0 -2
  47. package/dist/screens/settings/voice-settings-agent.js +0 -39
  48. package/dist/screens/settings/voice-settings-api-keys.d.ts +0 -13
  49. package/dist/screens/settings/voice-settings-api-keys.js +0 -8
  50. package/dist/screens/settings/voice-settings-output.d.ts +0 -16
  51. package/dist/screens/settings/voice-settings-output.js +0 -7
  52. package/dist/screens/settings/voice-settings-providers.d.ts +0 -9
  53. package/dist/screens/settings/voice-settings-providers.js +0 -6
  54. package/dist/screens/settings/voice-settings-types.d.ts +0 -33
  55. package/dist/screens/settings/voice-settings-types.js +0 -55
  56. package/dist/screens/settings/voice-settings-wake-word.d.ts +0 -9
  57. package/dist/screens/settings/voice-settings-wake-word.js +0 -8
  58. package/dist/screens/settings/voice-settings.d.ts +0 -10
  59. package/dist/screens/settings/voice-settings.js +0 -173
  60. package/src/components/voice-indicator.tsx +0 -353
  61. package/src/components/voice-playback.tsx +0 -97
  62. package/src/components/voice-subscribe-events.ts +0 -16
  63. package/src/screens/settings/voice-settings-agent.tsx +0 -73
  64. package/src/screens/settings/voice-settings-api-keys.tsx +0 -91
  65. package/src/screens/settings/voice-settings-output.tsx +0 -126
  66. package/src/screens/settings/voice-settings-providers.tsx +0 -49
  67. package/src/screens/settings/voice-settings-types.ts +0 -94
  68. package/src/screens/settings/voice-settings-wake-word.tsx +0 -94
  69. package/src/screens/settings/voice-settings.tsx +0 -326
@@ -1,126 +0,0 @@
1
- import type React from "react";
2
- import { OPENAI_TTS_VOICES, type VoiceSettings } from "./voice-settings-types";
3
-
4
- interface OutputProps {
5
- settings: VoiceSettings;
6
- localSettings: VoiceSettings;
7
- saving: boolean;
8
- testText: string;
9
- hasUnsavedChanges: boolean;
10
- audioDevices: string[];
11
- onChange: (patch: Partial<VoiceSettings>) => void;
12
- onTestTextChange: (text: string) => void;
13
- onTestTTS: () => void;
14
- onTestRecording: () => void;
15
- }
16
-
17
- export const VoiceSettingsOutput: React.FC<OutputProps> = ({
18
- settings,
19
- localSettings,
20
- saving,
21
- testText,
22
- hasUnsavedChanges,
23
- audioDevices,
24
- onChange,
25
- onTestTextChange,
26
- onTestTTS,
27
- onTestRecording,
28
- }) => (
29
- <>
30
- {localSettings.ttsProvider === "cloud" && (
31
- <div className="mb-3">
32
- <label className="form-label">Voice</label>
33
- <select
34
- className="form-select"
35
- value={localSettings.ttsVoice || "nova"}
36
- onChange={(e) => onChange({ ttsVoice: e.target.value })}
37
- disabled={saving}
38
- >
39
- {OPENAI_TTS_VOICES.map((voice) => (
40
- <option key={voice.id} value={voice.id}>
41
- {voice.name} - {voice.description}
42
- </option>
43
- ))}
44
- </select>
45
- </div>
46
- )}
47
-
48
- <div className="mb-4">
49
- <label className="form-label">Speaking Speed: {localSettings.ttsSpeed.toFixed(1)}x</label>
50
- <input
51
- type="range"
52
- className="form-range"
53
- min="0.5"
54
- max="2"
55
- step="0.1"
56
- value={localSettings.ttsSpeed}
57
- onChange={(e) => onChange({ ttsSpeed: parseFloat(e.target.value) })}
58
- />
59
- <div className="d-flex justify-content-between">
60
- <small className="text-muted">Slower</small>
61
- <small className="text-muted">Faster</small>
62
- </div>
63
- </div>
64
-
65
- <div className="mb-4">
66
- <label className="form-label">Test TTS</label>
67
- <div className="input-group">
68
- <input
69
- type="text"
70
- className="form-control"
71
- value={testText}
72
- onChange={(e) => onTestTextChange(e.target.value)}
73
- placeholder="Enter text to speak"
74
- />
75
- <button
76
- className="btn btn-outline-primary"
77
- type="button"
78
- onClick={onTestTTS}
79
- disabled={
80
- !settings.enabled && localSettings.ttsProvider === "cloud" && !settings.openaiApiKeySet
81
- }
82
- >
83
- <i className="bi bi-play-fill me-1"></i>
84
- Speak
85
- </button>
86
- </div>
87
- <small className="text-muted">
88
- {hasUnsavedChanges && "Save changes first to test with new settings"}
89
- </small>
90
- </div>
91
-
92
- <hr className="my-4" />
93
-
94
- <div className="mb-4">
95
- <h6 className="mb-3">Test Voice Input</h6>
96
- <p className="text-muted small">
97
- Click the button below to test voice recording. Speak and it will stop when silence is
98
- detected.
99
- </p>
100
- <button
101
- className="btn btn-primary"
102
- onClick={onTestRecording}
103
- disabled={!settings.enabled || saving}
104
- >
105
- <i className="bi bi-mic me-2"></i>
106
- Test Recording
107
- </button>
108
- {!settings.enabled && (
109
- <small className="text-muted ms-2">Enable and save voice settings first</small>
110
- )}
111
- </div>
112
-
113
- {audioDevices.length > 0 && (
114
- <div className="mb-4">
115
- <h6 className="mb-3">Available Audio Devices</h6>
116
- <ul className="list-group list-group-flush">
117
- {audioDevices.map((device, index) => (
118
- <li key={index} className="list-group-item bg-transparent">
119
- <small>{device}</small>
120
- </li>
121
- ))}
122
- </ul>
123
- </div>
124
- )}
125
- </>
126
- );
@@ -1,49 +0,0 @@
1
- import type React from "react";
2
- import type { STTProvider, TTSProvider, VoiceSettings } from "./voice-settings-types";
3
-
4
- interface ProvidersProps {
5
- localSettings: VoiceSettings;
6
- saving: boolean;
7
- onChange: (patch: Partial<VoiceSettings>) => void;
8
- }
9
-
10
- export const VoiceSettingsProviders: React.FC<ProvidersProps> = ({
11
- localSettings,
12
- saving,
13
- onChange,
14
- }) => (
15
- <>
16
- <h6 className="mb-3">Speech to Text (STT)</h6>
17
-
18
- <div className="mb-4">
19
- <label className="form-label">STT Provider</label>
20
- <select
21
- className="form-select"
22
- value={localSettings.sttProvider}
23
- onChange={(e) => onChange({ sttProvider: e.target.value as STTProvider })}
24
- disabled={saving}
25
- >
26
- <option value="auto">Auto (Cloud with local fallback)</option>
27
- <option value="cloud">Cloud only (OpenAI Whisper)</option>
28
- <option value="local">Local only (requires setup)</option>
29
- </select>
30
- </div>
31
-
32
- <hr className="my-4" />
33
-
34
- <h6 className="mb-3">Text to Speech (TTS)</h6>
35
-
36
- <div className="mb-3">
37
- <label className="form-label">TTS Provider</label>
38
- <select
39
- className="form-select"
40
- value={localSettings.ttsProvider}
41
- onChange={(e) => onChange({ ttsProvider: e.target.value as TTSProvider })}
42
- disabled={saving}
43
- >
44
- <option value="browser">Browser (free, works offline)</option>
45
- <option value="cloud">Cloud (OpenAI TTS, natural voice)</option>
46
- </select>
47
- </div>
48
- </>
49
- );
@@ -1,94 +0,0 @@
1
- export type STTProvider = "auto" | "cloud" | "local";
2
- export type TTSProvider = "browser" | "cloud";
3
- export type PicovoiceKeyError = "refused" | "invalid" | "limit" | "throttled";
4
- export type BuiltinWakeWord =
5
- | "ALEXA"
6
- | "AMERICANO"
7
- | "BLUEBERRY"
8
- | "BUMBLEBEE"
9
- | "COMPUTER"
10
- | "GRAPEFRUIT"
11
- | "GRASSHOPPER"
12
- | "HEY_GOOGLE"
13
- | "HEY_SIRI"
14
- | "JARVIS"
15
- | "OK_GOOGLE"
16
- | "PICOVOICE"
17
- | "PORCUPINE"
18
- | "TERMINATOR";
19
-
20
- export interface VoiceSettingsData {
21
- enabled: boolean;
22
- voiceOutputEnabled?: boolean;
23
- wakeWord: BuiltinWakeWord;
24
- wakeWordSensitivity: number;
25
- speechSensitivity: number;
26
- sttProvider: STTProvider;
27
- ttsProvider: TTSProvider;
28
- ttsVoice?: string;
29
- ttsSpeed: number;
30
- }
31
-
32
- export interface VoiceSettings extends VoiceSettingsData {
33
- picovoiceAccessKeySet?: boolean;
34
- openaiApiKeySet?: boolean;
35
- }
36
-
37
- export const DEFAULT_SETTINGS: VoiceSettingsData = {
38
- enabled: false,
39
- voiceOutputEnabled: true,
40
- wakeWord: "COMPUTER",
41
- wakeWordSensitivity: 0.5,
42
- speechSensitivity: 0.7,
43
- sttProvider: "auto",
44
- ttsProvider: "browser",
45
- ttsSpeed: 1.0,
46
- };
47
-
48
- export const BUILTIN_WAKE_WORDS: { id: BuiltinWakeWord; name: string }[] = [
49
- { id: "ALEXA", name: "Alexa" },
50
- { id: "AMERICANO", name: "Americano" },
51
- { id: "BLUEBERRY", name: "Blueberry" },
52
- { id: "BUMBLEBEE", name: "Bumblebee" },
53
- { id: "COMPUTER", name: "Computer" },
54
- { id: "GRAPEFRUIT", name: "Grapefruit" },
55
- { id: "GRASSHOPPER", name: "Grasshopper" },
56
- { id: "HEY_GOOGLE", name: "Hey Google" },
57
- { id: "HEY_SIRI", name: "Hey Siri" },
58
- { id: "JARVIS", name: "Jarvis" },
59
- { id: "OK_GOOGLE", name: "Ok Google" },
60
- { id: "PICOVOICE", name: "Picovoice" },
61
- { id: "PORCUPINE", name: "Porcupine" },
62
- { id: "TERMINATOR", name: "Terminator" },
63
- ];
64
-
65
- export const PICOVOICE_KEY_ERROR_MESSAGES: Record<
66
- PicovoiceKeyError,
67
- { heading: string; body: string }
68
- > = {
69
- refused: {
70
- heading: "Picovoice access key refused",
71
- body: "Your key was rejected — it may have been revoked or banned. Generate a new key at console.picovoice.ai and paste it below.",
72
- },
73
- invalid: {
74
- heading: "Picovoice access key invalid",
75
- body: "Your key is malformed or expired. Generate a new key at console.picovoice.ai and paste it below.",
76
- },
77
- limit: {
78
- heading: "Picovoice device limit reached",
79
- body: "Your key has been activated on too many devices. Go to console.picovoice.ai to deactivate old devices, generate a new key, or upgrade your plan.",
80
- },
81
- throttled: {
82
- heading: "Picovoice activation throttled",
83
- body: "Too many activation attempts. Please wait a moment, then save your settings again to retry.",
84
- },
85
- };
86
-
87
- export const OPENAI_TTS_VOICES = [
88
- { id: "alloy", name: "Alloy", description: "Neutral and balanced" },
89
- { id: "echo", name: "Echo", description: "Warm and clear" },
90
- { id: "fable", name: "Fable", description: "Expressive and dynamic" },
91
- { id: "onyx", name: "Onyx", description: "Deep and authoritative" },
92
- { id: "nova", name: "Nova", description: "Friendly and upbeat" },
93
- { id: "shimmer", name: "Shimmer", description: "Soft and gentle" },
94
- ];
@@ -1,94 +0,0 @@
1
- import type React from "react";
2
- import { Tooltip } from "../../components/tooltip";
3
- import {
4
- BUILTIN_WAKE_WORDS,
5
- type BuiltinWakeWord,
6
- type VoiceSettings,
7
- } from "./voice-settings-types";
8
-
9
- interface WakeWordProps {
10
- localSettings: VoiceSettings;
11
- saving: boolean;
12
- onChange: (patch: Partial<VoiceSettings>) => void;
13
- }
14
-
15
- export const VoiceSettingsWakeWord: React.FC<WakeWordProps> = ({
16
- localSettings,
17
- saving,
18
- onChange,
19
- }) => (
20
- <>
21
- <div className="mb-4">
22
- <label className="form-label">
23
- Wake Word
24
- <Tooltip markdownContent="The phrase you say to activate voice recording. Choose a word that's easy for you to say and unlikely to occur in normal conversation." />
25
- </label>
26
- <select
27
- className="form-select"
28
- value={localSettings.wakeWord}
29
- onChange={(e) => onChange({ wakeWord: e.target.value as BuiltinWakeWord })}
30
- disabled={saving}
31
- >
32
- {BUILTIN_WAKE_WORDS.map((word) => (
33
- <option key={word.id} value={word.id}>
34
- {word.name}
35
- </option>
36
- ))}
37
- </select>
38
- </div>
39
-
40
- <div className="mb-4">
41
- <label className="form-label">
42
- Wake Word Sensitivity: {localSettings.wakeWordSensitivity.toFixed(2)}
43
- <Tooltip markdownContent="Higher values make the wake word easier to trigger but may cause false positives." />
44
- </label>
45
- <input
46
- type="range"
47
- className="form-range"
48
- min="0"
49
- max="1"
50
- step="0.05"
51
- value={localSettings.wakeWordSensitivity}
52
- onChange={(e) => onChange({ wakeWordSensitivity: parseFloat(e.target.value) })}
53
- />
54
- <div className="d-flex justify-content-between">
55
- <small className="text-muted">Less sensitive</small>
56
- <small className="text-muted">More sensitive</small>
57
- </div>
58
- </div>
59
-
60
- <div className="mb-4">
61
- <label className="form-label">
62
- Speech Detection: {(localSettings.speechSensitivity ?? 0.7).toFixed(2)}
63
- <Tooltip markdownContent="Controls how sensitive the microphone is when detecting speech. Higher values pick up quieter speech (better for quiet rooms or a close mic). Lower values require louder speech, which helps filter out background noise." />
64
- </label>
65
- <input
66
- type="range"
67
- className="form-range"
68
- min="0"
69
- max="1"
70
- step="0.05"
71
- value={localSettings.speechSensitivity ?? 0.7}
72
- onChange={(e) => onChange({ speechSensitivity: parseFloat(e.target.value) })}
73
- />
74
- <div className="d-flex justify-content-between mb-1">
75
- <small className="text-muted">Loud / Noisy environment</small>
76
- <small className="text-muted">Quiet / Close mic</small>
77
- </div>
78
- <div className="d-flex justify-content-between px-1">
79
- <small className="text-muted" style={{ fontSize: "0.7rem" }}>
80
- 0.2 — Shout
81
- </small>
82
- <small className="text-muted" style={{ fontSize: "0.7rem" }}>
83
- 0.5 — Normal
84
- </small>
85
- <small className="text-muted" style={{ fontSize: "0.7rem" }}>
86
- 0.7 — Desk
87
- </small>
88
- <small className="text-muted" style={{ fontSize: "0.7rem" }}>
89
- 0.9 — Whisper
90
- </small>
91
- </div>
92
- </div>
93
- </>
94
- );
@@ -1,326 +0,0 @@
1
- /**
2
- * Voice Settings Component
3
- *
4
- * Settings panel for voice input configuration.
5
- * Settings are stored directly via pvars (auto-synced).
6
- * Keys are stored as secure pvars (write-only).
7
- */
8
-
9
- import { rpcServerCalls, userVar } from "@peers-app/peers-sdk";
10
- import type React from "react";
11
- import { useCallback, useEffect, useState } from "react";
12
- import { VoiceSettingsAgent } from "./voice-settings-agent";
13
- import { VoiceSettingsApiKeys } from "./voice-settings-api-keys";
14
- import { VoiceSettingsOutput } from "./voice-settings-output";
15
- import { VoiceSettingsProviders } from "./voice-settings-providers";
16
- import {
17
- BUILTIN_WAKE_WORDS,
18
- DEFAULT_SETTINGS,
19
- type PicovoiceKeyError,
20
- type VoiceSettingsData,
21
- type VoiceSettings as VoiceSettingsType,
22
- } from "./voice-settings-types";
23
- import { VoiceSettingsWakeWord } from "./voice-settings-wake-word";
24
-
25
- // Pvars - shared with voice service (writes here trigger service updates)
26
- const voiceSettingsPvar = userVar<VoiceSettingsData>("voiceSettings", {
27
- defaultValue: DEFAULT_SETTINGS,
28
- });
29
- const picovoiceKeyPvar = userVar<string>("PICOVOICE_ACCESS_KEY", {
30
- isSecret: true,
31
- defaultValue: "",
32
- });
33
- const openaiKeyPvar = userVar<string>("OPENAI_API_KEY", { isSecret: true, defaultValue: "" });
34
-
35
- export const VoiceSettings: React.FC = () => {
36
- const [settings, setSettings] = useState<VoiceSettingsType>({ ...DEFAULT_SETTINGS });
37
- const [loading, setLoading] = useState(true);
38
- const [saving, setSaving] = useState(false);
39
- const [error, setError] = useState<string | null>(null);
40
- const [success, setSuccess] = useState<string | null>(null);
41
- const [audioDevices, setAudioDevices] = useState<string[]>([]);
42
- const [keyError, setKeyError] = useState<PicovoiceKeyError | null>(null);
43
-
44
- // Local state for unsaved changes
45
- const [localSettings, setLocalSettings] = useState<VoiceSettingsType>(settings);
46
- const [picovoiceKey, setPicovoiceKey] = useState("");
47
- const [openaiKey, setOpenaiKey] = useState("");
48
- const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
49
-
50
- const [testText, setTestText] = useState("Hello, this is a test of the text to speech system.");
51
-
52
- // Load initial settings from pvars
53
- useEffect(() => {
54
- const loadSettings = async () => {
55
- try {
56
- await voiceSettingsPvar.loadingPromise;
57
- await picovoiceKeyPvar.loadingPromise;
58
- await openaiKeyPvar.loadingPromise;
59
-
60
- const savedSettings = voiceSettingsPvar() || DEFAULT_SETTINGS;
61
- const picovoiceKeyValue = picovoiceKeyPvar();
62
- const openaiKeyValue = openaiKeyPvar();
63
-
64
- const loadedSettings: VoiceSettingsType = {
65
- ...savedSettings,
66
- picovoiceAccessKeySet: !!picovoiceKeyValue && picovoiceKeyValue !== "",
67
- openaiApiKeySet: !!openaiKeyValue && openaiKeyValue !== "",
68
- };
69
-
70
- setSettings(loadedSettings);
71
- setLocalSettings(loadedSettings);
72
-
73
- try {
74
- const devices = await rpcServerCalls.voiceGetAudioDevices();
75
- setAudioDevices(devices || []);
76
- } catch {
77
- // voiceGetAudioDevices not available (e.g. React Native) - leave empty
78
- }
79
-
80
- try {
81
- const voiceState = await rpcServerCalls.voiceGetState();
82
- setKeyError(voiceState.keyError ?? null);
83
- } catch {
84
- // voiceGetState not available - leave null
85
- }
86
- } catch (err) {
87
- console.error("Failed to load voice settings:", err);
88
- setError("Failed to load voice settings.");
89
- } finally {
90
- setLoading(false);
91
- }
92
- };
93
- loadSettings();
94
- }, []);
95
-
96
- // Track unsaved changes
97
- useEffect(() => {
98
- const settingsChanged = JSON.stringify(localSettings) !== JSON.stringify(settings);
99
- const keysEntered = picovoiceKey.length > 0 || openaiKey.length > 0;
100
- setHasUnsavedChanges(settingsChanged || keysEntered);
101
- }, [localSettings, settings, picovoiceKey, openaiKey]);
102
-
103
- const handleSaveAll = useCallback(async () => {
104
- setSaving(true);
105
- setError(null);
106
- setSuccess(null);
107
-
108
- try {
109
- if (picovoiceKey) {
110
- await picovoiceKeyPvar.loadingPromise;
111
- picovoiceKeyPvar(picovoiceKey);
112
- setPicovoiceKey("");
113
- setKeyError(null);
114
- }
115
- if (openaiKey) {
116
- await openaiKeyPvar.loadingPromise;
117
- openaiKeyPvar(openaiKey);
118
- setOpenaiKey("");
119
- }
120
-
121
- await voiceSettingsPvar.loadingPromise;
122
- voiceSettingsPvar({
123
- enabled: localSettings.enabled,
124
- voiceOutputEnabled: localSettings.voiceOutputEnabled ?? true,
125
- wakeWord: localSettings.wakeWord,
126
- wakeWordSensitivity: localSettings.wakeWordSensitivity,
127
- speechSensitivity: localSettings.speechSensitivity ?? 0.3,
128
- sttProvider: localSettings.sttProvider,
129
- ttsProvider: localSettings.ttsProvider,
130
- ttsVoice: localSettings.ttsVoice,
131
- ttsSpeed: localSettings.ttsSpeed,
132
- });
133
-
134
- const updatedSettings: VoiceSettingsType = {
135
- ...localSettings,
136
- picovoiceAccessKeySet: !!picovoiceKeyPvar() || !!picovoiceKey,
137
- openaiApiKeySet: !!openaiKeyPvar() || !!openaiKey,
138
- };
139
- setSettings(updatedSettings);
140
- setLocalSettings(updatedSettings);
141
-
142
- setSuccess("Settings saved successfully!");
143
- setTimeout(() => setSuccess(null), 3000);
144
- } catch (err) {
145
- console.error("Failed to save voice settings:", err);
146
- setError((err as Error).message);
147
- } finally {
148
- setSaving(false);
149
- }
150
- }, [localSettings, picovoiceKey, openaiKey]);
151
-
152
- const handleToggleEnabled = useCallback(async () => {
153
- if (!localSettings.picovoiceAccessKeySet && !picovoiceKey && !localSettings.enabled) {
154
- setError("Please enter and save a Picovoice Access Key first");
155
- return;
156
- }
157
- setLocalSettings((prev) => ({ ...prev, enabled: !prev.enabled }));
158
- }, [localSettings.enabled, localSettings.picovoiceAccessKeySet, picovoiceKey]);
159
-
160
- const handleTestTTS = useCallback(async () => {
161
- try {
162
- setError(null);
163
- await rpcServerCalls.voiceTestTTS(testText);
164
- } catch (err) {
165
- console.error("TTS test failed:", err);
166
- setError(`TTS test failed: ${(err as Error).message}`);
167
- }
168
- }, [testText]);
169
-
170
- const handleTestRecording = useCallback(async () => {
171
- try {
172
- setError(null);
173
- await rpcServerCalls.voiceStartRecording();
174
- } catch (err) {
175
- console.error("Recording test failed:", err);
176
- setError((err as Error).message);
177
- }
178
- }, []);
179
-
180
- const handleSettingsChange = useCallback((patch: Partial<VoiceSettingsType>) => {
181
- setLocalSettings((prev) => ({ ...prev, ...patch }));
182
- }, []);
183
-
184
- if (loading) {
185
- return (
186
- <div className="d-flex justify-content-center p-4">
187
- <div className="spinner-border text-primary" role="status">
188
- <span className="visually-hidden">Loading...</span>
189
- </div>
190
- </div>
191
- );
192
- }
193
-
194
- return (
195
- <div className="voice-settings">
196
- <div className="d-flex align-items-center mb-3">
197
- <h5 className="mb-0 me-3">Voice Input</h5>
198
- <div className="form-check form-switch me-auto">
199
- <input
200
- className="form-check-input"
201
- type="checkbox"
202
- id="voiceEnabled"
203
- checked={localSettings.enabled}
204
- onChange={handleToggleEnabled}
205
- disabled={saving}
206
- />
207
- <label className="form-check-label" htmlFor="voiceEnabled">
208
- {localSettings.enabled ? "Enabled" : "Disabled"}
209
- </label>
210
- </div>
211
- <button
212
- className="btn btn-primary btn-sm"
213
- onClick={handleSaveAll}
214
- disabled={saving || !hasUnsavedChanges}
215
- >
216
- {saving ? "Saving..." : "Save"}
217
- </button>
218
- </div>
219
-
220
- {localSettings.enabled && (
221
- <div className="d-flex align-items-center mb-3 ms-3">
222
- <h6 className="mb-0 me-3 text-muted">Voice Output</h6>
223
- <div className="form-check form-switch">
224
- <input
225
- className="form-check-input"
226
- type="checkbox"
227
- id="voiceOutputEnabled"
228
- checked={localSettings.voiceOutputEnabled !== false}
229
- onChange={(e) => handleSettingsChange({ voiceOutputEnabled: e.target.checked })}
230
- disabled={saving}
231
- />
232
- <label className="form-check-label" htmlFor="voiceOutputEnabled">
233
- {localSettings.voiceOutputEnabled !== false ? "On" : "Off"}
234
- </label>
235
- </div>
236
- <small className="text-muted ms-2">Speak assistant responses aloud</small>
237
- </div>
238
- )}
239
-
240
- {error && (
241
- <div className="alert alert-danger alert-dismissible fade show" role="alert">
242
- {error}
243
- <button type="button" className="btn-close" onClick={() => setError(null)}></button>
244
- </div>
245
- )}
246
-
247
- {success && (
248
- <div className="alert alert-success alert-dismissible fade show" role="alert">
249
- {success}
250
- <button type="button" className="btn-close" onClick={() => setSuccess(null)}></button>
251
- </div>
252
- )}
253
-
254
- <VoiceSettingsApiKeys
255
- settings={settings}
256
- picovoiceKey={picovoiceKey}
257
- openaiKey={openaiKey}
258
- keyError={keyError}
259
- onPicovoiceKeyChange={setPicovoiceKey}
260
- onOpenaiKeyChange={setOpenaiKey}
261
- onDismissKeyError={() => setKeyError(null)}
262
- />
263
-
264
- <hr className="my-4" />
265
-
266
- <VoiceSettingsWakeWord
267
- localSettings={localSettings}
268
- saving={saving}
269
- onChange={handleSettingsChange}
270
- />
271
-
272
- <hr className="my-4" />
273
-
274
- <VoiceSettingsProviders
275
- localSettings={localSettings}
276
- saving={saving}
277
- onChange={handleSettingsChange}
278
- />
279
-
280
- <VoiceSettingsOutput
281
- settings={settings}
282
- localSettings={localSettings}
283
- saving={saving}
284
- testText={testText}
285
- hasUnsavedChanges={hasUnsavedChanges}
286
- audioDevices={audioDevices}
287
- onChange={handleSettingsChange}
288
- onTestTextChange={setTestText}
289
- onTestTTS={handleTestTTS}
290
- onTestRecording={handleTestRecording}
291
- />
292
-
293
- <hr className="my-4" />
294
-
295
- <VoiceSettingsAgent />
296
-
297
- <div className="alert alert-info">
298
- <h6 className="alert-heading">
299
- <i className="bi bi-info-circle me-2"></i>
300
- How to use
301
- </h6>
302
- <ol className="mb-0 small">
303
- <li>Get a free Picovoice access key from the link above</li>
304
- <li>
305
- Enter the key and click <strong>Save</strong>
306
- </li>
307
- <li>
308
- Enable voice input and click <strong>Save</strong> again
309
- </li>
310
- <li>
311
- Say{" "}
312
- <strong>
313
- "{BUILTIN_WAKE_WORDS.find((w) => w.id === localSettings.wakeWord)?.name || "Computer"}
314
- "
315
- </strong>{" "}
316
- to activate voice recording
317
- </li>
318
- <li>Speak your message - recording stops automatically when you pause</li>
319
- <li>Your message will be sent to the assistant and the response will be spoken</li>
320
- </ol>
321
- </div>
322
- </div>
323
- );
324
- };
325
-
326
- export default VoiceSettings;