@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.
Files changed (95) hide show
  1. package/README.md +13 -0
  2. package/dist/components/MarkdownEditor.js +6 -4
  3. package/dist/components/PluginController.d.ts +21 -0
  4. package/dist/components/PluginController.js +116 -0
  5. package/dist/components/ai/Assistant.js +1 -1
  6. package/dist/components/ai/Avatar.d.ts +5 -3
  7. package/dist/components/ai/Avatar.js +14 -6
  8. package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.d.ts +2 -1
  9. package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.js +35 -14
  10. package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.d.ts +1 -0
  11. package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.js +3 -0
  12. package/dist/components/ai/EmbeddedAssistent/TTS/Player.d.ts +2 -0
  13. package/dist/components/ai/EmbeddedAssistent/TTS/Player.js +5 -0
  14. package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.d.ts +3 -0
  15. package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.js +41 -5
  16. package/dist/components/ai/utils.d.ts +1 -1
  17. package/dist/components.d.ts +1 -0
  18. package/dist/components.js +1 -0
  19. package/dist/controller/AIController.js +2 -1
  20. package/dist/controller/SettingsController.d.ts +18 -10
  21. package/dist/controller/SettingsController.js +28 -31
  22. package/dist/controller/SharedContentController.d.ts +58 -11
  23. package/dist/controller/SharedContentController.js +161 -26
  24. package/dist/controller/SidePluginController.d.ts +1 -12
  25. package/dist/core/components/ContextMenu.d.ts +10 -0
  26. package/dist/core/components/ContextMenu.js +93 -0
  27. package/dist/core.d.ts +2 -0
  28. package/dist/core.js +2 -0
  29. package/dist/hooks/UseChatHook.d.ts +1 -1
  30. package/dist/plugin/AccomplishmentHandler.d.ts +38 -0
  31. package/dist/plugin/AccomplishmentHandler.js +108 -0
  32. package/dist/plugin/ContextMenu.d.ts +17 -0
  33. package/dist/plugin/ContextMenu.js +45 -0
  34. package/dist/plugin/PluginController.js +9 -4
  35. package/dist/plugin/RimoriClient.d.ts +92 -65
  36. package/dist/plugin/RimoriClient.js +105 -75
  37. package/dist/plugin/ThemeSetter.js +4 -4
  38. package/dist/plugin/fromRimori/EventBus.d.ts +6 -3
  39. package/dist/plugin/fromRimori/EventBus.js +15 -9
  40. package/dist/plugin/fromRimori/PluginTypes.d.ts +51 -0
  41. package/dist/plugin/fromRimori/PluginTypes.js +1 -0
  42. package/dist/providers/PluginController.d.ts +21 -0
  43. package/dist/providers/PluginController.js +116 -0
  44. package/dist/providers/PluginProvider.js +26 -73
  45. package/dist/types/Actions.d.ts +4 -0
  46. package/dist/types/Actions.js +1 -0
  47. package/dist/utils/Language.d.ts +66 -0
  48. package/dist/utils/Language.js +67 -0
  49. package/dist/utils/difficultyConverter.d.ts +1 -0
  50. package/dist/utils/difficultyConverter.js +3 -0
  51. package/dist/worker/WorkerSetup.js +5 -4
  52. package/package.json +3 -3
  53. package/src/components/MarkdownEditor.tsx +78 -76
  54. package/src/components/ai/Assistant.tsx +1 -1
  55. package/src/components/ai/Avatar.tsx +65 -48
  56. package/src/components/ai/EmbeddedAssistent/CircleAudioAvatar.tsx +81 -58
  57. package/src/components/ai/EmbeddedAssistent/TTS/MessageSender.ts +4 -0
  58. package/src/components/ai/EmbeddedAssistent/TTS/Player.ts +6 -0
  59. package/src/components/ai/EmbeddedAssistent/VoiceRecoder.tsx +51 -8
  60. package/src/components/ai/utils.ts +1 -1
  61. package/src/components.ts +2 -1
  62. package/src/controller/AIController.ts +2 -1
  63. package/src/controller/SettingsController.ts +80 -75
  64. package/src/controller/SharedContentController.ts +214 -53
  65. package/src/controller/SidePluginController.ts +1 -13
  66. package/src/core/components/ContextMenu.tsx +123 -0
  67. package/src/core.ts +3 -1
  68. package/src/hooks/UseChatHook.ts +17 -17
  69. package/src/plugin/AccomplishmentHandler.ts +165 -0
  70. package/src/plugin/PluginController.ts +107 -100
  71. package/src/plugin/RimoriClient.ts +267 -250
  72. package/src/plugin/ThemeSetter.ts +4 -5
  73. package/src/plugin/fromRimori/EventBus.ts +23 -12
  74. package/src/plugin/fromRimori/PluginTypes.ts +67 -0
  75. package/src/providers/PluginProvider.tsx +63 -110
  76. package/src/types/Actions.ts +6 -0
  77. package/src/utils/Language.ts +70 -0
  78. package/src/utils/difficultyConverter.ts +4 -0
  79. package/src/worker/WorkerSetup.ts +5 -4
  80. package/dist/components/avatar/Assistant.d.ts +0 -9
  81. package/dist/components/avatar/Assistant.js +0 -59
  82. package/dist/components/avatar/Avatar.d.ts +0 -12
  83. package/dist/components/avatar/Avatar.js +0 -42
  84. package/dist/components/avatar/EmbeddedAssistent/AudioInputField.d.ts +0 -7
  85. package/dist/components/avatar/EmbeddedAssistent/AudioInputField.js +0 -38
  86. package/dist/components/avatar/EmbeddedAssistent/CircleAudioAvatar.d.ts +0 -7
  87. package/dist/components/avatar/EmbeddedAssistent/CircleAudioAvatar.js +0 -59
  88. package/dist/components/avatar/EmbeddedAssistent/TTS/MessageSender.d.ts +0 -19
  89. package/dist/components/avatar/EmbeddedAssistent/TTS/MessageSender.js +0 -84
  90. package/dist/components/avatar/EmbeddedAssistent/TTS/Player.d.ts +0 -25
  91. package/dist/components/avatar/EmbeddedAssistent/TTS/Player.js +0 -180
  92. package/dist/components/avatar/EmbeddedAssistent/VoiceRecoder.d.ts +0 -7
  93. package/dist/components/avatar/EmbeddedAssistent/VoiceRecoder.js +0 -45
  94. package/dist/components/avatar/utils.d.ts +0 -6
  95. 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
