@fishaudio/agent-react 0.0.1
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/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/index.cjs +389 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +75 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +359 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fish Audio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @fishaudio/agent-react
|
|
2
|
+
|
|
3
|
+
React hooks and components for [Fish Audio](https://fish.audio) voice agents, built on [`@fishaudio/agent-client`](https://github.com/fishaudio/fish-agent-sdk-web/tree/main/packages/client).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @fishaudio/agent-react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires React 18+.
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
"use client";
|
|
17
|
+
|
|
18
|
+
import { useConversation } from "@fishaudio/agent-react";
|
|
19
|
+
|
|
20
|
+
export function VoiceWidget() {
|
|
21
|
+
const conversation = useConversation();
|
|
22
|
+
|
|
23
|
+
const start = async () => {
|
|
24
|
+
const sessionToken = await fetch("/api/voice-session", { method: "POST" }).then((r) => r.json());
|
|
25
|
+
await conversation.startSession({ sessionToken });
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return conversation.status === "connected" ? (
|
|
29
|
+
<button onClick={() => conversation.endSession()}>
|
|
30
|
+
End call{conversation.isSpeaking ? " · speaking" : ""}
|
|
31
|
+
</button>
|
|
32
|
+
) : (
|
|
33
|
+
<button onClick={start}>Start call</button>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The hook ends the session automatically when the component unmounts. Start calls made from the required user interaction path are safe under React StrictMode's effect replay.
|
|
39
|
+
|
|
40
|
+
## Documentation
|
|
41
|
+
|
|
42
|
+
- [React overview](https://github.com/fishaudio/fish-agent-sdk-web/blob/main/docs/react/README.md) — quickstart, Next.js.
|
|
43
|
+
- [`useConversation`](https://github.com/fishaudio/fish-agent-sdk-web/blob/main/docs/react/useConversation.md) — session lifecycle as a hook.
|
|
44
|
+
- [Provider](https://github.com/fishaudio/fish-agent-sdk-web/blob/main/docs/react/provider.md) — share one conversation across a component tree.
|
|
45
|
+
- [`useAgentMessages`](https://github.com/fishaudio/fish-agent-sdk-web/blob/main/docs/react/useAgentMessages.md) — build a streaming chat UI.
|
|
46
|
+
- [Audio visualization](https://github.com/fishaudio/fish-agent-sdk-web/blob/main/docs/react/audio-visualization.md) — `useAudioLevels` and `<AgentAudioVisualizer />`.
|
|
47
|
+
|
|
48
|
+
Authentication, events, client tools, and errors are documented with the [client SDK](https://github.com/fishaudio/fish-agent-sdk-web/blob/main/docs/README.md) — everything there applies here.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
AgentAudioVisualizer: () => AgentAudioVisualizer,
|
|
24
|
+
AgentSession: () => import_agent_client2.AgentSession,
|
|
25
|
+
AgentSessionProvider: () => AgentSessionProvider,
|
|
26
|
+
FishAgentError: () => import_agent_client2.FishAgentError,
|
|
27
|
+
useAgentMessages: () => useAgentMessages,
|
|
28
|
+
useAgentSessionContext: () => useAgentSessionContext,
|
|
29
|
+
useAudioLevels: () => useAudioLevels,
|
|
30
|
+
useConversation: () => useConversation,
|
|
31
|
+
useOptionalAgentSessionContext: () => useOptionalAgentSessionContext
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(src_exports);
|
|
34
|
+
|
|
35
|
+
// src/useConversation.ts
|
|
36
|
+
var import_agent_client = require("@fishaudio/agent-client");
|
|
37
|
+
var import_react = require("react");
|
|
38
|
+
function startCancelledError() {
|
|
39
|
+
const error = new Error("Conversation start was cancelled by endSession");
|
|
40
|
+
error.name = "AbortError";
|
|
41
|
+
return error;
|
|
42
|
+
}
|
|
43
|
+
function useConversation(defaults = {}) {
|
|
44
|
+
const defaultsRef = (0, import_react.useRef)(defaults);
|
|
45
|
+
defaultsRef.current = defaults;
|
|
46
|
+
const sessionRef = (0, import_react.useRef)(null);
|
|
47
|
+
const startingRef = (0, import_react.useRef)(null);
|
|
48
|
+
const abandonedSessionRef = (0, import_react.useRef)(null);
|
|
49
|
+
const lifecycleEpochRef = (0, import_react.useRef)(0);
|
|
50
|
+
const mountedRef = (0, import_react.useRef)(true);
|
|
51
|
+
const [session, setSession] = (0, import_react.useState)(null);
|
|
52
|
+
const [status, setStatus] = (0, import_react.useState)("idle");
|
|
53
|
+
const [mode, setMode] = (0, import_react.useState)("listening");
|
|
54
|
+
const [micMuted, setMicMutedState] = (0, import_react.useState)(false);
|
|
55
|
+
const releaseAbandonedSession = (0, import_react.useCallback)(async () => {
|
|
56
|
+
const abandoned = abandonedSessionRef.current;
|
|
57
|
+
if (!abandoned) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
await abandoned.end();
|
|
61
|
+
if (abandonedSessionRef.current === abandoned) {
|
|
62
|
+
abandonedSessionRef.current = null;
|
|
63
|
+
}
|
|
64
|
+
}, []);
|
|
65
|
+
const startSession = (0, import_react.useCallback)(
|
|
66
|
+
async (overrides = {}) => {
|
|
67
|
+
const requestEpoch = lifecycleEpochRef.current;
|
|
68
|
+
const startAtEpoch = async () => {
|
|
69
|
+
if (!mountedRef.current) {
|
|
70
|
+
throw new Error("Cannot start a conversation after useConversation has unmounted");
|
|
71
|
+
}
|
|
72
|
+
if (requestEpoch !== lifecycleEpochRef.current) {
|
|
73
|
+
throw startCancelledError();
|
|
74
|
+
}
|
|
75
|
+
if (abandonedSessionRef.current) {
|
|
76
|
+
await releaseAbandonedSession();
|
|
77
|
+
if (requestEpoch !== lifecycleEpochRef.current) {
|
|
78
|
+
throw startCancelledError();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const current = sessionRef.current;
|
|
82
|
+
if (current) {
|
|
83
|
+
if (current.status !== "ended") {
|
|
84
|
+
return current.sessionId;
|
|
85
|
+
}
|
|
86
|
+
await current.end();
|
|
87
|
+
if (requestEpoch !== lifecycleEpochRef.current) {
|
|
88
|
+
throw startCancelledError();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const existing = startingRef.current;
|
|
92
|
+
if (existing) {
|
|
93
|
+
if (!existing.state.cancelled) {
|
|
94
|
+
return existing.promise;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
await existing.promise;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (existing.state.sessionCreated) {
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (requestEpoch !== lifecycleEpochRef.current) {
|
|
104
|
+
throw startCancelledError();
|
|
105
|
+
}
|
|
106
|
+
return startAtEpoch();
|
|
107
|
+
}
|
|
108
|
+
setStatus("connecting");
|
|
109
|
+
const state = { cancelled: false, sessionCreated: false };
|
|
110
|
+
const starting = (async () => {
|
|
111
|
+
try {
|
|
112
|
+
const next = await import_agent_client.AgentSession.start({
|
|
113
|
+
...defaultsRef.current,
|
|
114
|
+
...overrides
|
|
115
|
+
});
|
|
116
|
+
state.sessionCreated = true;
|
|
117
|
+
if (state.cancelled || requestEpoch !== lifecycleEpochRef.current || !mountedRef.current) {
|
|
118
|
+
try {
|
|
119
|
+
await next.end();
|
|
120
|
+
} catch (error) {
|
|
121
|
+
abandonedSessionRef.current = next;
|
|
122
|
+
if (!mountedRef.current) {
|
|
123
|
+
void releaseAbandonedSession().catch(() => void 0);
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
return next.sessionId;
|
|
128
|
+
}
|
|
129
|
+
sessionRef.current = next;
|
|
130
|
+
setSession(next);
|
|
131
|
+
setMicMutedState(next.micMuted);
|
|
132
|
+
setMode(next.mode);
|
|
133
|
+
setStatus(next.status);
|
|
134
|
+
next.on("statusChange", setStatus);
|
|
135
|
+
next.on("modeChange", setMode);
|
|
136
|
+
return next.sessionId;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (!state.cancelled && mountedRef.current) {
|
|
139
|
+
setStatus("idle");
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
} finally {
|
|
143
|
+
if (startingRef.current?.state === state) {
|
|
144
|
+
startingRef.current = null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
})();
|
|
148
|
+
const pending = { state, promise: starting };
|
|
149
|
+
startingRef.current = pending;
|
|
150
|
+
return starting;
|
|
151
|
+
};
|
|
152
|
+
return startAtEpoch();
|
|
153
|
+
},
|
|
154
|
+
[releaseAbandonedSession]
|
|
155
|
+
);
|
|
156
|
+
const endSession = (0, import_react.useCallback)(async () => {
|
|
157
|
+
lifecycleEpochRef.current += 1;
|
|
158
|
+
const pending = startingRef.current;
|
|
159
|
+
const current = sessionRef.current;
|
|
160
|
+
const abandoned = abandonedSessionRef.current;
|
|
161
|
+
if (!pending && !abandoned && !current) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (pending) {
|
|
165
|
+
pending.state.cancelled = true;
|
|
166
|
+
if (mountedRef.current) {
|
|
167
|
+
setStatus("ended");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
await current?.end();
|
|
171
|
+
if (abandoned) {
|
|
172
|
+
await releaseAbandonedSession();
|
|
173
|
+
}
|
|
174
|
+
if (pending) {
|
|
175
|
+
try {
|
|
176
|
+
await pending.promise;
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (pending.state.sessionCreated) {
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}, [releaseAbandonedSession]);
|
|
184
|
+
const setMicMuted = (0, import_react.useCallback)(async (muted) => {
|
|
185
|
+
await sessionRef.current?.setMicMuted(muted);
|
|
186
|
+
setMicMutedState(muted);
|
|
187
|
+
}, []);
|
|
188
|
+
(0, import_react.useEffect)(() => {
|
|
189
|
+
mountedRef.current = true;
|
|
190
|
+
return () => {
|
|
191
|
+
mountedRef.current = false;
|
|
192
|
+
lifecycleEpochRef.current += 1;
|
|
193
|
+
const pending = startingRef.current;
|
|
194
|
+
if (pending) {
|
|
195
|
+
pending.state.cancelled = true;
|
|
196
|
+
}
|
|
197
|
+
const current = sessionRef.current;
|
|
198
|
+
if (current) {
|
|
199
|
+
current.off("statusChange", setStatus);
|
|
200
|
+
current.off("modeChange", setMode);
|
|
201
|
+
void current.end().catch(() => void 0);
|
|
202
|
+
}
|
|
203
|
+
void releaseAbandonedSession().catch(() => void 0);
|
|
204
|
+
};
|
|
205
|
+
}, [releaseAbandonedSession]);
|
|
206
|
+
return {
|
|
207
|
+
session,
|
|
208
|
+
status,
|
|
209
|
+
mode,
|
|
210
|
+
isSpeaking: mode === "speaking",
|
|
211
|
+
micMuted,
|
|
212
|
+
startSession,
|
|
213
|
+
endSession,
|
|
214
|
+
setMicMuted,
|
|
215
|
+
sendUserMessage: (0, import_react.useCallback)(
|
|
216
|
+
(text, options) => sessionRef.current?.sendUserMessage(text, options),
|
|
217
|
+
[]
|
|
218
|
+
),
|
|
219
|
+
sendUserActivity: (0, import_react.useCallback)(() => sessionRef.current?.sendUserActivity(), []),
|
|
220
|
+
interrupt: (0, import_react.useCallback)(() => sessionRef.current?.interrupt(), []),
|
|
221
|
+
startAudio: (0, import_react.useCallback)(async () => sessionRef.current?.startAudio(), []),
|
|
222
|
+
setOutputVolume: (0, import_react.useCallback)(
|
|
223
|
+
(volume) => sessionRef.current?.setOutputVolume(volume),
|
|
224
|
+
[]
|
|
225
|
+
),
|
|
226
|
+
setInputDevice: (0, import_react.useCallback)(
|
|
227
|
+
async (deviceId) => sessionRef.current?.setInputDevice(deviceId),
|
|
228
|
+
[]
|
|
229
|
+
),
|
|
230
|
+
setOutputDevice: (0, import_react.useCallback)(
|
|
231
|
+
async (deviceId) => sessionRef.current?.setOutputDevice(deviceId),
|
|
232
|
+
[]
|
|
233
|
+
)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/provider.tsx
|
|
238
|
+
var import_react2 = require("react");
|
|
239
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
240
|
+
var ConversationContext = (0, import_react2.createContext)(null);
|
|
241
|
+
function AgentSessionProvider({
|
|
242
|
+
options,
|
|
243
|
+
children
|
|
244
|
+
}) {
|
|
245
|
+
const conversation = useConversation(options ?? {});
|
|
246
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ConversationContext.Provider, { value: conversation, children });
|
|
247
|
+
}
|
|
248
|
+
function useAgentSessionContext() {
|
|
249
|
+
const context = (0, import_react2.useContext)(ConversationContext);
|
|
250
|
+
if (!context) {
|
|
251
|
+
throw new Error("useAgentSessionContext must be used within <AgentSessionProvider>");
|
|
252
|
+
}
|
|
253
|
+
return context;
|
|
254
|
+
}
|
|
255
|
+
function useOptionalAgentSessionContext() {
|
|
256
|
+
return (0, import_react2.useContext)(ConversationContext);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/useAgentMessages.ts
|
|
260
|
+
var import_react3 = require("react");
|
|
261
|
+
function useAgentMessages(session) {
|
|
262
|
+
const context = useOptionalAgentSessionContext();
|
|
263
|
+
const active = session !== void 0 ? session : context?.session ?? null;
|
|
264
|
+
const [messages, setMessages] = (0, import_react3.useState)([]);
|
|
265
|
+
(0, import_react3.useEffect)(() => {
|
|
266
|
+
if (!active) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
setMessages(
|
|
270
|
+
active.getTranscript().map((segment) => ({
|
|
271
|
+
key: `${segment.role}-${segment.segmentId}`,
|
|
272
|
+
role: segment.role,
|
|
273
|
+
text: segment.text,
|
|
274
|
+
final: segment.final
|
|
275
|
+
}))
|
|
276
|
+
);
|
|
277
|
+
const upsert = (key, role, text, final) => {
|
|
278
|
+
setMessages((previous) => {
|
|
279
|
+
const index = previous.findIndex((message) => message.key === key);
|
|
280
|
+
const entry = { key, role, text, final };
|
|
281
|
+
if (index === -1) {
|
|
282
|
+
return [...previous, entry];
|
|
283
|
+
}
|
|
284
|
+
const next = [...previous];
|
|
285
|
+
next[index] = entry;
|
|
286
|
+
return next;
|
|
287
|
+
});
|
|
288
|
+
};
|
|
289
|
+
const onUserTranscript = (event) => upsert(`user-${event.segmentId}`, "user", event.text, event.final);
|
|
290
|
+
const onAgentDelta = (event) => upsert(`agent-${event.segmentId}`, "agent", event.text, false);
|
|
291
|
+
const onAgentResponse = (event) => upsert(`agent-${event.segmentId}`, "agent", event.text, true);
|
|
292
|
+
active.on("userTranscript", onUserTranscript);
|
|
293
|
+
active.on("agentResponseDelta", onAgentDelta);
|
|
294
|
+
active.on("agentResponse", onAgentResponse);
|
|
295
|
+
return () => {
|
|
296
|
+
active.off("userTranscript", onUserTranscript);
|
|
297
|
+
active.off("agentResponseDelta", onAgentDelta);
|
|
298
|
+
active.off("agentResponse", onAgentResponse);
|
|
299
|
+
};
|
|
300
|
+
}, [active]);
|
|
301
|
+
return messages;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/useAudioLevels.ts
|
|
305
|
+
var import_react4 = require("react");
|
|
306
|
+
function useAudioLevels(session, fps = 20) {
|
|
307
|
+
const context = useOptionalAgentSessionContext();
|
|
308
|
+
const active = session !== void 0 ? session : context?.session ?? null;
|
|
309
|
+
const [levels, setLevels] = (0, import_react4.useState)({ input: 0, output: 0 });
|
|
310
|
+
(0, import_react4.useEffect)(() => {
|
|
311
|
+
if (!active) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const timer = setInterval(
|
|
315
|
+
() => {
|
|
316
|
+
setLevels({ input: active.getInputVolume(), output: active.getOutputVolume() });
|
|
317
|
+
},
|
|
318
|
+
Math.max(1e3 / fps, 16)
|
|
319
|
+
);
|
|
320
|
+
return () => {
|
|
321
|
+
clearInterval(timer);
|
|
322
|
+
};
|
|
323
|
+
}, [active, fps]);
|
|
324
|
+
return levels;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/AgentAudioVisualizer.tsx
|
|
328
|
+
var import_react5 = require("react");
|
|
329
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
330
|
+
function AgentAudioVisualizer({
|
|
331
|
+
session,
|
|
332
|
+
bars = 24,
|
|
333
|
+
width = 240,
|
|
334
|
+
height = 64,
|
|
335
|
+
className
|
|
336
|
+
}) {
|
|
337
|
+
const context = useOptionalAgentSessionContext();
|
|
338
|
+
const active = session !== void 0 ? session : context?.session ?? null;
|
|
339
|
+
const canvasRef = (0, import_react5.useRef)(null);
|
|
340
|
+
(0, import_react5.useEffect)(() => {
|
|
341
|
+
const canvas = canvasRef.current;
|
|
342
|
+
if (!canvas || !active) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const context2d = canvas.getContext("2d");
|
|
346
|
+
if (!context2d) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
let frame = 0;
|
|
350
|
+
const draw = () => {
|
|
351
|
+
const data = active.getOutputFrequencyData();
|
|
352
|
+
context2d.clearRect(0, 0, canvas.width, canvas.height);
|
|
353
|
+
context2d.fillStyle = getComputedStyle(canvas).color;
|
|
354
|
+
const barWidth = canvas.width / bars;
|
|
355
|
+
for (let index = 0; index < bars; index += 1) {
|
|
356
|
+
const sample = data.length ? (data[Math.floor(index / bars * data.length)] ?? 0) / 255 : 0;
|
|
357
|
+
const barHeight = Math.max(2, sample * canvas.height);
|
|
358
|
+
context2d.fillRect(
|
|
359
|
+
index * barWidth + barWidth * 0.15,
|
|
360
|
+
(canvas.height - barHeight) / 2,
|
|
361
|
+
barWidth * 0.7,
|
|
362
|
+
barHeight
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
frame = requestAnimationFrame(draw);
|
|
366
|
+
};
|
|
367
|
+
frame = requestAnimationFrame(draw);
|
|
368
|
+
return () => {
|
|
369
|
+
cancelAnimationFrame(frame);
|
|
370
|
+
};
|
|
371
|
+
}, [active, bars]);
|
|
372
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("canvas", { ref: canvasRef, width, height, className });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/index.ts
|
|
376
|
+
var import_agent_client2 = require("@fishaudio/agent-client");
|
|
377
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
378
|
+
0 && (module.exports = {
|
|
379
|
+
AgentAudioVisualizer,
|
|
380
|
+
AgentSession,
|
|
381
|
+
AgentSessionProvider,
|
|
382
|
+
FishAgentError,
|
|
383
|
+
useAgentMessages,
|
|
384
|
+
useAgentSessionContext,
|
|
385
|
+
useAudioLevels,
|
|
386
|
+
useConversation,
|
|
387
|
+
useOptionalAgentSessionContext
|
|
388
|
+
});
|
|
389
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/useConversation.ts","../src/provider.tsx","../src/useAgentMessages.ts","../src/useAudioLevels.ts","../src/AgentAudioVisualizer.tsx"],"sourcesContent":["export { useConversation, type ConversationStatus, type UseConversationReturn } from \"./useConversation.js\";\nexport {\n AgentSessionProvider,\n useAgentSessionContext,\n useOptionalAgentSessionContext,\n type AgentSessionProviderProps,\n} from \"./provider.js\";\nexport { useAgentMessages, type ChatMessage } from \"./useAgentMessages.js\";\nexport { useAudioLevels, type AudioLevels } from \"./useAudioLevels.js\";\nexport {\n AgentAudioVisualizer,\n type AgentAudioVisualizerProps,\n} from \"./AgentAudioVisualizer.js\";\n// Convenience re-exports so app code can stay on one import.\nexport {\n AgentSession,\n FishAgentError,\n type AgentMode,\n type AgentSessionOptions,\n type ConversationMessage,\n type EndReason,\n type SessionStatus,\n type SessionToken,\n type TranscriptSegment,\n} from \"@fishaudio/agent-client\";\n","import {\n AgentSession,\n type AgentMode,\n type AgentSessionOptions,\n type SessionStatus,\n} from \"@fishaudio/agent-client\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\n/** \"idle\" = no session yet (or a failed start); the rest mirror the session. */\nexport type ConversationStatus = SessionStatus | \"idle\";\n\nexport interface UseConversationReturn {\n /** Live session object for event access; null before the first start. */\n session: AgentSession | null;\n status: ConversationStatus;\n mode: AgentMode;\n isSpeaking: boolean;\n micMuted: boolean;\n startSession: (overrides?: Partial<AgentSessionOptions>) => Promise<string>;\n endSession: () => Promise<void>;\n setMicMuted: (muted: boolean) => Promise<void>;\n sendUserMessage: (text: string, options?: { audio?: boolean }) => void;\n sendUserActivity: () => void;\n interrupt: () => void;\n startAudio: () => Promise<void>;\n setOutputVolume: (volume: number) => void;\n setInputDevice: (deviceId: string) => Promise<void>;\n setOutputDevice: (deviceId: string) => Promise<void>;\n}\n\ninterface PendingStart {\n state: { cancelled: boolean; sessionCreated: boolean };\n promise: Promise<string>;\n}\n\nfunction startCancelledError(): Error {\n const error = new Error(\"Conversation start was cancelled by endSession\");\n error.name = \"AbortError\";\n return error;\n}\n\nexport function useConversation(\n defaults: Partial<AgentSessionOptions> = {},\n): UseConversationReturn {\n const defaultsRef = useRef(defaults);\n defaultsRef.current = defaults;\n\n const sessionRef = useRef<AgentSession | null>(null);\n const startingRef = useRef<PendingStart | null>(null);\n const abandonedSessionRef = useRef<AgentSession | null>(null);\n const lifecycleEpochRef = useRef(0);\n const mountedRef = useRef(true);\n const [session, setSession] = useState<AgentSession | null>(null);\n const [status, setStatus] = useState<ConversationStatus>(\"idle\");\n const [mode, setMode] = useState<AgentMode>(\"listening\");\n const [micMuted, setMicMutedState] = useState(false);\n\n const releaseAbandonedSession = useCallback(async () => {\n const abandoned = abandonedSessionRef.current;\n if (!abandoned) {\n return;\n }\n await abandoned.end();\n if (abandonedSessionRef.current === abandoned) {\n abandonedSessionRef.current = null;\n }\n }, []);\n\n const startSession = useCallback(\n async (overrides: Partial<AgentSessionOptions> = {}): Promise<string> => {\n const requestEpoch = lifecycleEpochRef.current;\n\n const startAtEpoch = async (): Promise<string> => {\n if (!mountedRef.current) {\n throw new Error(\"Cannot start a conversation after useConversation has unmounted\");\n }\n if (requestEpoch !== lifecycleEpochRef.current) {\n throw startCancelledError();\n }\n\n if (abandonedSessionRef.current) {\n await releaseAbandonedSession();\n if (requestEpoch !== lifecycleEpochRef.current) {\n throw startCancelledError();\n }\n }\n\n const current = sessionRef.current;\n if (current) {\n if (current.status !== \"ended\") {\n return current.sessionId;\n }\n // status can reach ended before an in-flight transport teardown\n // promise settles. AgentSession.end() joins that promise, so a fresh\n // session never overlaps the old connection's cleanup.\n await current.end();\n if (requestEpoch !== lifecycleEpochRef.current) {\n throw startCancelledError();\n }\n }\n\n // Double-click guard: a second call while a start is in flight joins\n // the first. A call queued after hangup waits for the cancelled start\n // to release its resources before it creates a fresh session.\n const existing = startingRef.current;\n if (existing) {\n if (!existing.state.cancelled) {\n return existing.promise;\n }\n try {\n await existing.promise;\n } catch (error) {\n // A failed session request leaves nothing to clean up, so a queued\n // restart may continue. A teardown failure means the old session may\n // still own resources and must stop the restart.\n if (existing.state.sessionCreated) {\n throw error;\n }\n }\n if (requestEpoch !== lifecycleEpochRef.current) {\n throw startCancelledError();\n }\n return startAtEpoch();\n }\n\n setStatus(\"connecting\");\n const state = { cancelled: false, sessionCreated: false };\n const starting = (async () => {\n try {\n const next = await AgentSession.start({\n ...defaultsRef.current,\n ...overrides,\n } as AgentSessionOptions);\n state.sessionCreated = true;\n // endSession or a real unmount may happen while permissions/session\n // creation are still pending. Never publish that late session into\n // the hook; close it before it can leak its microphone or connection.\n if (\n state.cancelled ||\n requestEpoch !== lifecycleEpochRef.current ||\n !mountedRef.current\n ) {\n try {\n await next.end();\n } catch (error) {\n abandonedSessionRef.current = next;\n if (!mountedRef.current) {\n void releaseAbandonedSession().catch(() => undefined);\n }\n throw error;\n }\n return next.sessionId;\n }\n sessionRef.current = next;\n setSession(next);\n // Mirror, don't assume: a microphone:false start joins muted.\n setMicMutedState(next.micMuted);\n setMode(next.mode);\n setStatus(next.status);\n next.on(\"statusChange\", setStatus);\n next.on(\"modeChange\", setMode);\n return next.sessionId;\n } catch (error) {\n if (!state.cancelled && mountedRef.current) {\n setStatus(\"idle\");\n }\n throw error;\n } finally {\n if (startingRef.current?.state === state) {\n startingRef.current = null;\n }\n }\n })();\n const pending: PendingStart = { state, promise: starting };\n startingRef.current = pending;\n return starting;\n };\n\n return startAtEpoch();\n },\n [releaseAbandonedSession],\n );\n\n const endSession = useCallback(async () => {\n lifecycleEpochRef.current += 1;\n const pending = startingRef.current;\n const current = sessionRef.current;\n const abandoned = abandonedSessionRef.current;\n if (!pending && !abandoned && !current) {\n return;\n }\n\n if (pending) {\n pending.state.cancelled = true;\n if (mountedRef.current) {\n setStatus(\"ended\");\n }\n }\n await current?.end();\n if (abandoned) {\n await releaseAbandonedSession();\n }\n if (pending) {\n try {\n await pending.promise;\n } catch (error) {\n // The original startSession caller still receives a start failure. From\n // endSession's perspective, a failed request means there is nothing to\n // release; a failure after creation is a teardown failure and propagates.\n if (pending.state.sessionCreated) {\n throw error;\n }\n }\n }\n }, [releaseAbandonedSession]);\n\n const setMicMuted = useCallback(async (muted: boolean) => {\n await sessionRef.current?.setMicMuted(muted);\n setMicMutedState(muted);\n }, []);\n\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n lifecycleEpochRef.current += 1;\n const pending = startingRef.current;\n if (pending) {\n pending.state.cancelled = true;\n }\n const current = sessionRef.current;\n if (current) {\n current.off(\"statusChange\", setStatus);\n current.off(\"modeChange\", setMode);\n void current.end().catch(() => undefined);\n }\n void releaseAbandonedSession().catch(() => undefined);\n };\n }, [releaseAbandonedSession]);\n\n return {\n session,\n status,\n mode,\n isSpeaking: mode === \"speaking\",\n micMuted,\n startSession,\n endSession,\n setMicMuted,\n sendUserMessage: useCallback(\n (text: string, options?: { audio?: boolean }) =>\n sessionRef.current?.sendUserMessage(text, options),\n [],\n ),\n sendUserActivity: useCallback(() => sessionRef.current?.sendUserActivity(), []),\n interrupt: useCallback(() => sessionRef.current?.interrupt(), []),\n startAudio: useCallback(async () => sessionRef.current?.startAudio(), []),\n setOutputVolume: useCallback(\n (volume: number) => sessionRef.current?.setOutputVolume(volume),\n [],\n ),\n setInputDevice: useCallback(\n async (deviceId: string) => sessionRef.current?.setInputDevice(deviceId),\n [],\n ),\n setOutputDevice: useCallback(\n async (deviceId: string) => sessionRef.current?.setOutputDevice(deviceId),\n [],\n ),\n };\n}\n","import type { AgentSessionOptions } from \"@fishaudio/agent-client\";\nimport { createContext, useContext, type PropsWithChildren, type ReactElement } from \"react\";\nimport { useConversation, type UseConversationReturn } from \"./useConversation.js\";\n\nconst ConversationContext = createContext<UseConversationReturn | null>(null);\n\nexport type AgentSessionProviderProps = PropsWithChildren<{\n options?: Partial<AgentSessionOptions>;\n}>;\n\n/** Hosts one conversation for a component tree. */\nexport function AgentSessionProvider({\n options,\n children,\n}: AgentSessionProviderProps): ReactElement {\n const conversation = useConversation(options ?? {});\n return (\n <ConversationContext.Provider value={conversation}>{children}</ConversationContext.Provider>\n );\n}\n\nexport function useAgentSessionContext(): UseConversationReturn {\n const context = useContext(ConversationContext);\n if (!context) {\n throw new Error(\"useAgentSessionContext must be used within <AgentSessionProvider>\");\n }\n return context;\n}\n\n/** Nullable variant for components that also accept an explicit session prop. */\nexport function useOptionalAgentSessionContext(): UseConversationReturn | null {\n return useContext(ConversationContext);\n}\n","import type { AgentSession } from \"@fishaudio/agent-client\";\nimport { useEffect, useState } from \"react\";\nimport { useOptionalAgentSessionContext } from \"./provider.js\";\n\nexport interface ChatMessage {\n /** Stable per-segment key for list rendering. */\n key: string;\n role: \"user\" | \"agent\";\n text: string;\n /** False while the segment is still streaming (interim transcript / deltas). */\n final: boolean;\n}\n\n/**\n * Streaming chat log: user segments update in place as transcription refines,\n * agent segments grow with each delta and pin on completion. Pass a session or\n * rely on the surrounding <AgentSessionProvider>.\n */\nexport function useAgentMessages(session?: AgentSession | null): ChatMessage[] {\n const context = useOptionalAgentSessionContext();\n const active = session !== undefined ? session : (context?.session ?? null);\n const [messages, setMessages] = useState<ChatMessage[]>([]);\n\n useEffect(() => {\n if (!active) {\n return;\n }\n // Backfill events emitted before this effect ran (React commits the session\n // after startSession resolves); live events then update the same keys.\n setMessages(\n active.getTranscript().map((segment) => ({\n key: `${segment.role}-${segment.segmentId}`,\n role: segment.role,\n text: segment.text,\n final: segment.final,\n })),\n );\n\n const upsert = (key: string, role: \"user\" | \"agent\", text: string, final: boolean) => {\n setMessages((previous) => {\n const index = previous.findIndex((message) => message.key === key);\n const entry: ChatMessage = { key, role, text, final };\n if (index === -1) {\n return [...previous, entry];\n }\n const next = [...previous];\n next[index] = entry;\n return next;\n });\n };\n\n const onUserTranscript = (event: { segmentId: string; text: string; final: boolean }) =>\n upsert(`user-${event.segmentId}`, \"user\", event.text, event.final);\n const onAgentDelta = (event: { segmentId: string; text: string }) =>\n upsert(`agent-${event.segmentId}`, \"agent\", event.text, false);\n const onAgentResponse = (event: { segmentId: string; text: string }) =>\n upsert(`agent-${event.segmentId}`, \"agent\", event.text, true);\n\n active.on(\"userTranscript\", onUserTranscript);\n active.on(\"agentResponseDelta\", onAgentDelta);\n active.on(\"agentResponse\", onAgentResponse);\n return () => {\n active.off(\"userTranscript\", onUserTranscript);\n active.off(\"agentResponseDelta\", onAgentDelta);\n active.off(\"agentResponse\", onAgentResponse);\n };\n }, [active]);\n\n return messages;\n}\n","import type { AgentSession } from \"@fishaudio/agent-client\";\nimport { useEffect, useState } from \"react\";\nimport { useOptionalAgentSessionContext } from \"./provider.js\";\n\nexport interface AudioLevels {\n /** Microphone RMS in [0, 1]. */\n input: number;\n /** Agent audio RMS in [0, 1]. */\n output: number;\n}\n\n/** Polls session audio levels for meters/animations. */\nexport function useAudioLevels(session?: AgentSession | null, fps = 20): AudioLevels {\n const context = useOptionalAgentSessionContext();\n const active = session !== undefined ? session : (context?.session ?? null);\n const [levels, setLevels] = useState<AudioLevels>({ input: 0, output: 0 });\n\n useEffect(() => {\n if (!active) {\n return;\n }\n const timer = setInterval(\n () => {\n setLevels({ input: active.getInputVolume(), output: active.getOutputVolume() });\n },\n Math.max(1000 / fps, 16),\n );\n return () => {\n clearInterval(timer);\n };\n }, [active, fps]);\n\n return levels;\n}\n","import type { AgentSession } from \"@fishaudio/agent-client\";\nimport { useEffect, useRef, type ReactElement } from \"react\";\nimport { useOptionalAgentSessionContext } from \"./provider.js\";\n\nexport interface AgentAudioVisualizerProps {\n session?: AgentSession | null;\n bars?: number;\n width?: number;\n height?: number;\n className?: string;\n}\n\n/**\n * Frequency bars for the agent's voice, drawn from the session's analyser data\n * (no transport objects involved). Colors follow CSS `color` on the canvas.\n */\nexport function AgentAudioVisualizer({\n session,\n bars = 24,\n width = 240,\n height = 64,\n className,\n}: AgentAudioVisualizerProps): ReactElement {\n const context = useOptionalAgentSessionContext();\n const active = session !== undefined ? session : (context?.session ?? null);\n const canvasRef = useRef<HTMLCanvasElement | null>(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas || !active) {\n return;\n }\n const context2d = canvas.getContext(\"2d\");\n if (!context2d) {\n return;\n }\n\n let frame = 0;\n const draw = () => {\n const data = active.getOutputFrequencyData();\n context2d.clearRect(0, 0, canvas.width, canvas.height);\n context2d.fillStyle = getComputedStyle(canvas).color;\n const barWidth = canvas.width / bars;\n for (let index = 0; index < bars; index += 1) {\n const sample = data.length\n ? (data[Math.floor((index / bars) * data.length)] ?? 0) / 255\n : 0;\n const barHeight = Math.max(2, sample * canvas.height);\n context2d.fillRect(\n index * barWidth + barWidth * 0.15,\n (canvas.height - barHeight) / 2,\n barWidth * 0.7,\n barHeight,\n );\n }\n frame = requestAnimationFrame(draw);\n };\n frame = requestAnimationFrame(draw);\n return () => {\n cancelAnimationFrame(frame);\n };\n }, [active, bars]);\n\n return <canvas ref={canvasRef} width={width} height={height} className={className} />;\n}\n"],"mappings":"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","names":["import_react","import_react","import_react","import_react","import_jsx_runtime","import_agent_client"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { SessionStatus, AgentSession, AgentMode, AgentSessionOptions } from '@fishaudio/agent-client';
|
|
2
|
+
export { AgentMode, AgentSession, AgentSessionOptions, ConversationMessage, EndReason, FishAgentError, SessionStatus, SessionToken, TranscriptSegment } from '@fishaudio/agent-client';
|
|
3
|
+
import { PropsWithChildren, ReactElement } from 'react';
|
|
4
|
+
|
|
5
|
+
/** "idle" = no session yet (or a failed start); the rest mirror the session. */
|
|
6
|
+
type ConversationStatus = SessionStatus | "idle";
|
|
7
|
+
interface UseConversationReturn {
|
|
8
|
+
/** Live session object for event access; null before the first start. */
|
|
9
|
+
session: AgentSession | null;
|
|
10
|
+
status: ConversationStatus;
|
|
11
|
+
mode: AgentMode;
|
|
12
|
+
isSpeaking: boolean;
|
|
13
|
+
micMuted: boolean;
|
|
14
|
+
startSession: (overrides?: Partial<AgentSessionOptions>) => Promise<string>;
|
|
15
|
+
endSession: () => Promise<void>;
|
|
16
|
+
setMicMuted: (muted: boolean) => Promise<void>;
|
|
17
|
+
sendUserMessage: (text: string, options?: {
|
|
18
|
+
audio?: boolean;
|
|
19
|
+
}) => void;
|
|
20
|
+
sendUserActivity: () => void;
|
|
21
|
+
interrupt: () => void;
|
|
22
|
+
startAudio: () => Promise<void>;
|
|
23
|
+
setOutputVolume: (volume: number) => void;
|
|
24
|
+
setInputDevice: (deviceId: string) => Promise<void>;
|
|
25
|
+
setOutputDevice: (deviceId: string) => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
declare function useConversation(defaults?: Partial<AgentSessionOptions>): UseConversationReturn;
|
|
28
|
+
|
|
29
|
+
type AgentSessionProviderProps = PropsWithChildren<{
|
|
30
|
+
options?: Partial<AgentSessionOptions>;
|
|
31
|
+
}>;
|
|
32
|
+
/** Hosts one conversation for a component tree. */
|
|
33
|
+
declare function AgentSessionProvider({ options, children, }: AgentSessionProviderProps): ReactElement;
|
|
34
|
+
declare function useAgentSessionContext(): UseConversationReturn;
|
|
35
|
+
/** Nullable variant for components that also accept an explicit session prop. */
|
|
36
|
+
declare function useOptionalAgentSessionContext(): UseConversationReturn | null;
|
|
37
|
+
|
|
38
|
+
interface ChatMessage {
|
|
39
|
+
/** Stable per-segment key for list rendering. */
|
|
40
|
+
key: string;
|
|
41
|
+
role: "user" | "agent";
|
|
42
|
+
text: string;
|
|
43
|
+
/** False while the segment is still streaming (interim transcript / deltas). */
|
|
44
|
+
final: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Streaming chat log: user segments update in place as transcription refines,
|
|
48
|
+
* agent segments grow with each delta and pin on completion. Pass a session or
|
|
49
|
+
* rely on the surrounding <AgentSessionProvider>.
|
|
50
|
+
*/
|
|
51
|
+
declare function useAgentMessages(session?: AgentSession | null): ChatMessage[];
|
|
52
|
+
|
|
53
|
+
interface AudioLevels {
|
|
54
|
+
/** Microphone RMS in [0, 1]. */
|
|
55
|
+
input: number;
|
|
56
|
+
/** Agent audio RMS in [0, 1]. */
|
|
57
|
+
output: number;
|
|
58
|
+
}
|
|
59
|
+
/** Polls session audio levels for meters/animations. */
|
|
60
|
+
declare function useAudioLevels(session?: AgentSession | null, fps?: number): AudioLevels;
|
|
61
|
+
|
|
62
|
+
interface AgentAudioVisualizerProps {
|
|
63
|
+
session?: AgentSession | null;
|
|
64
|
+
bars?: number;
|
|
65
|
+
width?: number;
|
|
66
|
+
height?: number;
|
|
67
|
+
className?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Frequency bars for the agent's voice, drawn from the session's analyser data
|
|
71
|
+
* (no transport objects involved). Colors follow CSS `color` on the canvas.
|
|
72
|
+
*/
|
|
73
|
+
declare function AgentAudioVisualizer({ session, bars, width, height, className, }: AgentAudioVisualizerProps): ReactElement;
|
|
74
|
+
|
|
75
|
+
export { AgentAudioVisualizer, type AgentAudioVisualizerProps, AgentSessionProvider, type AgentSessionProviderProps, type AudioLevels, type ChatMessage, type ConversationStatus, type UseConversationReturn, useAgentMessages, useAgentSessionContext, useAudioLevels, useConversation, useOptionalAgentSessionContext };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { SessionStatus, AgentSession, AgentMode, AgentSessionOptions } from '@fishaudio/agent-client';
|
|
2
|
+
export { AgentMode, AgentSession, AgentSessionOptions, ConversationMessage, EndReason, FishAgentError, SessionStatus, SessionToken, TranscriptSegment } from '@fishaudio/agent-client';
|
|
3
|
+
import { PropsWithChildren, ReactElement } from 'react';
|
|
4
|
+
|
|
5
|
+
/** "idle" = no session yet (or a failed start); the rest mirror the session. */
|
|
6
|
+
type ConversationStatus = SessionStatus | "idle";
|
|
7
|
+
interface UseConversationReturn {
|
|
8
|
+
/** Live session object for event access; null before the first start. */
|
|
9
|
+
session: AgentSession | null;
|
|
10
|
+
status: ConversationStatus;
|
|
11
|
+
mode: AgentMode;
|
|
12
|
+
isSpeaking: boolean;
|
|
13
|
+
micMuted: boolean;
|
|
14
|
+
startSession: (overrides?: Partial<AgentSessionOptions>) => Promise<string>;
|
|
15
|
+
endSession: () => Promise<void>;
|
|
16
|
+
setMicMuted: (muted: boolean) => Promise<void>;
|
|
17
|
+
sendUserMessage: (text: string, options?: {
|
|
18
|
+
audio?: boolean;
|
|
19
|
+
}) => void;
|
|
20
|
+
sendUserActivity: () => void;
|
|
21
|
+
interrupt: () => void;
|
|
22
|
+
startAudio: () => Promise<void>;
|
|
23
|
+
setOutputVolume: (volume: number) => void;
|
|
24
|
+
setInputDevice: (deviceId: string) => Promise<void>;
|
|
25
|
+
setOutputDevice: (deviceId: string) => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
declare function useConversation(defaults?: Partial<AgentSessionOptions>): UseConversationReturn;
|
|
28
|
+
|
|
29
|
+
type AgentSessionProviderProps = PropsWithChildren<{
|
|
30
|
+
options?: Partial<AgentSessionOptions>;
|
|
31
|
+
}>;
|
|
32
|
+
/** Hosts one conversation for a component tree. */
|
|
33
|
+
declare function AgentSessionProvider({ options, children, }: AgentSessionProviderProps): ReactElement;
|
|
34
|
+
declare function useAgentSessionContext(): UseConversationReturn;
|
|
35
|
+
/** Nullable variant for components that also accept an explicit session prop. */
|
|
36
|
+
declare function useOptionalAgentSessionContext(): UseConversationReturn | null;
|
|
37
|
+
|
|
38
|
+
interface ChatMessage {
|
|
39
|
+
/** Stable per-segment key for list rendering. */
|
|
40
|
+
key: string;
|
|
41
|
+
role: "user" | "agent";
|
|
42
|
+
text: string;
|
|
43
|
+
/** False while the segment is still streaming (interim transcript / deltas). */
|
|
44
|
+
final: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Streaming chat log: user segments update in place as transcription refines,
|
|
48
|
+
* agent segments grow with each delta and pin on completion. Pass a session or
|
|
49
|
+
* rely on the surrounding <AgentSessionProvider>.
|
|
50
|
+
*/
|
|
51
|
+
declare function useAgentMessages(session?: AgentSession | null): ChatMessage[];
|
|
52
|
+
|
|
53
|
+
interface AudioLevels {
|
|
54
|
+
/** Microphone RMS in [0, 1]. */
|
|
55
|
+
input: number;
|
|
56
|
+
/** Agent audio RMS in [0, 1]. */
|
|
57
|
+
output: number;
|
|
58
|
+
}
|
|
59
|
+
/** Polls session audio levels for meters/animations. */
|
|
60
|
+
declare function useAudioLevels(session?: AgentSession | null, fps?: number): AudioLevels;
|
|
61
|
+
|
|
62
|
+
interface AgentAudioVisualizerProps {
|
|
63
|
+
session?: AgentSession | null;
|
|
64
|
+
bars?: number;
|
|
65
|
+
width?: number;
|
|
66
|
+
height?: number;
|
|
67
|
+
className?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Frequency bars for the agent's voice, drawn from the session's analyser data
|
|
71
|
+
* (no transport objects involved). Colors follow CSS `color` on the canvas.
|
|
72
|
+
*/
|
|
73
|
+
declare function AgentAudioVisualizer({ session, bars, width, height, className, }: AgentAudioVisualizerProps): ReactElement;
|
|
74
|
+
|
|
75
|
+
export { AgentAudioVisualizer, type AgentAudioVisualizerProps, AgentSessionProvider, type AgentSessionProviderProps, type AudioLevels, type ChatMessage, type ConversationStatus, type UseConversationReturn, useAgentMessages, useAgentSessionContext, useAudioLevels, useConversation, useOptionalAgentSessionContext };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
// src/useConversation.ts
|
|
2
|
+
import {
|
|
3
|
+
AgentSession
|
|
4
|
+
} from "@fishaudio/agent-client";
|
|
5
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
6
|
+
function startCancelledError() {
|
|
7
|
+
const error = new Error("Conversation start was cancelled by endSession");
|
|
8
|
+
error.name = "AbortError";
|
|
9
|
+
return error;
|
|
10
|
+
}
|
|
11
|
+
function useConversation(defaults = {}) {
|
|
12
|
+
const defaultsRef = useRef(defaults);
|
|
13
|
+
defaultsRef.current = defaults;
|
|
14
|
+
const sessionRef = useRef(null);
|
|
15
|
+
const startingRef = useRef(null);
|
|
16
|
+
const abandonedSessionRef = useRef(null);
|
|
17
|
+
const lifecycleEpochRef = useRef(0);
|
|
18
|
+
const mountedRef = useRef(true);
|
|
19
|
+
const [session, setSession] = useState(null);
|
|
20
|
+
const [status, setStatus] = useState("idle");
|
|
21
|
+
const [mode, setMode] = useState("listening");
|
|
22
|
+
const [micMuted, setMicMutedState] = useState(false);
|
|
23
|
+
const releaseAbandonedSession = useCallback(async () => {
|
|
24
|
+
const abandoned = abandonedSessionRef.current;
|
|
25
|
+
if (!abandoned) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
await abandoned.end();
|
|
29
|
+
if (abandonedSessionRef.current === abandoned) {
|
|
30
|
+
abandonedSessionRef.current = null;
|
|
31
|
+
}
|
|
32
|
+
}, []);
|
|
33
|
+
const startSession = useCallback(
|
|
34
|
+
async (overrides = {}) => {
|
|
35
|
+
const requestEpoch = lifecycleEpochRef.current;
|
|
36
|
+
const startAtEpoch = async () => {
|
|
37
|
+
if (!mountedRef.current) {
|
|
38
|
+
throw new Error("Cannot start a conversation after useConversation has unmounted");
|
|
39
|
+
}
|
|
40
|
+
if (requestEpoch !== lifecycleEpochRef.current) {
|
|
41
|
+
throw startCancelledError();
|
|
42
|
+
}
|
|
43
|
+
if (abandonedSessionRef.current) {
|
|
44
|
+
await releaseAbandonedSession();
|
|
45
|
+
if (requestEpoch !== lifecycleEpochRef.current) {
|
|
46
|
+
throw startCancelledError();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const current = sessionRef.current;
|
|
50
|
+
if (current) {
|
|
51
|
+
if (current.status !== "ended") {
|
|
52
|
+
return current.sessionId;
|
|
53
|
+
}
|
|
54
|
+
await current.end();
|
|
55
|
+
if (requestEpoch !== lifecycleEpochRef.current) {
|
|
56
|
+
throw startCancelledError();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const existing = startingRef.current;
|
|
60
|
+
if (existing) {
|
|
61
|
+
if (!existing.state.cancelled) {
|
|
62
|
+
return existing.promise;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await existing.promise;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (existing.state.sessionCreated) {
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (requestEpoch !== lifecycleEpochRef.current) {
|
|
72
|
+
throw startCancelledError();
|
|
73
|
+
}
|
|
74
|
+
return startAtEpoch();
|
|
75
|
+
}
|
|
76
|
+
setStatus("connecting");
|
|
77
|
+
const state = { cancelled: false, sessionCreated: false };
|
|
78
|
+
const starting = (async () => {
|
|
79
|
+
try {
|
|
80
|
+
const next = await AgentSession.start({
|
|
81
|
+
...defaultsRef.current,
|
|
82
|
+
...overrides
|
|
83
|
+
});
|
|
84
|
+
state.sessionCreated = true;
|
|
85
|
+
if (state.cancelled || requestEpoch !== lifecycleEpochRef.current || !mountedRef.current) {
|
|
86
|
+
try {
|
|
87
|
+
await next.end();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
abandonedSessionRef.current = next;
|
|
90
|
+
if (!mountedRef.current) {
|
|
91
|
+
void releaseAbandonedSession().catch(() => void 0);
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
return next.sessionId;
|
|
96
|
+
}
|
|
97
|
+
sessionRef.current = next;
|
|
98
|
+
setSession(next);
|
|
99
|
+
setMicMutedState(next.micMuted);
|
|
100
|
+
setMode(next.mode);
|
|
101
|
+
setStatus(next.status);
|
|
102
|
+
next.on("statusChange", setStatus);
|
|
103
|
+
next.on("modeChange", setMode);
|
|
104
|
+
return next.sessionId;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (!state.cancelled && mountedRef.current) {
|
|
107
|
+
setStatus("idle");
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
} finally {
|
|
111
|
+
if (startingRef.current?.state === state) {
|
|
112
|
+
startingRef.current = null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
})();
|
|
116
|
+
const pending = { state, promise: starting };
|
|
117
|
+
startingRef.current = pending;
|
|
118
|
+
return starting;
|
|
119
|
+
};
|
|
120
|
+
return startAtEpoch();
|
|
121
|
+
},
|
|
122
|
+
[releaseAbandonedSession]
|
|
123
|
+
);
|
|
124
|
+
const endSession = useCallback(async () => {
|
|
125
|
+
lifecycleEpochRef.current += 1;
|
|
126
|
+
const pending = startingRef.current;
|
|
127
|
+
const current = sessionRef.current;
|
|
128
|
+
const abandoned = abandonedSessionRef.current;
|
|
129
|
+
if (!pending && !abandoned && !current) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (pending) {
|
|
133
|
+
pending.state.cancelled = true;
|
|
134
|
+
if (mountedRef.current) {
|
|
135
|
+
setStatus("ended");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
await current?.end();
|
|
139
|
+
if (abandoned) {
|
|
140
|
+
await releaseAbandonedSession();
|
|
141
|
+
}
|
|
142
|
+
if (pending) {
|
|
143
|
+
try {
|
|
144
|
+
await pending.promise;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (pending.state.sessionCreated) {
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}, [releaseAbandonedSession]);
|
|
152
|
+
const setMicMuted = useCallback(async (muted) => {
|
|
153
|
+
await sessionRef.current?.setMicMuted(muted);
|
|
154
|
+
setMicMutedState(muted);
|
|
155
|
+
}, []);
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
mountedRef.current = true;
|
|
158
|
+
return () => {
|
|
159
|
+
mountedRef.current = false;
|
|
160
|
+
lifecycleEpochRef.current += 1;
|
|
161
|
+
const pending = startingRef.current;
|
|
162
|
+
if (pending) {
|
|
163
|
+
pending.state.cancelled = true;
|
|
164
|
+
}
|
|
165
|
+
const current = sessionRef.current;
|
|
166
|
+
if (current) {
|
|
167
|
+
current.off("statusChange", setStatus);
|
|
168
|
+
current.off("modeChange", setMode);
|
|
169
|
+
void current.end().catch(() => void 0);
|
|
170
|
+
}
|
|
171
|
+
void releaseAbandonedSession().catch(() => void 0);
|
|
172
|
+
};
|
|
173
|
+
}, [releaseAbandonedSession]);
|
|
174
|
+
return {
|
|
175
|
+
session,
|
|
176
|
+
status,
|
|
177
|
+
mode,
|
|
178
|
+
isSpeaking: mode === "speaking",
|
|
179
|
+
micMuted,
|
|
180
|
+
startSession,
|
|
181
|
+
endSession,
|
|
182
|
+
setMicMuted,
|
|
183
|
+
sendUserMessage: useCallback(
|
|
184
|
+
(text, options) => sessionRef.current?.sendUserMessage(text, options),
|
|
185
|
+
[]
|
|
186
|
+
),
|
|
187
|
+
sendUserActivity: useCallback(() => sessionRef.current?.sendUserActivity(), []),
|
|
188
|
+
interrupt: useCallback(() => sessionRef.current?.interrupt(), []),
|
|
189
|
+
startAudio: useCallback(async () => sessionRef.current?.startAudio(), []),
|
|
190
|
+
setOutputVolume: useCallback(
|
|
191
|
+
(volume) => sessionRef.current?.setOutputVolume(volume),
|
|
192
|
+
[]
|
|
193
|
+
),
|
|
194
|
+
setInputDevice: useCallback(
|
|
195
|
+
async (deviceId) => sessionRef.current?.setInputDevice(deviceId),
|
|
196
|
+
[]
|
|
197
|
+
),
|
|
198
|
+
setOutputDevice: useCallback(
|
|
199
|
+
async (deviceId) => sessionRef.current?.setOutputDevice(deviceId),
|
|
200
|
+
[]
|
|
201
|
+
)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/provider.tsx
|
|
206
|
+
import { createContext, useContext } from "react";
|
|
207
|
+
import { jsx } from "react/jsx-runtime";
|
|
208
|
+
var ConversationContext = createContext(null);
|
|
209
|
+
function AgentSessionProvider({
|
|
210
|
+
options,
|
|
211
|
+
children
|
|
212
|
+
}) {
|
|
213
|
+
const conversation = useConversation(options ?? {});
|
|
214
|
+
return /* @__PURE__ */ jsx(ConversationContext.Provider, { value: conversation, children });
|
|
215
|
+
}
|
|
216
|
+
function useAgentSessionContext() {
|
|
217
|
+
const context = useContext(ConversationContext);
|
|
218
|
+
if (!context) {
|
|
219
|
+
throw new Error("useAgentSessionContext must be used within <AgentSessionProvider>");
|
|
220
|
+
}
|
|
221
|
+
return context;
|
|
222
|
+
}
|
|
223
|
+
function useOptionalAgentSessionContext() {
|
|
224
|
+
return useContext(ConversationContext);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/useAgentMessages.ts
|
|
228
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
229
|
+
function useAgentMessages(session) {
|
|
230
|
+
const context = useOptionalAgentSessionContext();
|
|
231
|
+
const active = session !== void 0 ? session : context?.session ?? null;
|
|
232
|
+
const [messages, setMessages] = useState2([]);
|
|
233
|
+
useEffect2(() => {
|
|
234
|
+
if (!active) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
setMessages(
|
|
238
|
+
active.getTranscript().map((segment) => ({
|
|
239
|
+
key: `${segment.role}-${segment.segmentId}`,
|
|
240
|
+
role: segment.role,
|
|
241
|
+
text: segment.text,
|
|
242
|
+
final: segment.final
|
|
243
|
+
}))
|
|
244
|
+
);
|
|
245
|
+
const upsert = (key, role, text, final) => {
|
|
246
|
+
setMessages((previous) => {
|
|
247
|
+
const index = previous.findIndex((message) => message.key === key);
|
|
248
|
+
const entry = { key, role, text, final };
|
|
249
|
+
if (index === -1) {
|
|
250
|
+
return [...previous, entry];
|
|
251
|
+
}
|
|
252
|
+
const next = [...previous];
|
|
253
|
+
next[index] = entry;
|
|
254
|
+
return next;
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
const onUserTranscript = (event) => upsert(`user-${event.segmentId}`, "user", event.text, event.final);
|
|
258
|
+
const onAgentDelta = (event) => upsert(`agent-${event.segmentId}`, "agent", event.text, false);
|
|
259
|
+
const onAgentResponse = (event) => upsert(`agent-${event.segmentId}`, "agent", event.text, true);
|
|
260
|
+
active.on("userTranscript", onUserTranscript);
|
|
261
|
+
active.on("agentResponseDelta", onAgentDelta);
|
|
262
|
+
active.on("agentResponse", onAgentResponse);
|
|
263
|
+
return () => {
|
|
264
|
+
active.off("userTranscript", onUserTranscript);
|
|
265
|
+
active.off("agentResponseDelta", onAgentDelta);
|
|
266
|
+
active.off("agentResponse", onAgentResponse);
|
|
267
|
+
};
|
|
268
|
+
}, [active]);
|
|
269
|
+
return messages;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/useAudioLevels.ts
|
|
273
|
+
import { useEffect as useEffect3, useState as useState3 } from "react";
|
|
274
|
+
function useAudioLevels(session, fps = 20) {
|
|
275
|
+
const context = useOptionalAgentSessionContext();
|
|
276
|
+
const active = session !== void 0 ? session : context?.session ?? null;
|
|
277
|
+
const [levels, setLevels] = useState3({ input: 0, output: 0 });
|
|
278
|
+
useEffect3(() => {
|
|
279
|
+
if (!active) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const timer = setInterval(
|
|
283
|
+
() => {
|
|
284
|
+
setLevels({ input: active.getInputVolume(), output: active.getOutputVolume() });
|
|
285
|
+
},
|
|
286
|
+
Math.max(1e3 / fps, 16)
|
|
287
|
+
);
|
|
288
|
+
return () => {
|
|
289
|
+
clearInterval(timer);
|
|
290
|
+
};
|
|
291
|
+
}, [active, fps]);
|
|
292
|
+
return levels;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/AgentAudioVisualizer.tsx
|
|
296
|
+
import { useEffect as useEffect4, useRef as useRef2 } from "react";
|
|
297
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
298
|
+
function AgentAudioVisualizer({
|
|
299
|
+
session,
|
|
300
|
+
bars = 24,
|
|
301
|
+
width = 240,
|
|
302
|
+
height = 64,
|
|
303
|
+
className
|
|
304
|
+
}) {
|
|
305
|
+
const context = useOptionalAgentSessionContext();
|
|
306
|
+
const active = session !== void 0 ? session : context?.session ?? null;
|
|
307
|
+
const canvasRef = useRef2(null);
|
|
308
|
+
useEffect4(() => {
|
|
309
|
+
const canvas = canvasRef.current;
|
|
310
|
+
if (!canvas || !active) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const context2d = canvas.getContext("2d");
|
|
314
|
+
if (!context2d) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
let frame = 0;
|
|
318
|
+
const draw = () => {
|
|
319
|
+
const data = active.getOutputFrequencyData();
|
|
320
|
+
context2d.clearRect(0, 0, canvas.width, canvas.height);
|
|
321
|
+
context2d.fillStyle = getComputedStyle(canvas).color;
|
|
322
|
+
const barWidth = canvas.width / bars;
|
|
323
|
+
for (let index = 0; index < bars; index += 1) {
|
|
324
|
+
const sample = data.length ? (data[Math.floor(index / bars * data.length)] ?? 0) / 255 : 0;
|
|
325
|
+
const barHeight = Math.max(2, sample * canvas.height);
|
|
326
|
+
context2d.fillRect(
|
|
327
|
+
index * barWidth + barWidth * 0.15,
|
|
328
|
+
(canvas.height - barHeight) / 2,
|
|
329
|
+
barWidth * 0.7,
|
|
330
|
+
barHeight
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
frame = requestAnimationFrame(draw);
|
|
334
|
+
};
|
|
335
|
+
frame = requestAnimationFrame(draw);
|
|
336
|
+
return () => {
|
|
337
|
+
cancelAnimationFrame(frame);
|
|
338
|
+
};
|
|
339
|
+
}, [active, bars]);
|
|
340
|
+
return /* @__PURE__ */ jsx2("canvas", { ref: canvasRef, width, height, className });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/index.ts
|
|
344
|
+
import {
|
|
345
|
+
AgentSession as AgentSession2,
|
|
346
|
+
FishAgentError
|
|
347
|
+
} from "@fishaudio/agent-client";
|
|
348
|
+
export {
|
|
349
|
+
AgentAudioVisualizer,
|
|
350
|
+
AgentSession2 as AgentSession,
|
|
351
|
+
AgentSessionProvider,
|
|
352
|
+
FishAgentError,
|
|
353
|
+
useAgentMessages,
|
|
354
|
+
useAgentSessionContext,
|
|
355
|
+
useAudioLevels,
|
|
356
|
+
useConversation,
|
|
357
|
+
useOptionalAgentSessionContext
|
|
358
|
+
};
|
|
359
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useConversation.ts","../src/provider.tsx","../src/useAgentMessages.ts","../src/useAudioLevels.ts","../src/AgentAudioVisualizer.tsx","../src/index.ts"],"sourcesContent":["import {\n AgentSession,\n type AgentMode,\n type AgentSessionOptions,\n type SessionStatus,\n} from \"@fishaudio/agent-client\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\n/** \"idle\" = no session yet (or a failed start); the rest mirror the session. */\nexport type ConversationStatus = SessionStatus | \"idle\";\n\nexport interface UseConversationReturn {\n /** Live session object for event access; null before the first start. */\n session: AgentSession | null;\n status: ConversationStatus;\n mode: AgentMode;\n isSpeaking: boolean;\n micMuted: boolean;\n startSession: (overrides?: Partial<AgentSessionOptions>) => Promise<string>;\n endSession: () => Promise<void>;\n setMicMuted: (muted: boolean) => Promise<void>;\n sendUserMessage: (text: string, options?: { audio?: boolean }) => void;\n sendUserActivity: () => void;\n interrupt: () => void;\n startAudio: () => Promise<void>;\n setOutputVolume: (volume: number) => void;\n setInputDevice: (deviceId: string) => Promise<void>;\n setOutputDevice: (deviceId: string) => Promise<void>;\n}\n\ninterface PendingStart {\n state: { cancelled: boolean; sessionCreated: boolean };\n promise: Promise<string>;\n}\n\nfunction startCancelledError(): Error {\n const error = new Error(\"Conversation start was cancelled by endSession\");\n error.name = \"AbortError\";\n return error;\n}\n\nexport function useConversation(\n defaults: Partial<AgentSessionOptions> = {},\n): UseConversationReturn {\n const defaultsRef = useRef(defaults);\n defaultsRef.current = defaults;\n\n const sessionRef = useRef<AgentSession | null>(null);\n const startingRef = useRef<PendingStart | null>(null);\n const abandonedSessionRef = useRef<AgentSession | null>(null);\n const lifecycleEpochRef = useRef(0);\n const mountedRef = useRef(true);\n const [session, setSession] = useState<AgentSession | null>(null);\n const [status, setStatus] = useState<ConversationStatus>(\"idle\");\n const [mode, setMode] = useState<AgentMode>(\"listening\");\n const [micMuted, setMicMutedState] = useState(false);\n\n const releaseAbandonedSession = useCallback(async () => {\n const abandoned = abandonedSessionRef.current;\n if (!abandoned) {\n return;\n }\n await abandoned.end();\n if (abandonedSessionRef.current === abandoned) {\n abandonedSessionRef.current = null;\n }\n }, []);\n\n const startSession = useCallback(\n async (overrides: Partial<AgentSessionOptions> = {}): Promise<string> => {\n const requestEpoch = lifecycleEpochRef.current;\n\n const startAtEpoch = async (): Promise<string> => {\n if (!mountedRef.current) {\n throw new Error(\"Cannot start a conversation after useConversation has unmounted\");\n }\n if (requestEpoch !== lifecycleEpochRef.current) {\n throw startCancelledError();\n }\n\n if (abandonedSessionRef.current) {\n await releaseAbandonedSession();\n if (requestEpoch !== lifecycleEpochRef.current) {\n throw startCancelledError();\n }\n }\n\n const current = sessionRef.current;\n if (current) {\n if (current.status !== \"ended\") {\n return current.sessionId;\n }\n // status can reach ended before an in-flight transport teardown\n // promise settles. AgentSession.end() joins that promise, so a fresh\n // session never overlaps the old connection's cleanup.\n await current.end();\n if (requestEpoch !== lifecycleEpochRef.current) {\n throw startCancelledError();\n }\n }\n\n // Double-click guard: a second call while a start is in flight joins\n // the first. A call queued after hangup waits for the cancelled start\n // to release its resources before it creates a fresh session.\n const existing = startingRef.current;\n if (existing) {\n if (!existing.state.cancelled) {\n return existing.promise;\n }\n try {\n await existing.promise;\n } catch (error) {\n // A failed session request leaves nothing to clean up, so a queued\n // restart may continue. A teardown failure means the old session may\n // still own resources and must stop the restart.\n if (existing.state.sessionCreated) {\n throw error;\n }\n }\n if (requestEpoch !== lifecycleEpochRef.current) {\n throw startCancelledError();\n }\n return startAtEpoch();\n }\n\n setStatus(\"connecting\");\n const state = { cancelled: false, sessionCreated: false };\n const starting = (async () => {\n try {\n const next = await AgentSession.start({\n ...defaultsRef.current,\n ...overrides,\n } as AgentSessionOptions);\n state.sessionCreated = true;\n // endSession or a real unmount may happen while permissions/session\n // creation are still pending. Never publish that late session into\n // the hook; close it before it can leak its microphone or connection.\n if (\n state.cancelled ||\n requestEpoch !== lifecycleEpochRef.current ||\n !mountedRef.current\n ) {\n try {\n await next.end();\n } catch (error) {\n abandonedSessionRef.current = next;\n if (!mountedRef.current) {\n void releaseAbandonedSession().catch(() => undefined);\n }\n throw error;\n }\n return next.sessionId;\n }\n sessionRef.current = next;\n setSession(next);\n // Mirror, don't assume: a microphone:false start joins muted.\n setMicMutedState(next.micMuted);\n setMode(next.mode);\n setStatus(next.status);\n next.on(\"statusChange\", setStatus);\n next.on(\"modeChange\", setMode);\n return next.sessionId;\n } catch (error) {\n if (!state.cancelled && mountedRef.current) {\n setStatus(\"idle\");\n }\n throw error;\n } finally {\n if (startingRef.current?.state === state) {\n startingRef.current = null;\n }\n }\n })();\n const pending: PendingStart = { state, promise: starting };\n startingRef.current = pending;\n return starting;\n };\n\n return startAtEpoch();\n },\n [releaseAbandonedSession],\n );\n\n const endSession = useCallback(async () => {\n lifecycleEpochRef.current += 1;\n const pending = startingRef.current;\n const current = sessionRef.current;\n const abandoned = abandonedSessionRef.current;\n if (!pending && !abandoned && !current) {\n return;\n }\n\n if (pending) {\n pending.state.cancelled = true;\n if (mountedRef.current) {\n setStatus(\"ended\");\n }\n }\n await current?.end();\n if (abandoned) {\n await releaseAbandonedSession();\n }\n if (pending) {\n try {\n await pending.promise;\n } catch (error) {\n // The original startSession caller still receives a start failure. From\n // endSession's perspective, a failed request means there is nothing to\n // release; a failure after creation is a teardown failure and propagates.\n if (pending.state.sessionCreated) {\n throw error;\n }\n }\n }\n }, [releaseAbandonedSession]);\n\n const setMicMuted = useCallback(async (muted: boolean) => {\n await sessionRef.current?.setMicMuted(muted);\n setMicMutedState(muted);\n }, []);\n\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n lifecycleEpochRef.current += 1;\n const pending = startingRef.current;\n if (pending) {\n pending.state.cancelled = true;\n }\n const current = sessionRef.current;\n if (current) {\n current.off(\"statusChange\", setStatus);\n current.off(\"modeChange\", setMode);\n void current.end().catch(() => undefined);\n }\n void releaseAbandonedSession().catch(() => undefined);\n };\n }, [releaseAbandonedSession]);\n\n return {\n session,\n status,\n mode,\n isSpeaking: mode === \"speaking\",\n micMuted,\n startSession,\n endSession,\n setMicMuted,\n sendUserMessage: useCallback(\n (text: string, options?: { audio?: boolean }) =>\n sessionRef.current?.sendUserMessage(text, options),\n [],\n ),\n sendUserActivity: useCallback(() => sessionRef.current?.sendUserActivity(), []),\n interrupt: useCallback(() => sessionRef.current?.interrupt(), []),\n startAudio: useCallback(async () => sessionRef.current?.startAudio(), []),\n setOutputVolume: useCallback(\n (volume: number) => sessionRef.current?.setOutputVolume(volume),\n [],\n ),\n setInputDevice: useCallback(\n async (deviceId: string) => sessionRef.current?.setInputDevice(deviceId),\n [],\n ),\n setOutputDevice: useCallback(\n async (deviceId: string) => sessionRef.current?.setOutputDevice(deviceId),\n [],\n ),\n };\n}\n","import type { AgentSessionOptions } from \"@fishaudio/agent-client\";\nimport { createContext, useContext, type PropsWithChildren, type ReactElement } from \"react\";\nimport { useConversation, type UseConversationReturn } from \"./useConversation.js\";\n\nconst ConversationContext = createContext<UseConversationReturn | null>(null);\n\nexport type AgentSessionProviderProps = PropsWithChildren<{\n options?: Partial<AgentSessionOptions>;\n}>;\n\n/** Hosts one conversation for a component tree. */\nexport function AgentSessionProvider({\n options,\n children,\n}: AgentSessionProviderProps): ReactElement {\n const conversation = useConversation(options ?? {});\n return (\n <ConversationContext.Provider value={conversation}>{children}</ConversationContext.Provider>\n );\n}\n\nexport function useAgentSessionContext(): UseConversationReturn {\n const context = useContext(ConversationContext);\n if (!context) {\n throw new Error(\"useAgentSessionContext must be used within <AgentSessionProvider>\");\n }\n return context;\n}\n\n/** Nullable variant for components that also accept an explicit session prop. */\nexport function useOptionalAgentSessionContext(): UseConversationReturn | null {\n return useContext(ConversationContext);\n}\n","import type { AgentSession } from \"@fishaudio/agent-client\";\nimport { useEffect, useState } from \"react\";\nimport { useOptionalAgentSessionContext } from \"./provider.js\";\n\nexport interface ChatMessage {\n /** Stable per-segment key for list rendering. */\n key: string;\n role: \"user\" | \"agent\";\n text: string;\n /** False while the segment is still streaming (interim transcript / deltas). */\n final: boolean;\n}\n\n/**\n * Streaming chat log: user segments update in place as transcription refines,\n * agent segments grow with each delta and pin on completion. Pass a session or\n * rely on the surrounding <AgentSessionProvider>.\n */\nexport function useAgentMessages(session?: AgentSession | null): ChatMessage[] {\n const context = useOptionalAgentSessionContext();\n const active = session !== undefined ? session : (context?.session ?? null);\n const [messages, setMessages] = useState<ChatMessage[]>([]);\n\n useEffect(() => {\n if (!active) {\n return;\n }\n // Backfill events emitted before this effect ran (React commits the session\n // after startSession resolves); live events then update the same keys.\n setMessages(\n active.getTranscript().map((segment) => ({\n key: `${segment.role}-${segment.segmentId}`,\n role: segment.role,\n text: segment.text,\n final: segment.final,\n })),\n );\n\n const upsert = (key: string, role: \"user\" | \"agent\", text: string, final: boolean) => {\n setMessages((previous) => {\n const index = previous.findIndex((message) => message.key === key);\n const entry: ChatMessage = { key, role, text, final };\n if (index === -1) {\n return [...previous, entry];\n }\n const next = [...previous];\n next[index] = entry;\n return next;\n });\n };\n\n const onUserTranscript = (event: { segmentId: string; text: string; final: boolean }) =>\n upsert(`user-${event.segmentId}`, \"user\", event.text, event.final);\n const onAgentDelta = (event: { segmentId: string; text: string }) =>\n upsert(`agent-${event.segmentId}`, \"agent\", event.text, false);\n const onAgentResponse = (event: { segmentId: string; text: string }) =>\n upsert(`agent-${event.segmentId}`, \"agent\", event.text, true);\n\n active.on(\"userTranscript\", onUserTranscript);\n active.on(\"agentResponseDelta\", onAgentDelta);\n active.on(\"agentResponse\", onAgentResponse);\n return () => {\n active.off(\"userTranscript\", onUserTranscript);\n active.off(\"agentResponseDelta\", onAgentDelta);\n active.off(\"agentResponse\", onAgentResponse);\n };\n }, [active]);\n\n return messages;\n}\n","import type { AgentSession } from \"@fishaudio/agent-client\";\nimport { useEffect, useState } from \"react\";\nimport { useOptionalAgentSessionContext } from \"./provider.js\";\n\nexport interface AudioLevels {\n /** Microphone RMS in [0, 1]. */\n input: number;\n /** Agent audio RMS in [0, 1]. */\n output: number;\n}\n\n/** Polls session audio levels for meters/animations. */\nexport function useAudioLevels(session?: AgentSession | null, fps = 20): AudioLevels {\n const context = useOptionalAgentSessionContext();\n const active = session !== undefined ? session : (context?.session ?? null);\n const [levels, setLevels] = useState<AudioLevels>({ input: 0, output: 0 });\n\n useEffect(() => {\n if (!active) {\n return;\n }\n const timer = setInterval(\n () => {\n setLevels({ input: active.getInputVolume(), output: active.getOutputVolume() });\n },\n Math.max(1000 / fps, 16),\n );\n return () => {\n clearInterval(timer);\n };\n }, [active, fps]);\n\n return levels;\n}\n","import type { AgentSession } from \"@fishaudio/agent-client\";\nimport { useEffect, useRef, type ReactElement } from \"react\";\nimport { useOptionalAgentSessionContext } from \"./provider.js\";\n\nexport interface AgentAudioVisualizerProps {\n session?: AgentSession | null;\n bars?: number;\n width?: number;\n height?: number;\n className?: string;\n}\n\n/**\n * Frequency bars for the agent's voice, drawn from the session's analyser data\n * (no transport objects involved). Colors follow CSS `color` on the canvas.\n */\nexport function AgentAudioVisualizer({\n session,\n bars = 24,\n width = 240,\n height = 64,\n className,\n}: AgentAudioVisualizerProps): ReactElement {\n const context = useOptionalAgentSessionContext();\n const active = session !== undefined ? session : (context?.session ?? null);\n const canvasRef = useRef<HTMLCanvasElement | null>(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas || !active) {\n return;\n }\n const context2d = canvas.getContext(\"2d\");\n if (!context2d) {\n return;\n }\n\n let frame = 0;\n const draw = () => {\n const data = active.getOutputFrequencyData();\n context2d.clearRect(0, 0, canvas.width, canvas.height);\n context2d.fillStyle = getComputedStyle(canvas).color;\n const barWidth = canvas.width / bars;\n for (let index = 0; index < bars; index += 1) {\n const sample = data.length\n ? (data[Math.floor((index / bars) * data.length)] ?? 0) / 255\n : 0;\n const barHeight = Math.max(2, sample * canvas.height);\n context2d.fillRect(\n index * barWidth + barWidth * 0.15,\n (canvas.height - barHeight) / 2,\n barWidth * 0.7,\n barHeight,\n );\n }\n frame = requestAnimationFrame(draw);\n };\n frame = requestAnimationFrame(draw);\n return () => {\n cancelAnimationFrame(frame);\n };\n }, [active, bars]);\n\n return <canvas ref={canvasRef} width={width} height={height} className={className} />;\n}\n","export { useConversation, type ConversationStatus, type UseConversationReturn } from \"./useConversation.js\";\nexport {\n AgentSessionProvider,\n useAgentSessionContext,\n useOptionalAgentSessionContext,\n type AgentSessionProviderProps,\n} from \"./provider.js\";\nexport { useAgentMessages, type ChatMessage } from \"./useAgentMessages.js\";\nexport { useAudioLevels, type AudioLevels } from \"./useAudioLevels.js\";\nexport {\n AgentAudioVisualizer,\n type AgentAudioVisualizerProps,\n} from \"./AgentAudioVisualizer.js\";\n// Convenience re-exports so app code can stay on one import.\nexport {\n AgentSession,\n FishAgentError,\n type AgentMode,\n type AgentSessionOptions,\n type ConversationMessage,\n type EndReason,\n type SessionStatus,\n type SessionToken,\n type TranscriptSegment,\n} from \"@fishaudio/agent-client\";\n"],"mappings":"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","names":["useEffect","useState","useState","useEffect","useEffect","useState","useState","useEffect","useEffect","useRef","jsx","useRef","useEffect","AgentSession"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fishaudio/agent-react",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "React hooks and components for Fish Audio voice agents.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"fish-audio",
|
|
7
|
+
"voice",
|
|
8
|
+
"agent",
|
|
9
|
+
"react",
|
|
10
|
+
"hooks",
|
|
11
|
+
"webrtc"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/fishaudio/fish-agent-sdk-web.git",
|
|
17
|
+
"directory": "packages/react"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/fishaudio/fish-agent-sdk-web/tree/main/packages/react#readme",
|
|
20
|
+
"bugs": "https://github.com/fishaudio/fish-agent-sdk-web/issues",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"main": "./dist/index.cjs",
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"require": {
|
|
33
|
+
"types": "./dist/index.d.cts",
|
|
34
|
+
"default": "./dist/index.cjs"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"./package.json": "./package.json"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@fishaudio/agent-client": "^0.0.1"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"react": ">=18"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@testing-library/react": "^16.3.0",
|
|
57
|
+
"@types/react": "^19.1.0",
|
|
58
|
+
"jsdom": "^26.1.0",
|
|
59
|
+
"react": "^19.1.0",
|
|
60
|
+
"react-dom": "^19.1.0",
|
|
61
|
+
"tsup": "^8.5.0",
|
|
62
|
+
"typescript": "^5.9.3",
|
|
63
|
+
"vitest": "^4.0.5"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "tsup",
|
|
67
|
+
"test": "vitest run",
|
|
68
|
+
"typecheck": "tsc --noEmit"
|
|
69
|
+
}
|
|
70
|
+
}
|