@rimori/react-client 0.3.0-next.7 → 0.3.0-next.8
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/ai/Assistant.js +6 -0
- package/dist/components/ai/Avatar.js +6 -0
- package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.d.ts +1 -0
- package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.js +3 -0
- package/dist/components/ai/EmbeddedAssistent/TTS/Player.d.ts +1 -0
- package/dist/components/ai/EmbeddedAssistent/TTS/Player.js +10 -0
- package/dist/components/audio/Playbutton.js +52 -4
- package/package.json +3 -3
- package/src/components/ai/Assistant.tsx +7 -0
- package/src/components/ai/Avatar.tsx +7 -0
- package/src/components/ai/EmbeddedAssistent/TTS/MessageSender.ts +4 -0
- package/src/components/ai/EmbeddedAssistent/TTS/Player.ts +11 -0
- package/src/components/audio/Playbutton.tsx +58 -4
|
@@ -26,6 +26,12 @@ export function AssistantChat({ avatarImageUrl, voiceId, onComplete, autoStartCo
|
|
|
26
26
|
sender.handleNewText(autoStartConversation.assistantMessage, isLoading);
|
|
27
27
|
}
|
|
28
28
|
}, []);
|
|
29
|
+
// Cleanup: stop audio playback and clean up resources on unmount
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
return () => {
|
|
32
|
+
sender.cleanup();
|
|
33
|
+
};
|
|
34
|
+
}, [sender]);
|
|
29
35
|
useEffect(() => {
|
|
30
36
|
var _a;
|
|
31
37
|
let message = lastAssistantMessage;
|
|
@@ -38,6 +38,12 @@ export function Avatar({ avatarImageUrl, voiceId, agentTools, autoStartConversat
|
|
|
38
38
|
append([{ role: 'user', content: autoStartConversation.userMessage, id: messages.length.toString() }]);
|
|
39
39
|
}
|
|
40
40
|
}, [autoStartConversation, voiceId]);
|
|
41
|
+
// Cleanup: stop audio playback and clean up resources on unmount
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
return () => {
|
|
44
|
+
sender.cleanup();
|
|
45
|
+
};
|
|
46
|
+
}, [sender]);
|
|
41
47
|
useEffect(() => {
|
|
42
48
|
if ((lastMessage === null || lastMessage === void 0 ? void 0 : lastMessage.role) === 'assistant') {
|
|
43
49
|
sender.handleNewText(lastMessage.content, isLoading);
|
|
@@ -89,6 +89,16 @@ export class ChunkedAudioPlayer {
|
|
|
89
89
|
this.shouldMonitorLoudness = false;
|
|
90
90
|
cancelAnimationFrame(this.handle);
|
|
91
91
|
}
|
|
92
|
+
cleanup() {
|
|
93
|
+
// Stop playback first
|
|
94
|
+
this.stopPlayback();
|
|
95
|
+
// Close AudioContext to free resources
|
|
96
|
+
if (this.audioContext && this.audioContext.state !== 'closed') {
|
|
97
|
+
this.audioContext.close().catch((e) => {
|
|
98
|
+
console.warn('Error closing AudioContext:', e);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
92
102
|
playChunk(chunk) {
|
|
93
103
|
// console.log({queue: this.chunkQueue})
|
|
94
104
|
if (!chunk) {
|
|
@@ -8,7 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
|
-
import { useState, useEffect } from 'react';
|
|
11
|
+
import { useState, useEffect, useRef } from 'react';
|
|
12
12
|
import { FaPlayCircle, FaStopCircle } from 'react-icons/fa';
|
|
13
13
|
import { useRimori } from '../../providers/PluginProvider';
|
|
14
14
|
import { EventBus } from '@rimori/client';
|
|
@@ -20,6 +20,8 @@ export const AudioPlayer = ({ text, voice, language, hide, playListenerEvent, in
|
|
|
20
20
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
21
21
|
const [isLoading, setIsLoading] = useState(false);
|
|
22
22
|
const { ai } = useRimori();
|
|
23
|
+
const audioRef = useRef(null);
|
|
24
|
+
const eventBusListenerRef = useRef(null);
|
|
23
25
|
useEffect(() => {
|
|
24
26
|
if (audioUrl)
|
|
25
27
|
setAudioUrl(null);
|
|
@@ -37,9 +39,22 @@ export const AudioPlayer = ({ text, voice, language, hide, playListenerEvent, in
|
|
|
37
39
|
});
|
|
38
40
|
// Effect to play audio when audioUrl changes and play state is true
|
|
39
41
|
useEffect(() => {
|
|
40
|
-
if (!audioUrl || !isPlaying)
|
|
42
|
+
if (!audioUrl || !isPlaying) {
|
|
43
|
+
// Stop any existing audio when not playing
|
|
44
|
+
if (audioRef.current) {
|
|
45
|
+
audioRef.current.pause();
|
|
46
|
+
audioRef.current.currentTime = 0;
|
|
47
|
+
audioRef.current = null;
|
|
48
|
+
}
|
|
41
49
|
return;
|
|
50
|
+
}
|
|
51
|
+
// Clean up previous audio instance if it exists
|
|
52
|
+
if (audioRef.current) {
|
|
53
|
+
audioRef.current.pause();
|
|
54
|
+
audioRef.current.currentTime = 0;
|
|
55
|
+
}
|
|
42
56
|
const audio = new Audio(audioUrl);
|
|
57
|
+
audioRef.current = audio;
|
|
43
58
|
audio.playbackRate = speed;
|
|
44
59
|
audio
|
|
45
60
|
.play()
|
|
@@ -47,16 +62,41 @@ export const AudioPlayer = ({ text, voice, language, hide, playListenerEvent, in
|
|
|
47
62
|
audio.onended = () => {
|
|
48
63
|
setIsPlaying(false);
|
|
49
64
|
isFetchingAudio = false;
|
|
65
|
+
audioRef.current = null;
|
|
50
66
|
};
|
|
51
67
|
})
|
|
52
68
|
.catch((e) => {
|
|
53
69
|
console.warn('Error playing audio:', e);
|
|
54
70
|
setIsPlaying(false);
|
|
71
|
+
audioRef.current = null;
|
|
55
72
|
});
|
|
56
73
|
return () => {
|
|
57
|
-
|
|
74
|
+
if (audioRef.current) {
|
|
75
|
+
audioRef.current.pause();
|
|
76
|
+
audioRef.current.currentTime = 0;
|
|
77
|
+
audioRef.current = null;
|
|
78
|
+
}
|
|
58
79
|
};
|
|
59
80
|
}, [audioUrl, isPlaying, speed]);
|
|
81
|
+
// Cleanup on unmount - stop audio and revoke object URL
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
return () => {
|
|
84
|
+
if (audioRef.current) {
|
|
85
|
+
audioRef.current.pause();
|
|
86
|
+
audioRef.current.currentTime = 0;
|
|
87
|
+
audioRef.current = null;
|
|
88
|
+
}
|
|
89
|
+
setIsPlaying(false);
|
|
90
|
+
};
|
|
91
|
+
}, []);
|
|
92
|
+
// Cleanup audioUrl on unmount
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
return () => {
|
|
95
|
+
if (audioUrl) {
|
|
96
|
+
URL.revokeObjectURL(audioUrl);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}, [audioUrl]);
|
|
60
100
|
const togglePlayback = () => {
|
|
61
101
|
if (!isPlaying && !audioUrl) {
|
|
62
102
|
generateAudio().then(() => setIsPlaying(true));
|
|
@@ -68,7 +108,15 @@ export const AudioPlayer = ({ text, voice, language, hide, playListenerEvent, in
|
|
|
68
108
|
useEffect(() => {
|
|
69
109
|
if (!playListenerEvent)
|
|
70
110
|
return;
|
|
71
|
-
|
|
111
|
+
const handler = () => togglePlayback();
|
|
112
|
+
const listener = EventBus.on(playListenerEvent, handler);
|
|
113
|
+
eventBusListenerRef.current = listener;
|
|
114
|
+
return () => {
|
|
115
|
+
if (eventBusListenerRef.current) {
|
|
116
|
+
eventBusListenerRef.current.off();
|
|
117
|
+
eventBusListenerRef.current = null;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
72
120
|
}, [playListenerEvent]);
|
|
73
121
|
useEffect(() => {
|
|
74
122
|
if (!playOnMount || isFetchingAudio)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimori/react-client",
|
|
3
|
-
"version": "0.3.0-next.
|
|
3
|
+
"version": "0.3.0-next.8",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"format": "prettier --write ."
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"@rimori/client": "2.3.0-next.
|
|
26
|
+
"@rimori/client": "2.3.0-next.3",
|
|
27
27
|
"react": "^18.1.0",
|
|
28
28
|
"react-dom": "^18.1.0"
|
|
29
29
|
},
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@eslint/js": "^9.37.0",
|
|
37
|
-
"@rimori/client": "2.3.0-next.
|
|
37
|
+
"@rimori/client": "2.3.0-next.3",
|
|
38
38
|
"@types/react": "^18.3.21",
|
|
39
39
|
"eslint-config-prettier": "^10.1.8",
|
|
40
40
|
"eslint-plugin-prettier": "^5.5.4",
|
|
@@ -38,6 +38,13 @@ export function AssistantChat({ avatarImageUrl, voiceId, onComplete, autoStartCo
|
|
|
38
38
|
}
|
|
39
39
|
}, []);
|
|
40
40
|
|
|
41
|
+
// Cleanup: stop audio playback and clean up resources on unmount
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
return () => {
|
|
44
|
+
sender.cleanup();
|
|
45
|
+
};
|
|
46
|
+
}, [sender]);
|
|
47
|
+
|
|
41
48
|
useEffect(() => {
|
|
42
49
|
let message = lastAssistantMessage;
|
|
43
50
|
if (message !== messages[messages.length - 1]?.content) {
|
|
@@ -61,6 +61,13 @@ export function Avatar({
|
|
|
61
61
|
}
|
|
62
62
|
}, [autoStartConversation, voiceId]);
|
|
63
63
|
|
|
64
|
+
// Cleanup: stop audio playback and clean up resources on unmount
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
return () => {
|
|
67
|
+
sender.cleanup();
|
|
68
|
+
};
|
|
69
|
+
}, [sender]);
|
|
70
|
+
|
|
64
71
|
useEffect(() => {
|
|
65
72
|
if (lastMessage?.role === 'assistant') {
|
|
66
73
|
sender.handleNewText(lastMessage.content, isLoading);
|
|
@@ -88,6 +88,17 @@ export class ChunkedAudioPlayer {
|
|
|
88
88
|
cancelAnimationFrame(this.handle);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
public cleanup(): void {
|
|
92
|
+
// Stop playback first
|
|
93
|
+
this.stopPlayback();
|
|
94
|
+
// Close AudioContext to free resources
|
|
95
|
+
if (this.audioContext && this.audioContext.state !== 'closed') {
|
|
96
|
+
this.audioContext.close().catch((e) => {
|
|
97
|
+
console.warn('Error closing AudioContext:', e);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
91
102
|
private playChunk(chunk: ArrayBuffer): Promise<void> {
|
|
92
103
|
// console.log({queue: this.chunkQueue})
|
|
93
104
|
if (!chunk) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
2
|
import { FaPlayCircle, FaStopCircle } from 'react-icons/fa';
|
|
3
3
|
import { useRimori } from '../../providers/PluginProvider';
|
|
4
4
|
import { EventBus } from '@rimori/client';
|
|
@@ -34,6 +34,8 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
|
|
|
34
34
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
35
35
|
const [isLoading, setIsLoading] = useState(false);
|
|
36
36
|
const { ai } = useRimori();
|
|
37
|
+
const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
38
|
+
const eventBusListenerRef = useRef<{ off: () => void } | null>(null);
|
|
37
39
|
|
|
38
40
|
useEffect(() => {
|
|
39
41
|
if (audioUrl) setAudioUrl(null);
|
|
@@ -53,8 +55,24 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
|
|
|
53
55
|
|
|
54
56
|
// Effect to play audio when audioUrl changes and play state is true
|
|
55
57
|
useEffect(() => {
|
|
56
|
-
if (!audioUrl || !isPlaying)
|
|
58
|
+
if (!audioUrl || !isPlaying) {
|
|
59
|
+
// Stop any existing audio when not playing
|
|
60
|
+
if (audioRef.current) {
|
|
61
|
+
audioRef.current.pause();
|
|
62
|
+
audioRef.current.currentTime = 0;
|
|
63
|
+
audioRef.current = null;
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Clean up previous audio instance if it exists
|
|
69
|
+
if (audioRef.current) {
|
|
70
|
+
audioRef.current.pause();
|
|
71
|
+
audioRef.current.currentTime = 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
57
74
|
const audio = new Audio(audioUrl);
|
|
75
|
+
audioRef.current = audio;
|
|
58
76
|
audio.playbackRate = speed;
|
|
59
77
|
audio
|
|
60
78
|
.play()
|
|
@@ -62,18 +80,45 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
|
|
|
62
80
|
audio.onended = () => {
|
|
63
81
|
setIsPlaying(false);
|
|
64
82
|
isFetchingAudio = false;
|
|
83
|
+
audioRef.current = null;
|
|
65
84
|
};
|
|
66
85
|
})
|
|
67
86
|
.catch((e) => {
|
|
68
87
|
console.warn('Error playing audio:', e);
|
|
69
88
|
setIsPlaying(false);
|
|
89
|
+
audioRef.current = null;
|
|
70
90
|
});
|
|
71
91
|
|
|
72
92
|
return () => {
|
|
73
|
-
|
|
93
|
+
if (audioRef.current) {
|
|
94
|
+
audioRef.current.pause();
|
|
95
|
+
audioRef.current.currentTime = 0;
|
|
96
|
+
audioRef.current = null;
|
|
97
|
+
}
|
|
74
98
|
};
|
|
75
99
|
}, [audioUrl, isPlaying, speed]);
|
|
76
100
|
|
|
101
|
+
// Cleanup on unmount - stop audio and revoke object URL
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
return () => {
|
|
104
|
+
if (audioRef.current) {
|
|
105
|
+
audioRef.current.pause();
|
|
106
|
+
audioRef.current.currentTime = 0;
|
|
107
|
+
audioRef.current = null;
|
|
108
|
+
}
|
|
109
|
+
setIsPlaying(false);
|
|
110
|
+
};
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
// Cleanup audioUrl on unmount
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
return () => {
|
|
116
|
+
if (audioUrl) {
|
|
117
|
+
URL.revokeObjectURL(audioUrl);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}, [audioUrl]);
|
|
121
|
+
|
|
77
122
|
const togglePlayback = () => {
|
|
78
123
|
if (!isPlaying && !audioUrl) {
|
|
79
124
|
generateAudio().then(() => setIsPlaying(true));
|
|
@@ -84,7 +129,16 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
|
|
|
84
129
|
|
|
85
130
|
useEffect(() => {
|
|
86
131
|
if (!playListenerEvent) return;
|
|
87
|
-
|
|
132
|
+
const handler = () => togglePlayback();
|
|
133
|
+
const listener = EventBus.on(playListenerEvent, handler);
|
|
134
|
+
eventBusListenerRef.current = listener;
|
|
135
|
+
|
|
136
|
+
return () => {
|
|
137
|
+
if (eventBusListenerRef.current) {
|
|
138
|
+
eventBusListenerRef.current.off();
|
|
139
|
+
eventBusListenerRef.current = null;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
88
142
|
}, [playListenerEvent]);
|
|
89
143
|
|
|
90
144
|
useEffect(() => {
|