@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.
- package/dist/components/access-level-field.d.ts +10 -0
- package/dist/components/access-level-field.js +28 -0
- package/dist/components/access-level-field.test.d.ts +1 -0
- package/dist/components/access-level-field.test.js +28 -0
- package/dist/components/message-logs/message-logs.d.ts +2 -1
- package/dist/components/message-logs/message-logs.js +44 -2
- package/dist/components/message-logs/tool-access-approval-card.d.ts +13 -0
- package/dist/components/message-logs/tool-access-approval-card.js +81 -0
- package/dist/components/message-logs/tool-access-approval-card.test.d.ts +1 -0
- package/dist/components/message-logs/tool-access-approval-card.test.js +102 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -2
- package/dist/screens/assistants/assistant-info.d.ts +1 -0
- package/dist/screens/assistants/assistant-info.js +3 -1
- package/dist/screens/assistants/assistant-list.d.ts +1 -0
- package/dist/screens/assistants/assistant-list.js +8 -1
- package/dist/screens/packages/package-versions.js +1 -1
- package/dist/screens/settings/settings-page.js +0 -5
- package/dist/screens/tools/tool-access-ipc.d.ts +5 -0
- package/dist/screens/tools/tool-access-ipc.js +65 -0
- package/dist/screens/tools/tool-access-ipc.test.d.ts +1 -0
- package/dist/screens/tools/tool-access-ipc.test.js +40 -0
- package/dist/screens/tools/tool-info.d.ts +1 -0
- package/dist/screens/tools/tool-info.js +3 -1
- package/package.json +3 -3
- package/src/components/access-level-field.test.tsx +32 -0
- package/src/components/access-level-field.tsx +68 -0
- package/src/components/message-logs/message-logs.tsx +58 -1
- package/src/components/message-logs/tool-access-approval-card.test.tsx +127 -0
- package/src/components/message-logs/tool-access-approval-card.tsx +178 -0
- package/src/index.tsx +0 -2
- package/src/screens/assistants/assistant-info.tsx +4 -0
- package/src/screens/assistants/assistant-list.tsx +29 -2
- package/src/screens/packages/package-versions.tsx +1 -0
- package/src/screens/settings/settings-page.tsx +0 -5
- package/src/screens/tools/tool-access-ipc.test.ts +55 -0
- package/src/screens/tools/tool-access-ipc.ts +88 -0
- package/src/screens/tools/tool-info.tsx +4 -0
- package/tsconfig.json +1 -0
- package/dist/components/voice-indicator.d.ts +0 -15
- package/dist/components/voice-indicator.js +0 -276
- package/dist/components/voice-playback.d.ts +0 -15
- package/dist/components/voice-playback.js +0 -86
- package/dist/components/voice-subscribe-events.d.ts +0 -28
- package/dist/components/voice-subscribe-events.js +0 -2
- package/dist/screens/settings/voice-settings-agent.d.ts +0 -2
- package/dist/screens/settings/voice-settings-agent.js +0 -39
- package/dist/screens/settings/voice-settings-api-keys.d.ts +0 -13
- package/dist/screens/settings/voice-settings-api-keys.js +0 -8
- package/dist/screens/settings/voice-settings-output.d.ts +0 -16
- package/dist/screens/settings/voice-settings-output.js +0 -7
- package/dist/screens/settings/voice-settings-providers.d.ts +0 -9
- package/dist/screens/settings/voice-settings-providers.js +0 -6
- package/dist/screens/settings/voice-settings-types.d.ts +0 -33
- package/dist/screens/settings/voice-settings-types.js +0 -55
- package/dist/screens/settings/voice-settings-wake-word.d.ts +0 -9
- package/dist/screens/settings/voice-settings-wake-word.js +0 -8
- package/dist/screens/settings/voice-settings.d.ts +0 -10
- package/dist/screens/settings/voice-settings.js +0 -173
- package/src/components/voice-indicator.tsx +0 -353
- package/src/components/voice-playback.tsx +0 -97
- package/src/components/voice-subscribe-events.ts +0 -16
- package/src/screens/settings/voice-settings-agent.tsx +0 -73
- package/src/screens/settings/voice-settings-api-keys.tsx +0 -91
- package/src/screens/settings/voice-settings-output.tsx +0 -126
- package/src/screens/settings/voice-settings-providers.tsx +0 -49
- package/src/screens/settings/voice-settings-types.ts +0 -94
- package/src/screens/settings/voice-settings-wake-word.tsx +0 -94
- 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;
|