- keepMarks: true,
33
- keepAttributes: false,
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
- keepMarks: true,
37
- keepAttributes: false,
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, title, agentTools, autoStartConversation }: Props): import("react/jsx-runtime").JSX.Element;
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, title, agentTools, autoStartConversation }) {
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
- sender.setOnLoudnessChange((value) => event.emit('self.avatar.triggerLoudness', value));
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: [title && _jsx("p", { className: "text-center mt-5 w-3/4 mx-auto rounded-lg dark:text-gray-100", children: title }), _jsx(CircleAudioAvatar, { imageUrl: avatarImageUrl, width: "250px", className: 'mx-auto' }), _jsx("div", { 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', children: _jsx(VoiceRecorder, { className: 'w-7', iconSize: '300', onVoiceRecorded: (message) => {
36
- append([{ role: 'user', content: "Message(" + Math.floor((messages.length + 1) / 2) + "): " + message, id: messages.length.toString() }]);
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 = (event) => {
17
- draw(ctx, canvas, image, event.data.loudness);
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
- // Subscribe to loudness changes
20
- const listenerId = EventBus.on('self.avatar.triggerLoudness', handleLoudness);
41
+ const listener = EventBus.on('self.avatar.triggerLoudness', handleLoudness);
21
42
  return () => {
22
- EventBus.off(listenerId);
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; // Adjust the divisor for sensitivity
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(20,20, 20, 0.9)';
53
- ctx.lineWidth = 5; // Adjust the width of the frame as needed
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
  };
@@ -15,5 +15,6 @@ export declare class MessageSender {
15
15
  private reset;
16
16
  setVolume(volume: number): void;
17
17
  setOnLoudnessChange(callback: (value: number) => void): void;
18
+ setOnEndOfSpeech(callback: () => void): void;
18
19
  }
19
20
  export {};
@@ -83,4 +83,7 @@ export class MessageSender {
83
83
  callback(loudness);
84
84
  });
85
85
  }
86
+ setOnEndOfSpeech(callback) {
87
+ this.player.setOnEndOfSpeech(callback);
88
+ }
86
89
  }
@@ -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
- onVoiceRecorded(yield llm.getTextFromVoice(audioBlob));
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
- return (_jsx("div", { className: className, children: _jsx("button", { onClick: isRecording ? stopRecording : startRecording, children: _jsx(FaMicrophone, { size: iconSize, className: "h-7 w-7 mr-2 " + (isRecording ? "text-red-600" : "") }) }) }));
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
  });
@@ -1,6 +1,6 @@
1
1
  export interface FirstMessages {
2
2
  instructions?: string;
3
- userMessage: string;
3
+ userMessage?: string;
4
4
  assistantMessage?: string;
5
5
  }
6
6
  export declare function getFirstMessages(instructions: FirstMessages): any[];
@@ -7,3 +7,4 @@ export * from "./plugin/ThemeSetter";
7
7
  export * from "./providers/PluginProvider";
8
8
  export * from "./components/ai/Avatar";
9
9
  export * from "./components/ai/Assistant";
10
+ export * from "./types/Actions";
@@ -8,3 +8,4 @@ export * from "./plugin/ThemeSetter";
8
8
  export * from "./providers/PluginProvider";
9
9
  export * from "./components/ai/Avatar";
10
10
  export * from "./components/ai/Assistant";
11
+ export * from "./types/Actions";
@@ -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
- onResponse(messageId, content.replace(/\\n/g, '\n'), false);
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
- motherTongue: string;
5
- languageLevel: LanguageLevel;
6
- contextMenuOnSelect: boolean;
7
- }
8
- export interface SystemSettings {
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
- private saveSettings;
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, genericSettings?: "user" | "system"): Promise<T>;
25
- setSettings(settings: any, genericSettings?: "user" | "system"): Promise<void>;
33
+ getSettings<T extends object>(defaultSettings: T): Promise<T>;
26
34
  }