@rimori/client 1.0.5 → 1.1.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/README.md +955 -28
- package/dist/components/MarkdownEditor.js +6 -4
- package/dist/components/PluginController.d.ts +21 -0
- package/dist/components/PluginController.js +116 -0
- package/dist/components/ai/Assistant.js +1 -1
- package/dist/components/ai/Avatar.d.ts +6 -4
- package/dist/components/ai/Avatar.js +14 -6
- package/dist/components/ai/EmbeddedAssistent/AudioInputField.js +1 -1
- package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.d.ts +2 -1
- package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.js +36 -15
- 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 +2 -0
- package/dist/components/ai/EmbeddedAssistent/TTS/Player.js +5 -0
- package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.d.ts +3 -0
- package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.js +41 -5
- package/dist/components/ai/utils.d.ts +1 -1
- package/dist/components.d.ts +1 -0
- package/dist/components.js +1 -0
- package/dist/controller/AIController.js +2 -1
- package/dist/controller/SettingsController.d.ts +15 -15
- package/dist/controller/SettingsController.js +15 -16
- package/dist/controller/SharedContentController.d.ts +58 -11
- package/dist/controller/SharedContentController.js +161 -26
- package/dist/controller/SidePluginController.d.ts +1 -12
- package/dist/controller/SidePluginController.js +2 -1
- package/dist/core/components/ContextMenu.d.ts +10 -0
- package/dist/core/components/ContextMenu.js +93 -0
- package/dist/core.d.ts +1 -4
- package/dist/core.js +1 -4
- package/dist/hooks/UseChatHook.d.ts +1 -1
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -8
- package/dist/plugin/AccomplishmentHandler.d.ts +38 -0
- package/dist/plugin/AccomplishmentHandler.js +108 -0
- package/dist/plugin/ContextMenu.d.ts +17 -0
- package/dist/plugin/ContextMenu.js +45 -0
- package/dist/plugin/PluginController.js +6 -3
- package/dist/plugin/RimoriClient.d.ts +92 -65
- package/dist/plugin/RimoriClient.js +105 -75
- package/dist/plugin/ThemeSetter.js +2 -2
- package/dist/plugin/fromRimori/EventBus.d.ts +6 -3
- package/dist/plugin/fromRimori/EventBus.js +15 -9
- package/dist/plugin/fromRimori/PluginTypes.d.ts +48 -0
- package/dist/plugin/fromRimori/PluginTypes.js +1 -0
- package/dist/providers/PluginController.d.ts +21 -0
- package/dist/providers/PluginController.js +116 -0
- package/dist/providers/PluginProvider.js +26 -73
- package/dist/types/Actions.d.ts +4 -0
- package/dist/types/Actions.js +1 -0
- package/dist/utils/Language.d.ts +66 -0
- package/dist/utils/Language.js +67 -0
- package/dist/utils/difficultyConverter.d.ts +1 -0
- package/dist/utils/difficultyConverter.js +3 -0
- package/dist/worker/WorkerSetup.js +4 -4
- package/package.json +2 -3
- package/src/components/MarkdownEditor.tsx +78 -76
- package/src/components/ai/Assistant.tsx +1 -1
- package/src/components/ai/Avatar.tsx +66 -49
- package/src/components/ai/EmbeddedAssistent/AudioInputField.tsx +1 -1
- package/src/components/ai/EmbeddedAssistent/CircleAudioAvatar.tsx +82 -59
- package/src/components/ai/EmbeddedAssistent/TTS/MessageSender.ts +4 -0
- package/src/components/ai/EmbeddedAssistent/TTS/Player.ts +6 -0
- package/src/components/ai/EmbeddedAssistent/VoiceRecoder.tsx +51 -8
- package/src/components/ai/utils.ts +1 -1
- package/src/components.ts +2 -1
- package/src/controller/AIController.ts +2 -1
- package/src/controller/SettingsController.ts +83 -84
- package/src/controller/SharedContentController.ts +214 -53
- package/src/controller/SidePluginController.ts +3 -14
- package/src/core/components/ContextMenu.tsx +123 -0
- package/src/core.ts +1 -4
- package/src/hooks/UseChatHook.ts +17 -17
- package/src/index.ts +0 -8
- package/src/plugin/AccomplishmentHandler.ts +165 -0
- package/src/plugin/PluginController.ts +105 -103
- package/src/plugin/RimoriClient.ts +267 -250
- package/src/plugin/ThemeSetter.ts +2 -2
- package/src/plugin/fromRimori/EventBus.ts +23 -12
- package/src/plugin/fromRimori/PluginTypes.ts +64 -0
- package/src/providers/PluginProvider.tsx +63 -110
- package/src/types/Actions.ts +6 -0
- package/src/utils/Language.ts +70 -0
- package/src/utils/difficultyConverter.ts +4 -0
- package/src/worker/WorkerSetup.ts +4 -4
- package/dist/components/avatar/Assistant.d.ts +0 -9
- package/dist/components/avatar/Assistant.js +0 -59
- package/dist/components/avatar/Avatar.d.ts +0 -12
- package/dist/components/avatar/Avatar.js +0 -42
- package/dist/components/avatar/EmbeddedAssistent/AudioInputField.d.ts +0 -7
- package/dist/components/avatar/EmbeddedAssistent/AudioInputField.js +0 -38
- package/dist/components/avatar/EmbeddedAssistent/CircleAudioAvatar.d.ts +0 -7
- package/dist/components/avatar/EmbeddedAssistent/CircleAudioAvatar.js +0 -59
- package/dist/components/avatar/EmbeddedAssistent/TTS/MessageSender.d.ts +0 -19
- package/dist/components/avatar/EmbeddedAssistent/TTS/MessageSender.js +0 -84
- package/dist/components/avatar/EmbeddedAssistent/TTS/Player.d.ts +0 -25
- package/dist/components/avatar/EmbeddedAssistent/TTS/Player.js +0 -180
- package/dist/components/avatar/EmbeddedAssistent/VoiceRecoder.d.ts +0 -7
- package/dist/components/avatar/EmbeddedAssistent/VoiceRecoder.js +0 -45
- package/dist/components/avatar/utils.d.ts +0 -6
- package/dist/components/avatar/utils.js +0 -14
|
@@ -11,101 +11,103 @@ import { FaBold, FaCode, FaItalic, FaParagraph, FaStrikethrough } from "react-ic
|
|
|
11
11
|
// This inplementation is rooted in the Tiptap editor basic example https://codesandbox.io/p/devbox/editor-9x9dkd
|
|
12
12
|
|
|
13
13
|
interface EditorButtonProps {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
action: string;
|
|
15
|
+
isActive?: boolean;
|
|
16
|
+
label: string | React.ReactNode;
|
|
17
|
+
disabled?: boolean;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const EditorButton = ({ action, isActive, label, disabled }: EditorButtonProps) => {
|
|
21
|
-
|
|
21
|
+
const { editor } = useCurrentEditor() as any;
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (action.includes("heading")) {
|
|
28
|
-
const level = parseInt(action[action.length - 1]);
|
|
29
|
-
return (
|
|
30
|
-
<button
|
|
31
|
-
onClick={() => editor.chain().focus().toggleHeading({ level: level }).run()}
|
|
32
|
-
className={`pl-2 ${isActive ? "is-active" : ""}`}
|
|
33
|
-
>
|
|
34
|
-
{label}
|
|
35
|
-
</button>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
23
|
+
if (!editor) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
38
26
|
|
|
27
|
+
if (action.includes("heading")) {
|
|
28
|
+
const level = parseInt(action[action.length - 1]);
|
|
39
29
|
return (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
</button>
|
|
30
|
+
<button
|
|
31
|
+
onClick={() => editor.chain().focus().toggleHeading({ level: level }).run()}
|
|
32
|
+
className={`pl-2 ${isActive ? "is-active" : ""}`}
|
|
33
|
+
>
|
|
34
|
+
{label}
|
|
35
|
+
</button>
|
|
47
36
|
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<button
|
|
41
|
+
onClick={() => editor.chain().focus()[action]().run()}
|
|
42
|
+
disabled={disabled ? !editor.can().chain().focus()[action]().run() : false}
|
|
43
|
+
className={`pl-2 ${isActive ? "is-active" : ""}`}
|
|
44
|
+
>
|
|
45
|
+
{label}
|
|
46
|
+
</button>
|
|
47
|
+
);
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
const MenuBar = () => {
|
|
51
|
-
|
|
51
|
+
const { editor } = useCurrentEditor();
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
if (!editor) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
57
|
+
return (
|
|
58
|
+
<div className="bg-gray-400 dark:bg-gray-800 dark:text-white text-lg flex flex-row flex-wrap items-center p-1">
|
|
59
|
+
<EditorButton action="toggleBold" isActive={editor.isActive("bold")} label={<FaBold />} disabled />
|
|
60
|
+
<EditorButton action="toggleItalic" isActive={editor.isActive("italic")} label={<FaItalic />} disabled />
|
|
61
|
+
<EditorButton action="toggleStrike" isActive={editor.isActive("strike")} label={<FaStrikethrough />} disabled />
|
|
62
|
+
<EditorButton action="toggleCode" isActive={editor.isActive("code")} label={<FaCode />} disabled />
|
|
63
|
+
<EditorButton action="setParagraph" isActive={editor.isActive("paragraph")} label={<FaParagraph />} />
|
|
64
|
+
<EditorButton action='setHeading1' isActive={editor.isActive("heading", { level: 1 })} label={<LuHeading1 size={"24px"} />} />
|
|
65
|
+
<EditorButton action='setHeading2' isActive={editor.isActive("heading", { level: 2 })} label={<LuHeading2 size={"24px"} />} />
|
|
66
|
+
<EditorButton action='setHeading3' isActive={editor.isActive("heading", { level: 3 })} label={<LuHeading3 size={"24px"} />} />
|
|
67
|
+
<EditorButton action="toggleBulletList" isActive={editor.isActive("bulletList")} label={<AiOutlineUnorderedList size={"24px"} />} />
|
|
68
|
+
<EditorButton action="toggleOrderedList" isActive={editor.isActive("orderedList")} label={<GoListOrdered size={"24px"} />} />
|
|
69
|
+
<EditorButton action="toggleCodeBlock" isActive={editor.isActive("codeBlock")} label={<PiCodeBlock size={"24px"} />} />
|
|
70
|
+
<EditorButton action="toggleBlockquote" isActive={editor.isActive("blockquote")} label={<TbBlockquote size={"24px"} />} />
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
73
|
};
|
|
74
74
|
|
|
75
75
|
const extensions = [
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
76
|
+
StarterKit.configure({
|
|
77
|
+
bulletList: {
|
|
78
|
+
HTMLAttributes: {
|
|
79
|
+
class: "list-disc list-inside dark:text-white p-1 mt-1 [&_li]:mb-1 [&_p]:inline m-0",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
orderedList: {
|
|
83
|
+
HTMLAttributes: {
|
|
84
|
+
className: "list-decimal list-inside dark:text-white p-1 mt-1 [&_li]:mb-1 [&_p]:inline m-0",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
Markdown,
|
|
87
89
|
];
|
|
88
90
|
|
|
89
91
|
interface Props {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
content?: string;
|
|
93
|
+
editable: boolean;
|
|
94
|
+
className?: string;
|
|
95
|
+
onUpdate?: (content: string) => void;
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
export const MarkdownEditor = (props: Props) => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
99
|
+
return (
|
|
100
|
+
<div className={"text-md border border-gray-800 overflow-hidden " + props.className} style={{ borderWidth: props.editable ? 1 : 0 }}>
|
|
101
|
+
<EditorProvider
|
|
102
|
+
key={(props.editable ? "editable" : "readonly") + props.content}
|
|
103
|
+
slotBefore={props.editable ? <MenuBar /> : null}
|
|
104
|
+
extensions={extensions}
|
|
105
|
+
content={props.content}
|
|
106
|
+
editable={props.editable}
|
|
107
|
+
onUpdate={(e) => {
|
|
108
|
+
props.onUpdate && props.onUpdate(e.editor.storage.markdown.getMarkdown());
|
|
109
|
+
}}
|
|
110
|
+
></EditorProvider>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
111
113
|
};
|
|
@@ -23,7 +23,7 @@ export function AssistantChat({ avatarImageUrl, voiceId, onComplete, autoStartCo
|
|
|
23
23
|
const lastAssistantMessage = [...messages].filter((m) => m.role === 'assistant').pop()?.content;
|
|
24
24
|
|
|
25
25
|
useEffect(() => {
|
|
26
|
-
sender.setOnLoudnessChange((value: number) => event.emit('self.avatar.triggerLoudness', value));
|
|
26
|
+
sender.setOnLoudnessChange((value: number) => event.emit('self.avatar.triggerLoudness', { loudness: value }));
|
|
27
27
|
|
|
28
28
|
if (!autoStartConversation) {
|
|
29
29
|
return;
|
|
@@ -1,61 +1,78 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useEffect, useMemo } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
2
|
import { VoiceRecorder } from './EmbeddedAssistent/VoiceRecoder';
|
|
4
3
|
import { MessageSender } from './EmbeddedAssistent/TTS/MessageSender';
|
|
5
4
|
import { CircleAudioAvatar } from './EmbeddedAssistent/CircleAudioAvatar';
|
|
5
|
+
import { Tool } from '../../controller/AIController';
|
|
6
6
|
import { useChat } from '../../hooks/UseChatHook';
|
|
7
7
|
import { usePlugin } from '../../components';
|
|
8
8
|
import { getFirstMessages } from './utils';
|
|
9
9
|
import { FirstMessages } from './utils';
|
|
10
10
|
|
|
11
11
|
interface Props {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
voiceId: any;
|
|
13
|
+
agentTools: Tool[];
|
|
14
|
+
avatarImageUrl: string;
|
|
15
|
+
circleSize?: string;
|
|
16
|
+
isDarkTheme?: boolean;
|
|
17
|
+
children?: React.ReactNode;
|
|
18
|
+
autoStartConversation?: FirstMessages;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
export function Avatar({ avatarImageUrl, voiceId,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
21
|
+
export function Avatar({ avatarImageUrl, voiceId, agentTools, autoStartConversation, children, isDarkTheme = false, circleSize = "300px" }: Props) {
|
|
22
|
+
const { llm, event } = usePlugin();
|
|
23
|
+
const [agentReplying, setAgentReplying] = useState(false);
|
|
24
|
+
const [isProcessingMessage, setIsProcessingMessage] = useState(false);
|
|
25
|
+
const sender = useMemo(() => new MessageSender(llm.getVoice, voiceId), []);
|
|
26
|
+
const { messages, append, isLoading, lastMessage, setMessages } = useChat(agentTools);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
console.log("messages", messages);
|
|
30
|
+
}, [messages]);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!isLoading) setIsProcessingMessage(false);
|
|
34
|
+
}, [isLoading]);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
sender.setOnLoudnessChange((value) => event.emit('self.avatar.triggerLoudness', { loudness: value }));
|
|
38
|
+
sender.setOnEndOfSpeech(() => setAgentReplying(false));
|
|
39
|
+
|
|
40
|
+
if (!autoStartConversation) return;
|
|
41
|
+
|
|
42
|
+
setMessages(getFirstMessages(autoStartConversation));
|
|
43
|
+
// append([{ role: 'user', content: autoStartConversation.userMessage }]);
|
|
44
|
+
|
|
45
|
+
if (autoStartConversation.assistantMessage) {
|
|
46
|
+
// console.log("autostartmessages", { autoStartConversation, isLoading });
|
|
47
|
+
sender.handleNewText(autoStartConversation.assistantMessage, isLoading);
|
|
48
|
+
} else if (autoStartConversation.userMessage) {
|
|
49
|
+
append([{ role: 'user', content: autoStartConversation.userMessage, id: messages.length.toString() }]);
|
|
50
|
+
}
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (lastMessage?.role === 'assistant') {
|
|
55
|
+
sender.handleNewText(lastMessage.content, isLoading);
|
|
56
|
+
}
|
|
57
|
+
}, [lastMessage, isLoading]);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className='pb-8'>
|
|
61
|
+
<CircleAudioAvatar
|
|
62
|
+
width={circleSize}
|
|
63
|
+
className='mx-auto'
|
|
64
|
+
imageUrl={avatarImageUrl}
|
|
65
|
+
isDarkTheme={isDarkTheme} />
|
|
66
|
+
{children}
|
|
67
|
+
<VoiceRecorder
|
|
68
|
+
iconSize='300'
|
|
69
|
+
disabled={agentReplying}
|
|
70
|
+
loading={isProcessingMessage}
|
|
71
|
+
onVoiceRecorded={(message) => {
|
|
72
|
+
setAgentReplying(true);
|
|
73
|
+
append([{ role: 'user', content: "Message(" + Math.floor((messages.length + 1) / 2) + "): " + message, id: messages.length.toString() }]);
|
|
74
|
+
}}
|
|
75
|
+
onRecordingStatusChange={(running) => !running && setIsProcessingMessage(true)} />
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
61
78
|
};
|
|
@@ -43,7 +43,7 @@ export function AudioInputField({ onSubmit, onAudioControl, blockSubmission = fa
|
|
|
43
43
|
className="cursor-default">
|
|
44
44
|
{audioEnabled ? <HiMiniSpeakerWave className='w-9 h-9 cursor-pointer' /> : <HiMiniSpeakerXMark className='w-9 h-9 cursor-pointer' />}
|
|
45
45
|
</button>}
|
|
46
|
-
<VoiceRecorder onVoiceRecorded={(m: string) => {
|
|
46
|
+
<VoiceRecorder onRecordingStatusChange={() => {}} onVoiceRecorded={(m: string) => {
|
|
47
47
|
console.log('onVoiceRecorded', m);
|
|
48
48
|
handleSubmit(m);
|
|
49
49
|
}}
|
|
@@ -1,75 +1,98 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { EventBus, EventBusMessage } from '../../../
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { EventBus, EventBusMessage } from '../../../plugin/fromRimori/EventBus';
|
|
3
3
|
|
|
4
4
|
interface CircleAudioAvatarProps {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
width?: string;
|
|
6
|
+
imageUrl: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
isDarkTheme?: boolean;
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
export function CircleAudioAvatar({ imageUrl, className, isDarkTheme = false, width = "150px" }: CircleAudioAvatarProps) {
|
|
12
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
13
|
+
const currentLoudnessRef = useRef(0);
|
|
14
|
+
const targetLoudnessRef = useRef(0);
|
|
15
|
+
const animationFrameRef = useRef<number | null>(null);
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
const
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const canvas = canvasRef.current;
|
|
19
|
+
if (canvas) {
|
|
20
|
+
const ctx = canvas.getContext('2d');
|
|
21
|
+
if (ctx) {
|
|
22
|
+
const image = new Image();
|
|
23
|
+
image.src = imageUrl;
|
|
24
|
+
let isMounted = true;
|
|
13
25
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
image.onload = () => {
|
|
27
|
+
if (!isMounted) return;
|
|
28
|
+
draw(ctx, canvas, image, 0);
|
|
29
|
+
const animate = () => {
|
|
30
|
+
const decayRate = 0.06;
|
|
31
|
+
if (currentLoudnessRef.current > targetLoudnessRef.current) {
|
|
32
|
+
currentLoudnessRef.current = Math.max(
|
|
33
|
+
targetLoudnessRef.current,
|
|
34
|
+
currentLoudnessRef.current - decayRate * currentLoudnessRef.current
|
|
35
|
+
);
|
|
36
|
+
} else {
|
|
37
|
+
currentLoudnessRef.current = targetLoudnessRef.current;
|
|
38
|
+
}
|
|
39
|
+
draw(ctx, canvas, image, currentLoudnessRef.current);
|
|
40
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
41
|
+
};
|
|
42
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
43
|
+
};
|
|
24
44
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
45
|
+
const handleLoudness = ({ data }: EventBusMessage) => {
|
|
46
|
+
const newLoudness = data.loudness;
|
|
47
|
+
if (newLoudness > currentLoudnessRef.current) {
|
|
48
|
+
currentLoudnessRef.current = newLoudness;
|
|
49
|
+
}
|
|
50
|
+
targetLoudnessRef.current = newLoudness;
|
|
51
|
+
};
|
|
28
52
|
|
|
29
|
-
|
|
30
|
-
const listenerId = EventBus.on('self.avatar.triggerLoudness', handleLoudness);
|
|
53
|
+
const listener = EventBus.on('self.avatar.triggerLoudness', handleLoudness);
|
|
31
54
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
return () => {
|
|
56
|
+
isMounted = false;
|
|
57
|
+
listener.off();
|
|
58
|
+
if (animationFrameRef.current) {
|
|
59
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}, [imageUrl]);
|
|
38
65
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
66
|
+
const draw = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, image: HTMLImageElement, loudness: number) => {
|
|
67
|
+
if (canvas && ctx) {
|
|
68
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
43
69
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
ctx.stroke();
|
|
70
|
+
const radius = Math.min(canvas.width, canvas.height) / 3;
|
|
71
|
+
const centerX = canvas.width / 2;
|
|
72
|
+
const centerY = canvas.height / 2;
|
|
73
|
+
const pulseRadius = radius + loudness / 2.5;
|
|
74
|
+
ctx.beginPath();
|
|
75
|
+
ctx.arc(centerX, centerY, pulseRadius, 0, Math.PI * 2, true);
|
|
76
|
+
ctx.strokeStyle = isDarkTheme ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)';
|
|
77
|
+
ctx.lineWidth = 5;
|
|
78
|
+
ctx.stroke();
|
|
54
79
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
ctx.restore();
|
|
80
|
+
ctx.save();
|
|
81
|
+
ctx.beginPath();
|
|
82
|
+
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2, true);
|
|
83
|
+
ctx.closePath();
|
|
84
|
+
ctx.clip();
|
|
85
|
+
ctx.drawImage(image, centerX - radius, centerY - radius, radius * 2, radius * 2);
|
|
86
|
+
ctx.restore();
|
|
63
87
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
};
|
|
88
|
+
ctx.beginPath();
|
|
89
|
+
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2, true);
|
|
90
|
+
ctx.strokeStyle = isDarkTheme ? 'rgba(255, 255, 255, 0.9)' : 'rgba(0, 0, 0, 0.9)';
|
|
91
|
+
ctx.lineWidth = 5;
|
|
92
|
+
ctx.stroke();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
72
95
|
|
|
73
|
-
|
|
96
|
+
return <canvas ref={canvasRef} className={className} width={500} height={500} style={{ width }} />;
|
|
74
97
|
};
|
|
75
98
|
|
|
@@ -12,6 +12,7 @@ export class ChunkedAudioPlayer {
|
|
|
12
12
|
private loudnessCallback: (value: number) => void = () => { };
|
|
13
13
|
private currentIndex = 0;
|
|
14
14
|
private startedPlaying = false;
|
|
15
|
+
private onEndOfSpeech: () => void = () => { };
|
|
15
16
|
|
|
16
17
|
constructor() {
|
|
17
18
|
this.init();
|
|
@@ -139,6 +140,7 @@ export class ChunkedAudioPlayer {
|
|
|
139
140
|
// console.log('Loudness monitoring stopped.');
|
|
140
141
|
cancelAnimationFrame(this.handle);
|
|
141
142
|
this.loudnessCallback(0);
|
|
143
|
+
this.onEndOfSpeech();
|
|
142
144
|
return;
|
|
143
145
|
}
|
|
144
146
|
|
|
@@ -189,4 +191,8 @@ export class ChunkedAudioPlayer {
|
|
|
189
191
|
this.isPlaying = false;
|
|
190
192
|
this.init();
|
|
191
193
|
}
|
|
194
|
+
|
|
195
|
+
public setOnEndOfSpeech(callback: () => void) {
|
|
196
|
+
this.onEndOfSpeech = callback;
|
|
197
|
+
}
|
|
192
198
|
}
|
|
@@ -1,21 +1,32 @@
|
|
|
1
|
-
import { useState, useRef, forwardRef, useImperativeHandle } from 'react';
|
|
2
|
-
import { FaMicrophone } from 'react-icons/fa6';
|
|
1
|
+
import { useState, useRef, forwardRef, useImperativeHandle, useEffect } from 'react';
|
|
2
|
+
import { FaMicrophone, FaSpinner } from 'react-icons/fa6';
|
|
3
3
|
import { usePlugin } from '../../../components';
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
iconSize?: string;
|
|
7
7
|
className?: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
loading?: boolean;
|
|
10
|
+
onRecordingStatusChange: (running: boolean) => void;
|
|
8
11
|
onVoiceRecorded: (message: string) => void;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
export const VoiceRecorder = forwardRef(({ onVoiceRecorded, iconSize, className }: Props, ref) => {
|
|
14
|
+
export const VoiceRecorder = forwardRef(({ onVoiceRecorded, iconSize, className, disabled, loading, onRecordingStatusChange }: Props, ref) => {
|
|
12
15
|
const [isRecording, setIsRecording] = useState(false);
|
|
13
16
|
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
|
14
17
|
const audioChunksRef = useRef<Blob[]>([]);
|
|
18
|
+
const mediaStreamRef = useRef<MediaStream | null>(null);
|
|
15
19
|
const { llm } = usePlugin();
|
|
16
20
|
|
|
21
|
+
// Ref for latest onVoiceRecorded callback
|
|
22
|
+
const onVoiceRecordedRef = useRef(onVoiceRecorded);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
onVoiceRecordedRef.current = onVoiceRecorded;
|
|
25
|
+
}, [onVoiceRecorded]);
|
|
26
|
+
|
|
17
27
|
const startRecording = async () => {
|
|
18
28
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
29
|
+
mediaStreamRef.current = stream;
|
|
19
30
|
const mediaRecorder = new MediaRecorder(stream);
|
|
20
31
|
mediaRecorderRef.current = mediaRecorder;
|
|
21
32
|
|
|
@@ -27,17 +38,23 @@ export const VoiceRecorder = forwardRef(({ onVoiceRecorded, iconSize, className
|
|
|
27
38
|
const audioBlob = new Blob(audioChunksRef.current);
|
|
28
39
|
audioChunksRef.current = [];
|
|
29
40
|
|
|
30
|
-
|
|
41
|
+
onVoiceRecordedRef.current(await llm.getTextFromVoice(audioBlob));
|
|
31
42
|
};
|
|
32
43
|
|
|
33
44
|
mediaRecorder.start();
|
|
34
45
|
setIsRecording(true);
|
|
46
|
+
onRecordingStatusChange(true);
|
|
35
47
|
};
|
|
36
48
|
|
|
37
49
|
const stopRecording = () => {
|
|
38
50
|
if (mediaRecorderRef.current) {
|
|
39
51
|
mediaRecorderRef.current.stop();
|
|
40
52
|
setIsRecording(false);
|
|
53
|
+
onRecordingStatusChange(false);
|
|
54
|
+
}
|
|
55
|
+
if (mediaStreamRef.current) {
|
|
56
|
+
mediaStreamRef.current.getTracks().forEach(track => track.stop());
|
|
57
|
+
mediaStreamRef.current = null;
|
|
41
58
|
}
|
|
42
59
|
};
|
|
43
60
|
|
|
@@ -46,11 +63,37 @@ export const VoiceRecorder = forwardRef(({ onVoiceRecorded, iconSize, className
|
|
|
46
63
|
stopRecording,
|
|
47
64
|
}));
|
|
48
65
|
|
|
66
|
+
// push to talk feature
|
|
67
|
+
const spacePressedRef = useRef(false);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const handleKeyDown = async (event: KeyboardEvent) => {
|
|
71
|
+
if (event.code === 'Space' && !spacePressedRef.current) {
|
|
72
|
+
spacePressedRef.current = true;
|
|
73
|
+
await startRecording();
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const handleKeyUp = (event: KeyboardEvent) => {
|
|
77
|
+
if (event.code === 'Space' && spacePressedRef.current) {
|
|
78
|
+
spacePressedRef.current = false;
|
|
79
|
+
stopRecording();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
83
|
+
window.addEventListener('keyup', handleKeyUp);
|
|
84
|
+
return () => {
|
|
85
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
86
|
+
window.removeEventListener('keyup', handleKeyUp);
|
|
87
|
+
};
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
49
90
|
return (
|
|
50
|
-
<
|
|
51
|
-
|
|
91
|
+
<button className={"w-16 h-16 flex text-4xl shadow-lg flex-row justify-center items-center rounded-full mx-auto bg-gray-400 dark:bg-gray-800 pl-[6px] disabled:opacity-50 " + className}
|
|
92
|
+
onClick={isRecording ? stopRecording : startRecording}
|
|
93
|
+
disabled={disabled || loading}>
|
|
94
|
+
{loading ? <FaSpinner className="animate-spin mr-[6px]" /> :
|
|
52
95
|
<FaMicrophone size={iconSize} className={"h-7 w-7 mr-2 " + (isRecording ? "text-red-600" : "")} />
|
|
53
|
-
|
|
54
|
-
</
|
|
96
|
+
}
|
|
97
|
+
</button>
|
|
55
98
|
);
|
|
56
99
|
});
|
package/src/components.ts
CHANGED
|
@@ -7,4 +7,5 @@ export * from "./hooks/UseChatHook";
|
|
|
7
7
|
export * from "./plugin/ThemeSetter";
|
|
8
8
|
export * from "./providers/PluginProvider";
|
|
9
9
|
export * from "./components/ai/Avatar";
|
|
10
|
-
export * from "./components/ai/Assistant";
|
|
10
|
+
export * from "./components/ai/Assistant";
|
|
11
|
+
export * from "./types/Actions";
|