@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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONTRACT_REQUEST_CHANNEL,
|
|
3
|
+
inProcessTransportPair,
|
|
4
|
+
newid,
|
|
5
|
+
registerHostContractTransport,
|
|
6
|
+
} from "@peers-app/peers-sdk";
|
|
7
|
+
import { getToolAccessControlConsumer } from "./tool-access-ipc";
|
|
8
|
+
|
|
9
|
+
function setupHost() {
|
|
10
|
+
const calls: Array<{
|
|
11
|
+
dataContextId?: string;
|
|
12
|
+
member: string;
|
|
13
|
+
name: string;
|
|
14
|
+
operation: string;
|
|
15
|
+
}> = [];
|
|
16
|
+
const [consumerTransport, providerTransport] = inProcessTransportPair();
|
|
17
|
+
providerTransport.on(CONTRACT_REQUEST_CHANNEL, (call) => {
|
|
18
|
+
calls.push(call);
|
|
19
|
+
if (call.member === "tool" && call.name === "approve") {
|
|
20
|
+
return { result: { status: "executed" } };
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
calls,
|
|
26
|
+
dispose: registerHostContractTransport(consumerTransport),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("Tool Access Control host consumers", () => {
|
|
31
|
+
it("caches per context and scopes calls to the requested context", async () => {
|
|
32
|
+
const host = setupHost();
|
|
33
|
+
const groupContextId = newid();
|
|
34
|
+
const personalContextId = newid();
|
|
35
|
+
const groupConsumer = await getToolAccessControlConsumer(groupContextId);
|
|
36
|
+
|
|
37
|
+
expect(await getToolAccessControlConsumer(groupContextId)).toBe(groupConsumer);
|
|
38
|
+
expect(await getToolAccessControlConsumer(personalContextId)).not.toBe(groupConsumer);
|
|
39
|
+
|
|
40
|
+
await groupConsumer.tools.approve({
|
|
41
|
+
approvalId: newid(),
|
|
42
|
+
nonce: newid(),
|
|
43
|
+
scope: "once",
|
|
44
|
+
});
|
|
45
|
+
expect(host.calls).toContainEqual(
|
|
46
|
+
expect.objectContaining({
|
|
47
|
+
dataContextId: groupContextId,
|
|
48
|
+
member: "tool",
|
|
49
|
+
name: "approve",
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
host.dispose();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContractConsumer,
|
|
3
|
+
getHostContractTransport,
|
|
4
|
+
getToolAccessControlContractDefinition,
|
|
5
|
+
type IContractTransport,
|
|
6
|
+
type IToolAccessControlConsumer,
|
|
7
|
+
subscribeHostContractTransport,
|
|
8
|
+
waitForHostContractTransport,
|
|
9
|
+
} from "@peers-app/peers-sdk";
|
|
10
|
+
|
|
11
|
+
const HOST_TRANSPORT_TIMEOUT_MS = 10_000;
|
|
12
|
+
|
|
13
|
+
type CachedConsumer = {
|
|
14
|
+
transport: IContractTransport;
|
|
15
|
+
consumer: IToolAccessControlConsumer;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const cachedByContext = new Map<string, CachedConsumer>();
|
|
19
|
+
const pendingByContext = new Map<string, Promise<IToolAccessControlConsumer>>();
|
|
20
|
+
|
|
21
|
+
function disposeCachedConsumers(): void {
|
|
22
|
+
for (const cached of cachedByContext.values()) {
|
|
23
|
+
cached.consumer.dispose();
|
|
24
|
+
}
|
|
25
|
+
cachedByContext.clear();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
subscribeHostContractTransport(
|
|
29
|
+
(transport) => {
|
|
30
|
+
if ([...cachedByContext.values()].some((cached) => cached.transport !== transport)) {
|
|
31
|
+
disposeCachedConsumers();
|
|
32
|
+
pendingByContext.clear();
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{ emitCurrent: false },
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
async function loadConsumer(
|
|
39
|
+
transport: IContractTransport,
|
|
40
|
+
dataContextId: string,
|
|
41
|
+
): Promise<IToolAccessControlConsumer> {
|
|
42
|
+
cachedByContext.get(dataContextId)?.consumer.dispose();
|
|
43
|
+
cachedByContext.delete(dataContextId);
|
|
44
|
+
const consumer = createContractConsumer(getToolAccessControlContractDefinition(), transport, {
|
|
45
|
+
dataContextId,
|
|
46
|
+
}) as unknown as IToolAccessControlConsumer;
|
|
47
|
+
try {
|
|
48
|
+
await consumer.loadingPromise;
|
|
49
|
+
if (getHostContractTransport() !== transport) {
|
|
50
|
+
consumer.dispose();
|
|
51
|
+
pendingByContext.delete(dataContextId);
|
|
52
|
+
return getToolAccessControlConsumer(dataContextId);
|
|
53
|
+
}
|
|
54
|
+
cachedByContext.set(dataContextId, { transport, consumer });
|
|
55
|
+
return consumer;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
consumer.dispose();
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Return a typed local-host consumer for Tool Access Control in `dataContextId`.
|
|
64
|
+
*/
|
|
65
|
+
export async function getToolAccessControlConsumer(
|
|
66
|
+
dataContextId: string,
|
|
67
|
+
): Promise<IToolAccessControlConsumer> {
|
|
68
|
+
const transport =
|
|
69
|
+
getHostContractTransport() ??
|
|
70
|
+
(await waitForHostContractTransport({ timeoutMs: HOST_TRANSPORT_TIMEOUT_MS }));
|
|
71
|
+
const cached = cachedByContext.get(dataContextId);
|
|
72
|
+
if (cached?.transport === transport) {
|
|
73
|
+
return cached.consumer;
|
|
74
|
+
}
|
|
75
|
+
const pending = pendingByContext.get(dataContextId);
|
|
76
|
+
if (pending) {
|
|
77
|
+
return pending;
|
|
78
|
+
}
|
|
79
|
+
const loadPromise = loadConsumer(transport, dataContextId);
|
|
80
|
+
pendingByContext.set(dataContextId, loadPromise);
|
|
81
|
+
try {
|
|
82
|
+
return await loadPromise;
|
|
83
|
+
} finally {
|
|
84
|
+
if (pendingByContext.get(dataContextId) === loadPromise) {
|
|
85
|
+
pendingByContext.delete(dataContextId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type IDoc, type ITool, observable } from "@peers-app/peers-sdk";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
|
+
import { AccessLevelField } from "../../components/access-level-field";
|
|
3
4
|
import { Checkbox } from "../../components/checkbox";
|
|
4
5
|
import { Input } from "../../components/input";
|
|
5
6
|
import { MarkdownEditorInline } from "../../components/markdown-editor/editor-inline";
|
|
6
7
|
|
|
8
|
+
/** Info tab for a tool, including its required access level. */
|
|
7
9
|
export const ToolInfo = (props: { tool: IDoc<ITool> }) => {
|
|
8
10
|
const { tool } = props;
|
|
9
11
|
|
|
@@ -40,6 +42,8 @@ export const ToolInfo = (props: { tool: IDoc<ITool> }) => {
|
|
|
40
42
|
title="Tool name"
|
|
41
43
|
/>
|
|
42
44
|
|
|
45
|
+
<AccessLevelField value={tool.qs.accessLevel} />
|
|
46
|
+
|
|
43
47
|
<div>
|
|
44
48
|
<label className="form-label small mt-2 me-2">Is Assistant Runner:</label>
|
|
45
49
|
<Checkbox className="form-check-input mt-2" checked={tool.qs.isAssistantRunner} />
|
package/tsconfig.json
CHANGED
|
@@ -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,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 {};
|