@rimori/client 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- 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 +5 -3
- package/dist/components/ai/Avatar.js +14 -6
- package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.d.ts +2 -1
- package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.js +35 -14
- 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 +18 -10
- package/dist/controller/SettingsController.js +28 -31
- 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/core/components/ContextMenu.d.ts +10 -0
- package/dist/core/components/ContextMenu.js +93 -0
- package/dist/core.d.ts +2 -0
- package/dist/core.js +2 -0
- package/dist/hooks/UseChatHook.d.ts +1 -1
- 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 +9 -4
- package/dist/plugin/RimoriClient.d.ts +92 -65
- package/dist/plugin/RimoriClient.js +105 -75
- package/dist/plugin/ThemeSetter.js +4 -4
- package/dist/plugin/fromRimori/EventBus.d.ts +6 -3
- package/dist/plugin/fromRimori/EventBus.js +15 -9
- package/dist/plugin/fromRimori/PluginTypes.d.ts +51 -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 +5 -4
- package/package.json +3 -3
- package/src/components/MarkdownEditor.tsx +78 -76
- package/src/components/ai/Assistant.tsx +1 -1
- package/src/components/ai/Avatar.tsx +65 -48
- package/src/components/ai/EmbeddedAssistent/CircleAudioAvatar.tsx +81 -58
- 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 +80 -75
- package/src/controller/SharedContentController.ts +214 -53
- package/src/controller/SidePluginController.ts +1 -13
- package/src/core/components/ContextMenu.tsx +123 -0
- package/src/core.ts +3 -1
- package/src/hooks/UseChatHook.ts +17 -17
- package/src/plugin/AccomplishmentHandler.ts +165 -0
- package/src/plugin/PluginController.ts +107 -100
- package/src/plugin/RimoriClient.ts +267 -250
- package/src/plugin/ThemeSetter.ts +4 -5
- package/src/plugin/fromRimori/EventBus.ts +23 -12
- package/src/plugin/fromRimori/PluginTypes.ts +67 -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 +5 -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
package/README.md
CHANGED
|
@@ -48,4 +48,17 @@ Inside the pages simply use the `usePlugin` hook.
|
|
|
48
48
|
|
|
49
49
|
```typescript
|
|
50
50
|
const { getSettings, ... } = usePlugin();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
If you use the components then you need to add the library to tailwind.config.js
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
export default {
|
|
57
|
+
darkMode: ["class"], // to detect the dark mode set by the plugin
|
|
58
|
+
content: [
|
|
59
|
+
....
|
|
60
|
+
"node_modules/@rimori/client/dist/components/**/*.{js,jsx}",
|
|
61
|
+
],
|
|
62
|
+
....
|
|
63
|
+
}
|
|
51
64
|
```
|
|
@@ -29,12 +29,14 @@ const MenuBar = () => {
|
|
|
29
29
|
const extensions = [
|
|
30
30
|
StarterKit.configure({
|
|
31
31
|
bulletList: {
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
HTMLAttributes: {
|
|
33
|
+
class: "list-disc list-inside dark:text-white p-1 mt-1 [&_li]:mb-1 [&_p]:inline m-0",
|
|
34
|
+
},
|
|
34
35
|
},
|
|
35
36
|
orderedList: {
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
HTMLAttributes: {
|
|
38
|
+
className: "list-decimal list-inside dark:text-white p-1 mt-1 [&_li]:mb-1 [&_p]:inline m-0",
|
|
39
|
+
},
|
|
38
40
|
},
|
|
39
41
|
}),
|
|
40
42
|
Markdown,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
import { RimoriClient } from "../plugin/RimoriClient";
|
|
3
|
+
export declare class PluginController {
|
|
4
|
+
private static client;
|
|
5
|
+
private static instance;
|
|
6
|
+
private communicationSecret;
|
|
7
|
+
private supabase;
|
|
8
|
+
private supabaseInfo;
|
|
9
|
+
private pluginId;
|
|
10
|
+
private constructor();
|
|
11
|
+
static getInstance(sender: string): Promise<RimoriClient>;
|
|
12
|
+
private getSecret;
|
|
13
|
+
getClient(): Promise<{
|
|
14
|
+
supabase: SupabaseClient;
|
|
15
|
+
tablePrefix: string;
|
|
16
|
+
pluginId: string;
|
|
17
|
+
}>;
|
|
18
|
+
getToken(): Promise<string>;
|
|
19
|
+
getSupabaseUrl(): string;
|
|
20
|
+
getGlobalEventTopic(preliminaryTopic: string): string;
|
|
21
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { createClient } from '@supabase/supabase-js';
|
|
11
|
+
import { EventBus } from '../plugin/fromRimori/EventBus';
|
|
12
|
+
import { RimoriClient } from "../plugin/RimoriClient";
|
|
13
|
+
import { setTheme } from '../plugin/ThemeSetter';
|
|
14
|
+
export class PluginController {
|
|
15
|
+
constructor(pluginId) {
|
|
16
|
+
this.communicationSecret = null;
|
|
17
|
+
this.supabase = null;
|
|
18
|
+
this.supabaseInfo = null;
|
|
19
|
+
this.pluginId = pluginId;
|
|
20
|
+
this.getClient = this.getClient.bind(this);
|
|
21
|
+
if (typeof WorkerGlobalScope === 'undefined') {
|
|
22
|
+
setTheme();
|
|
23
|
+
}
|
|
24
|
+
window.addEventListener("message", (event) => {
|
|
25
|
+
// console.log("client: message received", event);
|
|
26
|
+
const { topic, sender, data, eventId } = event.data.event;
|
|
27
|
+
// skip forwarding messages from own plugin
|
|
28
|
+
if (sender === pluginId)
|
|
29
|
+
return;
|
|
30
|
+
EventBus.emit(sender, topic, data, eventId);
|
|
31
|
+
});
|
|
32
|
+
EventBus.on("*", (event) => {
|
|
33
|
+
// skip messages which are not from the own plugin
|
|
34
|
+
if (event.sender !== this.pluginId)
|
|
35
|
+
return;
|
|
36
|
+
if (event.topic.startsWith("self."))
|
|
37
|
+
return;
|
|
38
|
+
window.parent.postMessage({ event, secret: this.getSecret() }, "*");
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
static getInstance(sender) {
|
|
42
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
43
|
+
if (!PluginController.instance) {
|
|
44
|
+
PluginController.instance = new PluginController(sender);
|
|
45
|
+
PluginController.client = yield RimoriClient.getInstance(PluginController.instance);
|
|
46
|
+
}
|
|
47
|
+
return PluginController.client;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
getSecret() {
|
|
51
|
+
if (!this.communicationSecret) {
|
|
52
|
+
const secret = new URLSearchParams(window.location.search).get("secret");
|
|
53
|
+
if (!secret) {
|
|
54
|
+
throw new Error("Communication secret not found in URL as query parameter");
|
|
55
|
+
}
|
|
56
|
+
this.communicationSecret = secret;
|
|
57
|
+
}
|
|
58
|
+
return this.communicationSecret;
|
|
59
|
+
}
|
|
60
|
+
getClient() {
|
|
61
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
+
if (this.supabase &&
|
|
63
|
+
this.supabaseInfo &&
|
|
64
|
+
this.supabaseInfo.expiration > new Date()) {
|
|
65
|
+
return { supabase: this.supabase, tablePrefix: this.supabaseInfo.tablePrefix, pluginId: this.supabaseInfo.pluginId };
|
|
66
|
+
}
|
|
67
|
+
const { data } = yield EventBus.request(this.pluginId, "global.supabase.requestAccess");
|
|
68
|
+
this.supabaseInfo = data;
|
|
69
|
+
this.supabase = createClient(this.supabaseInfo.url, this.supabaseInfo.key, {
|
|
70
|
+
accessToken: () => Promise.resolve(this.getToken())
|
|
71
|
+
});
|
|
72
|
+
return { supabase: this.supabase, tablePrefix: this.supabaseInfo.tablePrefix, pluginId: this.supabaseInfo.pluginId };
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
getToken() {
|
|
76
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
77
|
+
if (this.supabaseInfo && this.supabaseInfo.expiration && this.supabaseInfo.expiration > new Date()) {
|
|
78
|
+
return this.supabaseInfo.token;
|
|
79
|
+
}
|
|
80
|
+
const { data } = yield EventBus.request(this.pluginId, "global.supabase.requestAccess");
|
|
81
|
+
if (!this.supabaseInfo) {
|
|
82
|
+
throw new Error("Supabase info not found");
|
|
83
|
+
}
|
|
84
|
+
this.supabaseInfo.token = data.token;
|
|
85
|
+
this.supabaseInfo.expiration = data.expiration;
|
|
86
|
+
return this.supabaseInfo.token;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
getSupabaseUrl() {
|
|
90
|
+
if (!this.supabaseInfo) {
|
|
91
|
+
throw new Error("Supabase info not found");
|
|
92
|
+
}
|
|
93
|
+
return this.supabaseInfo.url;
|
|
94
|
+
}
|
|
95
|
+
getGlobalEventTopic(preliminaryTopic) {
|
|
96
|
+
var _a, _b;
|
|
97
|
+
if (preliminaryTopic.startsWith("global.")) {
|
|
98
|
+
return preliminaryTopic;
|
|
99
|
+
}
|
|
100
|
+
if (preliminaryTopic.startsWith("self.")) {
|
|
101
|
+
return preliminaryTopic;
|
|
102
|
+
}
|
|
103
|
+
const topicParts = preliminaryTopic.split(".");
|
|
104
|
+
if (topicParts.length === 3) {
|
|
105
|
+
if (!topicParts[0].startsWith("pl") && topicParts[0] !== "global") {
|
|
106
|
+
throw new Error("The event topic must start with the plugin id or 'global'.");
|
|
107
|
+
}
|
|
108
|
+
return preliminaryTopic;
|
|
109
|
+
}
|
|
110
|
+
else if (topicParts.length > 3) {
|
|
111
|
+
throw new Error(`The event topic must consist of 3 parts. <pluginId>.<topic area>.<action>. Received: ${preliminaryTopic}`);
|
|
112
|
+
}
|
|
113
|
+
const topicRoot = (_b = (_a = this.supabaseInfo) === null || _a === void 0 ? void 0 : _a.pluginId) !== null && _b !== void 0 ? _b : "global";
|
|
114
|
+
return `${topicRoot}.${preliminaryTopic}`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -15,7 +15,7 @@ export function AssistantChat({ avatarImageUrl, voiceId, onComplete, autoStartCo
|
|
|
15
15
|
const { messages, append, isLoading, setMessages } = useChat();
|
|
16
16
|
const lastAssistantMessage = (_a = [...messages].filter((m) => m.role === 'assistant').pop()) === null || _a === void 0 ? void 0 : _a.content;
|
|
17
17
|
useEffect(() => {
|
|
18
|
-
sender.setOnLoudnessChange((value) => event.emit('self.avatar.triggerLoudness', value));
|
|
18
|
+
sender.setOnLoudnessChange((value) => event.emit('self.avatar.triggerLoudness', { loudness: value }));
|
|
19
19
|
if (!autoStartConversation) {
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Tool } from '../../core';
|
|
2
2
|
import { FirstMessages } from './utils';
|
|
3
3
|
interface Props {
|
|
4
|
-
title?: string;
|
|
5
4
|
voiceId: any;
|
|
6
|
-
avatarImageUrl: string;
|
|
7
5
|
agentTools: Tool[];
|
|
6
|
+
avatarImageUrl: string;
|
|
7
|
+
circleSize?: string;
|
|
8
|
+
isDarkTheme?: boolean;
|
|
9
|
+
children?: React.ReactNode;
|
|
8
10
|
autoStartConversation?: FirstMessages;
|
|
9
11
|
}
|
|
10
|
-
export declare function Avatar({ avatarImageUrl, voiceId,
|
|
12
|
+
export declare function Avatar({ avatarImageUrl, voiceId, agentTools, autoStartConversation, children, isDarkTheme, circleSize }: Props): import("react/jsx-runtime").JSX.Element;
|
|
11
13
|
export {};
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo } from 'react';
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
3
|
import { VoiceRecorder } from './EmbeddedAssistent/VoiceRecoder';
|
|
4
4
|
import { MessageSender } from './EmbeddedAssistent/TTS/MessageSender';
|
|
5
5
|
import { CircleAudioAvatar } from './EmbeddedAssistent/CircleAudioAvatar';
|
|
6
6
|
import { useChat } from '../../hooks/UseChatHook';
|
|
7
7
|
import { usePlugin } from '../../components';
|
|
8
8
|
import { getFirstMessages } from './utils';
|
|
9
|
-
export function Avatar({ avatarImageUrl, voiceId,
|
|
9
|
+
export function Avatar({ avatarImageUrl, voiceId, agentTools, autoStartConversation, children, isDarkTheme = false, circleSize = "300px" }) {
|
|
10
10
|
const { llm, event } = usePlugin();
|
|
11
|
+
const [agentReplying, setAgentReplying] = useState(false);
|
|
12
|
+
const [isProcessingMessage, setIsProcessingMessage] = useState(false);
|
|
11
13
|
const sender = useMemo(() => new MessageSender(llm.getVoice, voiceId), []);
|
|
12
14
|
const { messages, append, isLoading, lastMessage, setMessages } = useChat(agentTools);
|
|
13
15
|
useEffect(() => {
|
|
14
16
|
console.log("messages", messages);
|
|
15
17
|
}, [messages]);
|
|
16
18
|
useEffect(() => {
|
|
17
|
-
|
|
19
|
+
if (!isLoading)
|
|
20
|
+
setIsProcessingMessage(false);
|
|
21
|
+
}, [isLoading]);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
sender.setOnLoudnessChange((value) => event.emit('self.avatar.triggerLoudness', { loudness: value }));
|
|
24
|
+
sender.setOnEndOfSpeech(() => setAgentReplying(false));
|
|
18
25
|
if (!autoStartConversation)
|
|
19
26
|
return;
|
|
20
27
|
setMessages(getFirstMessages(autoStartConversation));
|
|
@@ -32,8 +39,9 @@ export function Avatar({ avatarImageUrl, voiceId, title, agentTools, autoStartCo
|
|
|
32
39
|
sender.handleNewText(lastMessage.content, isLoading);
|
|
33
40
|
}
|
|
34
41
|
}, [lastMessage, isLoading]);
|
|
35
|
-
return (_jsxs("div", { className: 'pb-8', children: [
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
return (_jsxs("div", { className: 'pb-8', children: [_jsx(CircleAudioAvatar, { width: circleSize, className: 'mx-auto', imageUrl: avatarImageUrl, isDarkTheme: isDarkTheme }), children, _jsx(VoiceRecorder, { iconSize: '300', disabled: agentReplying, loading: isProcessingMessage, onVoiceRecorded: (message) => {
|
|
43
|
+
setAgentReplying(true);
|
|
44
|
+
append([{ role: 'user', content: "Message(" + Math.floor((messages.length + 1) / 2) + "): " + message, id: messages.length.toString() }]);
|
|
45
|
+
}, onRecordingStatusChange: (running) => !running && setIsProcessingMessage(true) })] }));
|
|
38
46
|
}
|
|
39
47
|
;
|
|
@@ -2,6 +2,7 @@ interface CircleAudioAvatarProps {
|
|
|
2
2
|
width?: string;
|
|
3
3
|
imageUrl: string;
|
|
4
4
|
className?: string;
|
|
5
|
+
isDarkTheme?: boolean;
|
|
5
6
|
}
|
|
6
|
-
export declare function CircleAudioAvatar({ imageUrl, className, width }: CircleAudioAvatarProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export declare function CircleAudioAvatar({ imageUrl, className, isDarkTheme, width }: CircleAudioAvatarProps): import("react/jsx-runtime").JSX.Element;
|
|
7
8
|
export {};
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef } from 'react';
|
|
3
3
|
import { EventBus } from '../../../core';
|
|
4
|
-
export function CircleAudioAvatar({ imageUrl, className, width = "150px" }) {
|
|
4
|
+
export function CircleAudioAvatar({ imageUrl, className, isDarkTheme = false, width = "150px" }) {
|
|
5
5
|
const canvasRef = useRef(null);
|
|
6
|
+
const currentLoudnessRef = useRef(0);
|
|
7
|
+
const targetLoudnessRef = useRef(0);
|
|
8
|
+
const animationFrameRef = useRef(null);
|
|
6
9
|
useEffect(() => {
|
|
7
10
|
const canvas = canvasRef.current;
|
|
8
11
|
if (canvas) {
|
|
@@ -10,35 +13,54 @@ export function CircleAudioAvatar({ imageUrl, className, width = "150px" }) {
|
|
|
10
13
|
if (ctx) {
|
|
11
14
|
const image = new Image();
|
|
12
15
|
image.src = imageUrl;
|
|
16
|
+
let isMounted = true;
|
|
13
17
|
image.onload = () => {
|
|
18
|
+
if (!isMounted)
|
|
19
|
+
return;
|
|
14
20
|
draw(ctx, canvas, image, 0);
|
|
21
|
+
const animate = () => {
|
|
22
|
+
const decayRate = 0.06;
|
|
23
|
+
if (currentLoudnessRef.current > targetLoudnessRef.current) {
|
|
24
|
+
currentLoudnessRef.current = Math.max(targetLoudnessRef.current, currentLoudnessRef.current - decayRate * currentLoudnessRef.current);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
currentLoudnessRef.current = targetLoudnessRef.current;
|
|
28
|
+
}
|
|
29
|
+
draw(ctx, canvas, image, currentLoudnessRef.current);
|
|
30
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
31
|
+
};
|
|
32
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
15
33
|
};
|
|
16
|
-
const handleLoudness = (
|
|
17
|
-
|
|
34
|
+
const handleLoudness = ({ data }) => {
|
|
35
|
+
const newLoudness = data.loudness;
|
|
36
|
+
if (newLoudness > currentLoudnessRef.current) {
|
|
37
|
+
currentLoudnessRef.current = newLoudness;
|
|
38
|
+
}
|
|
39
|
+
targetLoudnessRef.current = newLoudness;
|
|
18
40
|
};
|
|
19
|
-
|
|
20
|
-
const listenerId = EventBus.on('self.avatar.triggerLoudness', handleLoudness);
|
|
41
|
+
const listener = EventBus.on('self.avatar.triggerLoudness', handleLoudness);
|
|
21
42
|
return () => {
|
|
22
|
-
|
|
43
|
+
isMounted = false;
|
|
44
|
+
listener.off();
|
|
45
|
+
if (animationFrameRef.current) {
|
|
46
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
47
|
+
}
|
|
23
48
|
};
|
|
24
49
|
}
|
|
25
50
|
}
|
|
26
51
|
}, [imageUrl]);
|
|
27
|
-
// Function to draw on the canvas
|
|
28
52
|
const draw = (ctx, canvas, image, loudness) => {
|
|
29
53
|
if (canvas && ctx) {
|
|
30
54
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
31
|
-
// Draw pulsing circle
|
|
32
55
|
const radius = Math.min(canvas.width, canvas.height) / 3;
|
|
33
56
|
const centerX = canvas.width / 2;
|
|
34
57
|
const centerY = canvas.height / 2;
|
|
35
|
-
const pulseRadius = radius + loudness / 2.5;
|
|
58
|
+
const pulseRadius = radius + loudness / 2.5;
|
|
36
59
|
ctx.beginPath();
|
|
37
60
|
ctx.arc(centerX, centerY, pulseRadius, 0, Math.PI * 2, true);
|
|
38
|
-
ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
|
|
61
|
+
ctx.strokeStyle = isDarkTheme ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)';
|
|
39
62
|
ctx.lineWidth = 5;
|
|
40
63
|
ctx.stroke();
|
|
41
|
-
// Draw image circle
|
|
42
64
|
ctx.save();
|
|
43
65
|
ctx.beginPath();
|
|
44
66
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2, true);
|
|
@@ -46,11 +68,10 @@ export function CircleAudioAvatar({ imageUrl, className, width = "150px" }) {
|
|
|
46
68
|
ctx.clip();
|
|
47
69
|
ctx.drawImage(image, centerX - radius, centerY - radius, radius * 2, radius * 2);
|
|
48
70
|
ctx.restore();
|
|
49
|
-
// Draw circular frame around the image
|
|
50
71
|
ctx.beginPath();
|
|
51
72
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2, true);
|
|
52
|
-
ctx.strokeStyle = 'rgba(
|
|
53
|
-
ctx.lineWidth = 5;
|
|
73
|
+
ctx.strokeStyle = isDarkTheme ? 'rgba(255, 255, 255, 0.9)' : 'rgba(0, 0, 0, 0.9)';
|
|
74
|
+
ctx.lineWidth = 5;
|
|
54
75
|
ctx.stroke();
|
|
55
76
|
}
|
|
56
77
|
};
|
|
@@ -11,6 +11,7 @@ export declare class ChunkedAudioPlayer {
|
|
|
11
11
|
private loudnessCallback;
|
|
12
12
|
private currentIndex;
|
|
13
13
|
private startedPlaying;
|
|
14
|
+
private onEndOfSpeech;
|
|
14
15
|
constructor();
|
|
15
16
|
private init;
|
|
16
17
|
setOnLoudnessChange(callback: (value: number) => void): void;
|
|
@@ -22,4 +23,5 @@ export declare class ChunkedAudioPlayer {
|
|
|
22
23
|
playAgain(): Promise<void>;
|
|
23
24
|
private monitorLoudness;
|
|
24
25
|
reset(): void;
|
|
26
|
+
setOnEndOfSpeech(callback: () => void): void;
|
|
25
27
|
}
|
|
@@ -18,6 +18,7 @@ export class ChunkedAudioPlayer {
|
|
|
18
18
|
this.loudnessCallback = () => { };
|
|
19
19
|
this.currentIndex = 0;
|
|
20
20
|
this.startedPlaying = false;
|
|
21
|
+
this.onEndOfSpeech = () => { };
|
|
21
22
|
this.init();
|
|
22
23
|
}
|
|
23
24
|
init() {
|
|
@@ -134,6 +135,7 @@ export class ChunkedAudioPlayer {
|
|
|
134
135
|
// console.log('Loudness monitoring stopped.');
|
|
135
136
|
cancelAnimationFrame(this.handle);
|
|
136
137
|
this.loudnessCallback(0);
|
|
138
|
+
this.onEndOfSpeech();
|
|
137
139
|
return;
|
|
138
140
|
}
|
|
139
141
|
// Get the time domain data from the analyser (this is a snapshot of the waveform)
|
|
@@ -177,4 +179,7 @@ export class ChunkedAudioPlayer {
|
|
|
177
179
|
this.isPlaying = false;
|
|
178
180
|
this.init();
|
|
179
181
|
}
|
|
182
|
+
setOnEndOfSpeech(callback) {
|
|
183
|
+
this.onEndOfSpeech = callback;
|
|
184
|
+
}
|
|
180
185
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
interface Props {
|
|
2
2
|
iconSize?: string;
|
|
3
3
|
className?: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
loading?: boolean;
|
|
6
|
+
onRecordingStatusChange: (running: boolean) => void;
|
|
4
7
|
onVoiceRecorded: (message: string) => void;
|
|
5
8
|
}
|
|
6
9
|
export declare const VoiceRecorder: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<unknown>>;
|
|
@@ -8,16 +8,23 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
11
|
-
import { useState, useRef, forwardRef, useImperativeHandle } from 'react';
|
|
12
|
-
import { FaMicrophone } from 'react-icons/fa6';
|
|
11
|
+
import { useState, useRef, forwardRef, useImperativeHandle, useEffect } from 'react';
|
|
12
|
+
import { FaMicrophone, FaSpinner } from 'react-icons/fa6';
|
|
13
13
|
import { usePlugin } from '../../../components';
|
|
14
|
-
export const VoiceRecorder = forwardRef(({ onVoiceRecorded, iconSize, className }, ref) => {
|
|
14
|
+
export const VoiceRecorder = forwardRef(({ onVoiceRecorded, iconSize, className, disabled, loading, onRecordingStatusChange }, ref) => {
|
|
15
15
|
const [isRecording, setIsRecording] = useState(false);
|
|
16
16
|
const mediaRecorderRef = useRef(null);
|
|
17
17
|
const audioChunksRef = useRef([]);
|
|
18
|
+
const mediaStreamRef = useRef(null);
|
|
18
19
|
const { llm } = usePlugin();
|
|
20
|
+
// Ref for latest onVoiceRecorded callback
|
|
21
|
+
const onVoiceRecordedRef = useRef(onVoiceRecorded);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
onVoiceRecordedRef.current = onVoiceRecorded;
|
|
24
|
+
}, [onVoiceRecorded]);
|
|
19
25
|
const startRecording = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
20
26
|
const stream = yield navigator.mediaDevices.getUserMedia({ audio: true });
|
|
27
|
+
mediaStreamRef.current = stream;
|
|
21
28
|
const mediaRecorder = new MediaRecorder(stream);
|
|
22
29
|
mediaRecorderRef.current = mediaRecorder;
|
|
23
30
|
mediaRecorder.ondataavailable = (event) => {
|
|
@@ -26,20 +33,49 @@ export const VoiceRecorder = forwardRef(({ onVoiceRecorded, iconSize, className
|
|
|
26
33
|
mediaRecorder.onstop = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
27
34
|
const audioBlob = new Blob(audioChunksRef.current);
|
|
28
35
|
audioChunksRef.current = [];
|
|
29
|
-
|
|
36
|
+
onVoiceRecordedRef.current(yield llm.getTextFromVoice(audioBlob));
|
|
30
37
|
});
|
|
31
38
|
mediaRecorder.start();
|
|
32
39
|
setIsRecording(true);
|
|
40
|
+
onRecordingStatusChange(true);
|
|
33
41
|
});
|
|
34
42
|
const stopRecording = () => {
|
|
35
43
|
if (mediaRecorderRef.current) {
|
|
36
44
|
mediaRecorderRef.current.stop();
|
|
37
45
|
setIsRecording(false);
|
|
46
|
+
onRecordingStatusChange(false);
|
|
47
|
+
}
|
|
48
|
+
if (mediaStreamRef.current) {
|
|
49
|
+
mediaStreamRef.current.getTracks().forEach(track => track.stop());
|
|
50
|
+
mediaStreamRef.current = null;
|
|
38
51
|
}
|
|
39
52
|
};
|
|
40
53
|
useImperativeHandle(ref, () => ({
|
|
41
54
|
startRecording,
|
|
42
55
|
stopRecording,
|
|
43
56
|
}));
|
|
44
|
-
|
|
57
|
+
// push to talk feature
|
|
58
|
+
const spacePressedRef = useRef(false);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const handleKeyDown = (event) => __awaiter(void 0, void 0, void 0, function* () {
|
|
61
|
+
if (event.code === 'Space' && !spacePressedRef.current) {
|
|
62
|
+
spacePressedRef.current = true;
|
|
63
|
+
yield startRecording();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const handleKeyUp = (event) => {
|
|
67
|
+
if (event.code === 'Space' && spacePressedRef.current) {
|
|
68
|
+
spacePressedRef.current = false;
|
|
69
|
+
stopRecording();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
73
|
+
window.addEventListener('keyup', handleKeyUp);
|
|
74
|
+
return () => {
|
|
75
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
76
|
+
window.removeEventListener('keyup', handleKeyUp);
|
|
77
|
+
};
|
|
78
|
+
}, []);
|
|
79
|
+
return (_jsx("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, onClick: isRecording ? stopRecording : startRecording, disabled: disabled || loading, children: loading ? _jsx(FaSpinner, { className: "animate-spin mr-[6px]" }) :
|
|
80
|
+
_jsx(FaMicrophone, { size: iconSize, className: "h-7 w-7 mr-2 " + (isRecording ? "text-red-600" : "") }) }));
|
|
45
81
|
});
|
package/dist/components.d.ts
CHANGED
package/dist/components.js
CHANGED
|
@@ -47,7 +47,8 @@ export function streamChatGPT(supabaseUrl, messages, tools, onResponse, token) {
|
|
|
47
47
|
content += data;
|
|
48
48
|
// console.log("AI response:", content);
|
|
49
49
|
//content \n\n should be real line break when message is displayed
|
|
50
|
-
|
|
50
|
+
//content \"\" should be real double quote when message is displayed
|
|
51
|
+
onResponse(messageId, content.replace(/\\n/g, '\n').replace(/\\+"|"\\+/g, '"'), false);
|
|
51
52
|
}
|
|
52
53
|
else if (command === 'd') {
|
|
53
54
|
// console.log("AI usage:", JSON.parse(line.substring(2)));
|
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
import { SupabaseClient } from "@supabase/supabase-js";
|
|
2
2
|
import { LanguageLevel } from "../utils/difficultyConverter";
|
|
3
|
+
import { Language } from "../utils/Language";
|
|
3
4
|
export interface UserInfo {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
skill_level_reading: LanguageLevel;
|
|
6
|
+
skill_level_writing: LanguageLevel;
|
|
7
|
+
skill_level_grammar: LanguageLevel;
|
|
8
|
+
skill_level_speaking: LanguageLevel;
|
|
9
|
+
skill_level_listening: LanguageLevel;
|
|
10
|
+
skill_level_understanding: LanguageLevel;
|
|
11
|
+
goal_longterm: string;
|
|
12
|
+
goal_weekly: string;
|
|
13
|
+
study_buddy: string;
|
|
14
|
+
story_genre: string;
|
|
15
|
+
study_duration: number;
|
|
16
|
+
mother_tongue: Language;
|
|
17
|
+
motivation_type: string;
|
|
18
|
+
onboarding_completed: boolean;
|
|
19
|
+
context_menu_on_select: boolean;
|
|
9
20
|
}
|
|
10
21
|
export declare class SettingsController {
|
|
11
22
|
private pluginId;
|
|
12
23
|
private supabase;
|
|
13
24
|
constructor(supabase: SupabaseClient, pluginId: string);
|
|
14
|
-
private getSettingsType;
|
|
15
25
|
private fetchSettings;
|
|
16
|
-
|
|
26
|
+
setSettings(settings: any): Promise<void>;
|
|
17
27
|
getUserInfo(): Promise<UserInfo>;
|
|
18
28
|
/**
|
|
19
29
|
* Get the settings for the plugin. T can be any type of settings, UserSettings or SystemSettings.
|
|
20
30
|
* @param defaultSettings The default settings to use if no settings are found.
|
|
21
|
-
* @param genericSettings The type of settings to get.
|
|
22
31
|
* @returns The settings for the plugin.
|
|
23
32
|
*/
|
|
24
|
-
getSettings<T extends object>(defaultSettings: T
|
|
25
|
-
setSettings(settings: any, genericSettings?: "user" | "system"): Promise<void>;
|
|
33
|
+
getSettings<T extends object>(defaultSettings: T): Promise<T>;
|
|
26
34
|
}
|