@rimori/client 1.0.2 → 1.0.4

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 (135) hide show
  1. package/README.md +51 -0
  2. package/dist/components/CRUDModal.js +0 -1
  3. package/dist/components/ai/Assistant.d.ts +9 -0
  4. package/dist/components/ai/Assistant.js +59 -0
  5. package/dist/components/ai/Avatar.d.ts +11 -0
  6. package/dist/components/ai/Avatar.js +39 -0
  7. package/dist/components/ai/EmbeddedAssistent/AudioInputField.d.ts +7 -0
  8. package/dist/components/ai/EmbeddedAssistent/AudioInputField.js +38 -0
  9. package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.d.ts +7 -0
  10. package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.js +59 -0
  11. package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.d.ts +19 -0
  12. package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.js +86 -0
  13. package/dist/components/ai/EmbeddedAssistent/TTS/Player.d.ts +25 -0
  14. package/dist/components/ai/EmbeddedAssistent/TTS/Player.js +180 -0
  15. package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.d.ts +7 -0
  16. package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.js +45 -0
  17. package/dist/components/ai/utils.d.ts +6 -0
  18. package/dist/components/ai/utils.js +14 -0
  19. package/dist/components/audio/Playbutton.js +4 -5
  20. package/dist/components/avatar/Assistant.d.ts +9 -0
  21. package/dist/components/avatar/Assistant.js +59 -0
  22. package/dist/components/avatar/Avatar.d.ts +12 -0
  23. package/dist/components/avatar/Avatar.js +42 -0
  24. package/dist/components/avatar/EmbeddedAssistent/AudioInputField.d.ts +7 -0
  25. package/dist/components/avatar/EmbeddedAssistent/AudioInputField.js +38 -0
  26. package/dist/components/avatar/EmbeddedAssistent/CircleAudioAvatar.d.ts +7 -0
  27. package/dist/components/avatar/EmbeddedAssistent/CircleAudioAvatar.js +59 -0
  28. package/dist/components/avatar/EmbeddedAssistent/TTS/MessageSender.d.ts +19 -0
  29. package/dist/components/avatar/EmbeddedAssistent/TTS/MessageSender.js +84 -0
  30. package/dist/components/avatar/EmbeddedAssistent/TTS/Player.d.ts +25 -0
  31. package/dist/components/avatar/EmbeddedAssistent/TTS/Player.js +180 -0
  32. package/dist/components/avatar/EmbeddedAssistent/VoiceRecoder.d.ts +7 -0
  33. package/dist/components/avatar/EmbeddedAssistent/VoiceRecoder.js +45 -0
  34. package/dist/components/avatar/utils.d.ts +6 -0
  35. package/dist/components/avatar/utils.js +14 -0
  36. package/dist/components.d.ts +9 -0
  37. package/dist/components.js +10 -0
  38. package/dist/controller/AIController.d.ts +4 -3
  39. package/dist/controller/AIController.js +32 -8
  40. package/dist/controller/ObjectController.d.ts +2 -2
  41. package/dist/controller/ObjectController.js +4 -5
  42. package/dist/controller/SettingsController.d.ts +3 -1
  43. package/dist/controller/SettingsController.js +9 -0
  44. package/dist/controller/SharedContentController.js +6 -6
  45. package/dist/controller/SidePluginController.d.ts +14 -0
  46. package/dist/{plugin/VoiceController.js → controller/SidePluginController.js} +18 -15
  47. package/dist/controller/VoiceController.js +1 -1
  48. package/dist/core.d.ts +9 -0
  49. package/dist/core.js +10 -0
  50. package/dist/hooks/UseChatHook.js +2 -2
  51. package/dist/index.d.ts +3 -2
  52. package/dist/index.js +4 -2
  53. package/dist/plugin/PluginController.d.ts +4 -12
  54. package/dist/plugin/PluginController.js +43 -70
  55. package/dist/plugin/RimoriClient.d.ts +87 -27
  56. package/dist/plugin/RimoriClient.js +101 -67
  57. package/dist/plugin/fromRimori/EventBus.d.ts +98 -0
  58. package/dist/plugin/fromRimori/EventBus.js +240 -0
  59. package/dist/providers/PluginProvider.d.ts +1 -0
  60. package/dist/providers/PluginProvider.js +64 -12
  61. package/dist/worker/WorkerSetup.d.ts +6 -0
  62. package/dist/worker/WorkerSetup.js +79 -0
  63. package/package.json +16 -3
  64. package/src/components/CRUDModal.tsx +1 -3
  65. package/src/components/ai/Assistant.tsx +96 -0
  66. package/src/components/ai/Avatar.tsx +61 -0
  67. package/src/components/ai/EmbeddedAssistent/AudioInputField.tsx +64 -0
  68. package/src/components/ai/EmbeddedAssistent/CircleAudioAvatar.tsx +75 -0
  69. package/src/components/ai/EmbeddedAssistent/TTS/MessageSender.ts +91 -0
  70. package/src/components/ai/EmbeddedAssistent/TTS/Player.ts +192 -0
  71. package/src/components/ai/EmbeddedAssistent/VoiceRecoder.tsx +56 -0
  72. package/src/components/ai/utils.ts +23 -0
  73. package/src/components/audio/Playbutton.tsx +4 -5
  74. package/src/components.ts +10 -0
  75. package/src/controller/AIController.ts +84 -60
  76. package/src/controller/ObjectController.ts +4 -6
  77. package/src/controller/SettingsController.ts +10 -1
  78. package/src/controller/SharedContentController.ts +6 -6
  79. package/src/controller/SidePluginController.ts +36 -0
  80. package/src/controller/VoiceController.ts +1 -1
  81. package/src/core.ts +10 -0
  82. package/src/hooks/UseChatHook.ts +2 -2
  83. package/src/index.ts +4 -2
  84. package/src/plugin/PluginController.ts +46 -76
  85. package/src/plugin/RimoriClient.ts +151 -76
  86. package/src/plugin/fromRimori/EventBus.ts +301 -0
  87. package/src/plugin/fromRimori/readme.md +2 -0
  88. package/src/providers/PluginProvider.tsx +70 -14
  89. package/src/worker/WorkerSetup.ts +80 -0
  90. package/dist/CRUDModal.d.ts +0 -16
  91. package/dist/CRUDModal.js +0 -31
  92. package/dist/MarkdownEditor.d.ts +0 -8
  93. package/dist/MarkdownEditor.js +0 -46
  94. package/dist/audio/Playbutton.d.ts +0 -14
  95. package/dist/audio/Playbutton.js +0 -73
  96. package/dist/components/hooks/UseChatHook.d.ts +0 -15
  97. package/dist/components/hooks/UseChatHook.js +0 -21
  98. package/dist/plugin/AIController copy.d.ts +0 -22
  99. package/dist/plugin/AIController copy.js +0 -68
  100. package/dist/plugin/AIController.d.ts +0 -22
  101. package/dist/plugin/AIController.js +0 -68
  102. package/dist/plugin/ObjectController.d.ts +0 -34
  103. package/dist/plugin/ObjectController.js +0 -77
  104. package/dist/plugin/SettingController.d.ts +0 -13
  105. package/dist/plugin/SettingController.js +0 -55
  106. package/dist/plugin/VoiceController.d.ts +0 -2
  107. package/dist/providers/EventEmitter.d.ts +0 -11
  108. package/dist/providers/EventEmitter.js +0 -41
  109. package/dist/providers/EventEmitterContext.d.ts +0 -6
  110. package/dist/providers/EventEmitterContext.js +0 -19
  111. package/dist/utils/DifficultyConverter.d.ts +0 -3
  112. package/dist/utils/DifficultyConverter.js +0 -7
  113. package/dist/utils/constants.d.ts +0 -4
  114. package/dist/utils/constants.js +0 -12
  115. package/dist/utils/plugin/Client.d.ts +0 -72
  116. package/dist/utils/plugin/Client.js +0 -118
  117. package/dist/utils/plugin/PluginController.d.ts +0 -36
  118. package/dist/utils/plugin/PluginController.js +0 -119
  119. package/dist/utils/plugin/PluginUtils.d.ts +0 -2
  120. package/dist/utils/plugin/PluginUtils.js +0 -23
  121. package/dist/utils/plugin/RimoriClient.d.ts +0 -72
  122. package/dist/utils/plugin/RimoriClient.js +0 -118
  123. package/dist/utils/plugin/ThemeSetter.d.ts +0 -1
  124. package/dist/utils/plugin/ThemeSetter.js +0 -13
  125. package/dist/utils/plugin/WhereClauseBuilder.d.ts +0 -24
  126. package/dist/utils/plugin/WhereClauseBuilder.js +0 -79
  127. package/dist/utils/plugin/providers/EventEmitter.d.ts +0 -11
  128. package/dist/utils/plugin/providers/EventEmitter.js +0 -41
  129. package/dist/utils/plugin/providers/EventEmitterContext.d.ts +0 -6
  130. package/dist/utils/plugin/providers/EventEmitterContext.js +0 -19
  131. package/dist/utils/plugin/providers/PluginProvider.d.ts +0 -8
  132. package/dist/utils/plugin/providers/PluginProvider.js +0 -49
  133. package/src/providers/EventEmitter.ts +0 -48
  134. package/src/providers/EventEmitterContext.tsx +0 -27
  135. package/src/utils/constants.ts +0 -18
@@ -2,6 +2,7 @@ import React, { ReactNode } from 'react';
2
2
  import { RimoriClient } from '../plugin/RimoriClient';
3
3
  interface PluginProviderProps {
4
4
  children: ReactNode;
5
+ pluginId: string;
5
6
  }
6
7
  export declare const PluginProvider: React.FC<PluginProviderProps>;
7
8
  export declare const usePlugin: () => RimoriClient;
@@ -1,9 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext, useEffect, useState } from 'react';
3
3
  import { PluginController } from '../plugin/PluginController';
4
+ import { EventBusHandler } from '../plugin/fromRimori/EventBus';
5
+ EventBusHandler.getInstance("Plugin EventBus");
4
6
  const PluginContext = createContext(null);
5
- export const PluginProvider = ({ children }) => {
7
+ export const PluginProvider = ({ children, pluginId }) => {
6
8
  const [plugin, setPlugin] = useState(null);
9
+ const [contextMenuOnSelect, setContextMenuOnTextSelection] = useState(false);
7
10
  //route change
8
11
  useEffect(() => {
9
12
  let lastHash = window.location.hash;
@@ -11,33 +14,82 @@ export const PluginProvider = ({ children }) => {
11
14
  if (lastHash !== window.location.hash) {
12
15
  lastHash = window.location.hash;
13
16
  console.log('url changed:', lastHash);
14
- plugin === null || plugin === void 0 ? void 0 : plugin.emit('urlChange', window.location.hash);
17
+ plugin === null || plugin === void 0 ? void 0 : plugin.event.emit('session.triggerUrlChange', window.location.hash);
15
18
  }
16
19
  }, 100);
17
- PluginController.getInstance().then(setPlugin);
20
+ PluginController.getInstance(pluginId).then(setPlugin);
18
21
  }, []);
22
+ //check if context menu opens on text selection
23
+ useEffect(() => {
24
+ if (!plugin)
25
+ return;
26
+ plugin.plugin.getUserInfo().then((userInfo) => {
27
+ setContextMenuOnTextSelection(userInfo.contextMenuOnSelect);
28
+ }).catch(error => {
29
+ console.error('Error fetching settings:', error);
30
+ });
31
+ }, [plugin]);
32
+ //detect page height change
33
+ useEffect(() => {
34
+ const body = document.body;
35
+ const handleResize = () => plugin === null || plugin === void 0 ? void 0 : plugin.event.emit('session.triggerHeightChange', body.clientHeight);
36
+ body.addEventListener('resize', handleResize);
37
+ handleResize();
38
+ return () => body.removeEventListener('resize', handleResize);
39
+ }, [plugin]);
19
40
  //context menu
20
41
  useEffect(() => {
21
- let isOpen = false;
42
+ let lastMouseX = 0;
43
+ let lastMouseY = 0;
44
+ let isSelecting = false;
45
+ // Track mouse position
46
+ const handleMouseMove = (e) => {
47
+ lastMouseX = e.clientX;
48
+ lastMouseY = e.clientY;
49
+ };
22
50
  const handleContextMenu = (e) => {
23
51
  var _a;
24
52
  const selection = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString().trim();
25
53
  if (selection) {
26
54
  e.preventDefault();
27
- // console.log('context menu', selection);
28
- plugin === null || plugin === void 0 ? void 0 : plugin.emit('contextMenu', { text: selection, x: e.clientX, y: e.clientY, open: true });
29
- isOpen = true;
55
+ // console.log('context menu handled', selection);
56
+ plugin === null || plugin === void 0 ? void 0 : plugin.event.emit('global.contextMenu.trigger', { text: selection, x: e.clientX, y: e.clientY, open: true });
57
+ }
58
+ };
59
+ const handleSelectionChange = () => {
60
+ var _a;
61
+ // if (triggerOnTextSelection) {
62
+ const selection = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString().trim();
63
+ const open = !!selection && isSelecting;
64
+ // console.log('Selection change, contextMenuOnSelect:', contextMenuOnSelect);
65
+ plugin === null || plugin === void 0 ? void 0 : plugin.event.emit('global.contextMenu.trigger', { text: selection, x: lastMouseX, y: lastMouseY, open });
66
+ // }
67
+ };
68
+ const handleMouseUpDown = (e) => {
69
+ if (e.type === 'mousedown') {
70
+ isSelecting = false;
71
+ }
72
+ else if (e.type === 'mouseup') {
73
+ isSelecting = true;
74
+ // console.log('mouseup, contextMenuOnSelect:', contextMenuOnSelect);
75
+ if (contextMenuOnSelect) {
76
+ handleSelectionChange();
77
+ }
30
78
  }
31
79
  };
32
- // Hide the menu on click outside
33
- const handleClick = () => isOpen && (plugin === null || plugin === void 0 ? void 0 : plugin.emit('contextMenu', { text: '', x: 0, y: 0, open: false }));
34
- document.addEventListener("click", handleClick);
35
80
  document.addEventListener('contextmenu', handleContextMenu);
81
+ document.addEventListener('selectionchange', handleSelectionChange);
82
+ document.addEventListener("mousemove", handleMouseMove);
83
+ document.addEventListener('mousedown', handleMouseUpDown);
84
+ document.addEventListener('mouseup', handleMouseUpDown);
36
85
  return () => {
37
- document.removeEventListener("click", handleClick);
86
+ document.removeEventListener("mousemove", handleMouseMove);
38
87
  document.removeEventListener('contextmenu', handleContextMenu);
88
+ document.removeEventListener('selectionchange', handleSelectionChange);
89
+ document.removeEventListener('mousedown', handleMouseUpDown);
90
+ document.removeEventListener('mouseup', handleMouseUpDown);
39
91
  };
40
- }, [plugin]);
92
+ }, [plugin, contextMenuOnSelect]);
41
93
  if (!plugin) {
42
94
  return "";
43
95
  }
@@ -0,0 +1,6 @@
1
+ import { RimoriClient } from "../plugin/RimoriClient";
2
+ /**
3
+ * Sets up the web worker for the plugin to be able receive and send messages to Rimori.
4
+ * @param init - The function containing the subscription logic.
5
+ */
6
+ export declare function setupWorker(init: (controller: RimoriClient) => void | Promise<void>): void;
@@ -0,0 +1,79 @@
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 { PluginController } from "../plugin/PluginController";
11
+ import { EventBus, EventBusHandler } from "../plugin/fromRimori/EventBus";
12
+ let controller = null;
13
+ const listeners = [];
14
+ let debugEnabled = false;
15
+ /**
16
+ * Sets up the web worker for the plugin to be able receive and send messages to Rimori.
17
+ * @param init - The function containing the subscription logic.
18
+ */
19
+ export function setupWorker(init) {
20
+ // Mock of the window object for the worker context to be able to use the PluginController.
21
+ const mockWindow = {
22
+ location: { search: '?secret=123' },
23
+ parent: {
24
+ postMessage: (message) => {
25
+ message.event.sender = "worker." + message.event.sender;
26
+ checkDebugMode(message.event);
27
+ logIfDebug('[Worker] sending event to Rimori', message.event);
28
+ self.postMessage(message);
29
+ }
30
+ },
31
+ addEventListener: (_, listener) => {
32
+ listeners.push(listener);
33
+ },
34
+ APP_CONFIG: {
35
+ SUPABASE_URL: 'NOT_SET',
36
+ SUPABASE_ANON_KEY: 'NOT_SET',
37
+ },
38
+ };
39
+ // Assign the mock to globalThis.
40
+ Object.assign(globalThis, { window: mockWindow });
41
+ EventBusHandler.getInstance("Worker EventBus");
42
+ // Handle init message from Rimori.
43
+ self.onmessage = (response) => __awaiter(this, void 0, void 0, function* () {
44
+ checkDebugMode(response.data);
45
+ logIfDebug('[Worker] message received', response.data);
46
+ const event = response.data;
47
+ if (event.topic === 'global.worker.requestInit') {
48
+ if (!controller) {
49
+ mockWindow.APP_CONFIG.SUPABASE_URL = event.data.supabaseUrl;
50
+ mockWindow.APP_CONFIG.SUPABASE_ANON_KEY = event.data.supabaseAnonKey;
51
+ controller = yield PluginController.getInstance(event.data.pluginId);
52
+ logIfDebug('[Worker] Worker initialized.');
53
+ yield init(controller);
54
+ logIfDebug('[Worker] Plugin listeners initialized.');
55
+ }
56
+ const initEvent = {
57
+ timestamp: new Date().toISOString(),
58
+ eventId: event.eventId,
59
+ sender: "worker." + event.sender,
60
+ topic: 'global.worker.requestInit',
61
+ data: { success: true },
62
+ debug: debugEnabled
63
+ };
64
+ return self.postMessage({ secret: "123", event: initEvent });
65
+ }
66
+ listeners.forEach(listener => listener({ data: { event: response.data, secret: "123" } }));
67
+ });
68
+ }
69
+ function checkDebugMode(event) {
70
+ if (event.topic === 'global.system.requestDebug' || event.debug) {
71
+ debugEnabled = true;
72
+ EventBus.emit("worker", "global.system.requestDebug");
73
+ }
74
+ }
75
+ function logIfDebug(...args) {
76
+ if (debugEnabled) {
77
+ console.debug('[Worker] ' + args[0], ...args.slice(1));
78
+ }
79
+ }
package/package.json CHANGED
@@ -1,8 +1,22 @@
1
1
  {
2
2
  "name": "@rimori/client",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/index.js"
10
+ },
11
+ "./core": {
12
+ "types": "./dist/core.d.ts",
13
+ "default": "./dist/core.js"
14
+ },
15
+ "./components": {
16
+ "types": "./dist/components.d.ts",
17
+ "default": "./dist/components.js"
18
+ }
19
+ },
6
20
  "scripts": {
7
21
  "build": "tsc && sass src/style.scss:dist/style.css",
8
22
  "dev": "tsc -w",
@@ -16,7 +30,6 @@
16
30
  "@supabase/supabase-js": "^2.48.1",
17
31
  "@tiptap/react": "2.10.3",
18
32
  "@tiptap/starter-kit": "2.10.3",
19
- "ibridge-flex": "0.0.1",
20
33
  "uuid": "11.1.0",
21
34
  "react-icons": "^5.4.0",
22
35
  "tiptap-markdown": "^0.8.10",
@@ -27,4 +40,4 @@
27
40
  "@types/react-dom": "^18.0.1",
28
41
  "sass": "^1.82.0"
29
42
  }
30
- }
43
+ }
@@ -1,6 +1,4 @@
1
- "use client";
2
-
3
- import { useEffect, useState, useRef } from "react";
1
+ import { useEffect, useRef } from "react";
4
2
 
5
3
  interface Props {
6
4
  title: string;
@@ -0,0 +1,96 @@
1
+ import React, { useEffect, useMemo } from 'react';
2
+ import { CircleAudioAvatar } from './EmbeddedAssistent/CircleAudioAvatar';
3
+ import { AudioInputField } from './EmbeddedAssistent/AudioInputField';
4
+ import { MessageSender } from './EmbeddedAssistent/TTS/MessageSender';
5
+ import Markdown from 'react-markdown';
6
+ import { useChat } from '../../hooks/UseChatHook';
7
+ import { usePlugin } from '../../components';
8
+ import { FirstMessages, getFirstMessages } from './utils';
9
+
10
+ interface Props {
11
+ voiceId: any;
12
+ avatarImageUrl: string;
13
+ onComplete: (result: any) => void;
14
+ autoStartConversation?: FirstMessages;
15
+ }
16
+
17
+ export function AssistantChat({ avatarImageUrl, voiceId, onComplete, autoStartConversation }: Props) {
18
+ const [oralCommunication, setOralCommunication] = React.useState(true);
19
+ const { llm, event } = usePlugin();
20
+ const sender = useMemo(() => new MessageSender(llm.getVoice, voiceId), []);
21
+ const { messages, append, isLoading, setMessages } = useChat();
22
+
23
+ const lastAssistantMessage = [...messages].filter((m) => m.role === 'assistant').pop()?.content;
24
+
25
+ useEffect(() => {
26
+ sender.setOnLoudnessChange((value: number) => event.emit('self.avatar.triggerLoudness', value));
27
+
28
+ if (!autoStartConversation) {
29
+ return;
30
+ }
31
+
32
+ setMessages(getFirstMessages(autoStartConversation));
33
+ // append([{ role: 'user', content: autoStartConversation.userMessage }]);
34
+
35
+ if (autoStartConversation.assistantMessage) {
36
+ // console.log("autostartmessages", { autoStartConversation, isLoading });
37
+ sender.handleNewText(autoStartConversation.assistantMessage, isLoading);
38
+ }
39
+ }, []);
40
+
41
+ useEffect(() => {
42
+ let message = lastAssistantMessage;
43
+ if (message !== messages[messages.length - 1]?.content) {
44
+ message = undefined;
45
+ }
46
+ sender.handleNewText(message, isLoading);
47
+ }, [messages, isLoading]);
48
+
49
+ const lastMessage = messages[messages.length - 1];
50
+
51
+ useEffect(() => {
52
+ console.log("lastMessage", lastMessage);
53
+ const toolInvocations = lastMessage?.toolInvocations;
54
+ if (toolInvocations && toolInvocations.length > 0) {
55
+ console.log("toolInvocations", toolInvocations);
56
+ onComplete(toolInvocations[0].args);
57
+ }
58
+ }, [lastMessage]);
59
+
60
+ if (lastMessage?.toolInvocations && lastMessage.toolInvocations.length > 0) {
61
+ console.log("lastMessage test2", lastMessage);
62
+ const args = lastMessage.toolInvocations[0].args;
63
+
64
+ const success = args.explanationUnderstood === "TRUE" || args.studentKnowsTopic === "TRUE";
65
+
66
+ return <div className="px-5 pt-5 overflow-y-auto text-center" style={{ height: "478px" }}>
67
+ <h1 className='text-center mt-5 mb-5'>
68
+ {success ? "Great job!" : "You failed"}
69
+ </h1>
70
+ <p>{args.improvementHints}</p>
71
+ </div>
72
+ }
73
+
74
+ return (
75
+ <div>
76
+ {oralCommunication && <CircleAudioAvatar imageUrl={avatarImageUrl} className='mx-auto my-10' />}
77
+ <div className="w-full">
78
+ {lastAssistantMessage && <div className="px-5 pt-5 overflow-y-auto remirror-theme" style={{ height: "4k78px" }}>
79
+ <Markdown>{lastAssistantMessage}</Markdown>
80
+ </div>}
81
+ </div>
82
+ <AudioInputField
83
+ blockSubmission={isLoading}
84
+ onSubmit={message => {
85
+ append([{ role: 'user', content: message, id: messages.length.toString() }]);
86
+ }}
87
+ onAudioControl={voice => {
88
+ setOralCommunication(voice);
89
+ sender.setVolume(voice ? 1 : 0);
90
+ }} />
91
+ </div>
92
+ );
93
+ };
94
+
95
+
96
+
@@ -0,0 +1,61 @@
1
+ import { Tool } from '../../core';
2
+ import { useEffect, useMemo } from 'react';
3
+ import { VoiceRecorder } from './EmbeddedAssistent/VoiceRecoder';
4
+ import { MessageSender } from './EmbeddedAssistent/TTS/MessageSender';
5
+ import { CircleAudioAvatar } from './EmbeddedAssistent/CircleAudioAvatar';
6
+ import { useChat } from '../../hooks/UseChatHook';
7
+ import { usePlugin } from '../../components';
8
+ import { getFirstMessages } from './utils';
9
+ import { FirstMessages } from './utils';
10
+
11
+ interface Props {
12
+ title?: string;
13
+ voiceId: any;
14
+ avatarImageUrl: string;
15
+ agentTools: Tool[];
16
+ autoStartConversation?: FirstMessages;
17
+ }
18
+
19
+ export function Avatar({ avatarImageUrl, voiceId, title, agentTools, autoStartConversation }: Props) {
20
+ const { llm, event } = usePlugin();
21
+ const sender = useMemo(() => new MessageSender(llm.getVoice, voiceId), []);
22
+ const { messages, append, isLoading, lastMessage, setMessages } = useChat(agentTools);
23
+
24
+ useEffect(() => {
25
+ console.log("messages", messages);
26
+ }, [messages]);
27
+
28
+ useEffect(() => {
29
+ sender.setOnLoudnessChange((value: number) => event.emit('self.avatar.triggerLoudness', value));
30
+
31
+ if (!autoStartConversation) return;
32
+
33
+ setMessages(getFirstMessages(autoStartConversation));
34
+ // append([{ role: 'user', content: autoStartConversation.userMessage }]);
35
+
36
+ if (autoStartConversation.assistantMessage) {
37
+ // console.log("autostartmessages", { autoStartConversation, isLoading });
38
+ sender.handleNewText(autoStartConversation.assistantMessage, isLoading);
39
+ } else if (autoStartConversation.userMessage) {
40
+ append([{ role: 'user', content: autoStartConversation.userMessage, id: messages.length.toString() }]);
41
+ }
42
+ }, []);
43
+
44
+ useEffect(() => {
45
+ if (lastMessage?.role === 'assistant') {
46
+ sender.handleNewText(lastMessage.content, isLoading);
47
+ }
48
+ }, [lastMessage, isLoading]);
49
+
50
+ return (
51
+ <div className='pb-8'>
52
+ {title && <p className="text-center mt-5 w-3/4 mx-auto rounded-lg dark:text-gray-100">{title}</p>}
53
+ <CircleAudioAvatar imageUrl={avatarImageUrl} width={"250px"} className='mx-auto' />
54
+ <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'>
55
+ <VoiceRecorder className='w-7' iconSize='300' onVoiceRecorded={(message) => {
56
+ append([{ role: 'user', content: "Message(" + Math.floor((messages.length + 1) / 2) + "): " + message, id: messages.length.toString() }]);
57
+ }} />
58
+ </div>
59
+ </div>
60
+ );
61
+ };
@@ -0,0 +1,64 @@
1
+ import React, { useState } from 'react';
2
+ import { VoiceRecorder } from './VoiceRecoder';
3
+ import { BiSolidRightArrow } from "react-icons/bi";
4
+ import { HiMiniSpeakerXMark, HiMiniSpeakerWave } from "react-icons/hi2";
5
+
6
+ interface AudioInputFieldProps {
7
+ onSubmit: (text: string) => void;
8
+ onAudioControl?: (voice: boolean) => void;
9
+ blockSubmission?: boolean;
10
+ }
11
+
12
+ export function AudioInputField({ onSubmit, onAudioControl, blockSubmission = false }: AudioInputFieldProps) {
13
+ const [text, setText] = useState('');
14
+ const [audioEnabled, setAudioEnabled] = useState(true);
15
+
16
+ const handleSubmit = (manualText?: string) => {
17
+ if (blockSubmission) return;
18
+ const sendableText = manualText || text;
19
+ if (sendableText.trim()) {
20
+ onSubmit(sendableText);
21
+ setTimeout(() => {
22
+ setText('');
23
+ }, 100);
24
+ }
25
+ };
26
+
27
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
28
+ if (blockSubmission) return;
29
+ if (e.key === 'Enter' && e.ctrlKey) {
30
+ setText(text + '\n');
31
+ } else if (e.key === 'Enter') {
32
+ handleSubmit();
33
+ }
34
+ };
35
+
36
+ return (
37
+ <div className="flex items-center bg-gray-600 pt-2 pb-2 p-2">
38
+ {onAudioControl && <button
39
+ onClick={() => {
40
+ onAudioControl(!audioEnabled);
41
+ setAudioEnabled(!audioEnabled);
42
+ }}
43
+ className="cursor-default">
44
+ {audioEnabled ? <HiMiniSpeakerWave className='w-9 h-9 cursor-pointer' /> : <HiMiniSpeakerXMark className='w-9 h-9 cursor-pointer' />}
45
+ </button>}
46
+ <VoiceRecorder onVoiceRecorded={(m: string) => {
47
+ console.log('onVoiceRecorded', m);
48
+ handleSubmit(m);
49
+ }}
50
+ />
51
+ <textarea
52
+ value={text}
53
+ onChange={(e) => setText(e.target.value)}
54
+ onKeyDown={handleKeyDown}
55
+ className="flex-1 border-none rounded-lg p-2 text-gray-800 focus::outline-none"
56
+ placeholder='Type a message...'
57
+ disabled={blockSubmission}
58
+ />
59
+ <button onClick={() => handleSubmit()} className="cursor-default" disabled={blockSubmission}>
60
+ <BiSolidRightArrow className='w-9 h-10 cursor-pointer' />
61
+ </button>
62
+ </div>
63
+ );
64
+ };
@@ -0,0 +1,75 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { EventBus, EventBusMessage } from '../../../core';
3
+
4
+ interface CircleAudioAvatarProps {
5
+ width?: string;
6
+ imageUrl: string;
7
+ className?: string;
8
+ }
9
+
10
+
11
+ export function CircleAudioAvatar({ imageUrl, className, width = "150px" }: CircleAudioAvatarProps) {
12
+ const canvasRef = useRef<HTMLCanvasElement>(null);
13
+
14
+ useEffect(() => {
15
+ const canvas = canvasRef.current;
16
+ if (canvas) {
17
+ const ctx = canvas.getContext('2d');
18
+ if (ctx) {
19
+ const image = new Image();
20
+ image.src = imageUrl;
21
+ image.onload = () => {
22
+ draw(ctx, canvas, image, 0);
23
+ };
24
+
25
+ const handleLoudness = (event: EventBusMessage) => {
26
+ draw(ctx, canvas, image, event.data.loudness);
27
+ };
28
+
29
+ // Subscribe to loudness changes
30
+ const listenerId = EventBus.on('self.avatar.triggerLoudness', handleLoudness);
31
+
32
+ return () => {
33
+ EventBus.off(listenerId);
34
+ };
35
+ }
36
+ }
37
+ }, [imageUrl]);
38
+
39
+ // Function to draw on the canvas
40
+ const draw = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, image: HTMLImageElement, loudness: number) => {
41
+ if (canvas && ctx) {
42
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
43
+
44
+ // Draw pulsing circle
45
+ const radius = Math.min(canvas.width, canvas.height) / 3;
46
+ const centerX = canvas.width / 2;
47
+ const centerY = canvas.height / 2;
48
+ const pulseRadius = radius + loudness / 2.5; // Adjust the divisor for sensitivity
49
+ ctx.beginPath();
50
+ ctx.arc(centerX, centerY, pulseRadius, 0, Math.PI * 2, true);
51
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
52
+ ctx.lineWidth = 5;
53
+ ctx.stroke();
54
+
55
+ // Draw image circle
56
+ ctx.save();
57
+ ctx.beginPath();
58
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2, true);
59
+ ctx.closePath();
60
+ ctx.clip();
61
+ ctx.drawImage(image, centerX - radius, centerY - radius, radius * 2, radius * 2);
62
+ ctx.restore();
63
+
64
+ // Draw circular frame around the image
65
+ ctx.beginPath();
66
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2, true);
67
+ ctx.strokeStyle = 'rgba(20,20, 20, 0.9)';
68
+ ctx.lineWidth = 5; // Adjust the width of the frame as needed
69
+ ctx.stroke();
70
+ }
71
+ };
72
+
73
+ return <canvas ref={canvasRef} className={className} width={500} height={500} style={{ width }} />;
74
+ };
75
+
@@ -0,0 +1,91 @@
1
+ import { ChunkedAudioPlayer } from './Player';
2
+
3
+ type VoiceBackend = (text: string, voice?: string, speed?: number) => Promise<Blob>;
4
+
5
+ export class MessageSender {
6
+ private player = new ChunkedAudioPlayer();
7
+ private fetchedSentences = new Set<string>();
8
+ private lastLoading = false;
9
+ private voice: string;
10
+ private model: string;
11
+ private voiceBackend: VoiceBackend;
12
+
13
+ constructor(voiceBackend: VoiceBackend, voice: string = 'alloy', model = 'openai') {
14
+ this.voiceBackend = voiceBackend;
15
+ this.voice = voice;
16
+ this.model = model;
17
+ }
18
+
19
+ private getCompletedSentences(currentText: string, isLoading: boolean): string[] {
20
+ // Split the text based on the following characters: .,?!
21
+ // Only split on : when followed by a space
22
+ const pattern = /(.+?[,.?!]|.+?:\s+|.+?\n+)/g;
23
+ const result: string[] = [];
24
+ let match;
25
+ while ((match = pattern.exec(currentText)) !== null) {
26
+ const sentence = match[0].trim();
27
+ if (sentence.length > 0) {
28
+ result.push(sentence);
29
+ }
30
+ }
31
+ if (!isLoading) {
32
+ const lastFullSentence = result[result.length - 1];
33
+ const leftoverIndex = currentText.lastIndexOf(lastFullSentence) + lastFullSentence.length;
34
+ if (leftoverIndex < currentText.length) {
35
+ result.push(currentText.slice(leftoverIndex).trim());
36
+ }
37
+ }
38
+ return result;
39
+ }
40
+
41
+ public async handleNewText(currentText: string | undefined, isLoading: boolean) {
42
+ if (!this.lastLoading && isLoading) {
43
+ this.reset();
44
+ }
45
+ this.lastLoading = isLoading;
46
+
47
+ if (!currentText) {
48
+ return;
49
+ }
50
+
51
+ const sentences = this.getCompletedSentences(currentText, isLoading);
52
+
53
+ for (let i = 0; i < sentences.length; i++) {
54
+ const sentence = sentences[i];
55
+ if (!this.fetchedSentences.has(sentence)) {
56
+ this.fetchedSentences.add(sentence);
57
+ const audioData = await this.generateSpeech(sentence);
58
+ await this.player.addChunk(audioData, i);
59
+ }
60
+ }
61
+ }
62
+
63
+ private async generateSpeech(sentence: string): Promise<ArrayBuffer> {
64
+ const blob = await this.voiceBackend(sentence, this.voice, 1.0);
65
+ return await blob.arrayBuffer();
66
+ }
67
+
68
+ public play() {
69
+ this.player.playAgain();
70
+ }
71
+
72
+ public stop() {
73
+ this.player.stopPlayback();
74
+ }
75
+
76
+ private reset() {
77
+ this.stop();
78
+ this.fetchedSentences.clear();
79
+ this.player.reset();
80
+ }
81
+
82
+ public setVolume(volume: number) {
83
+ this.player.setVolume(volume);
84
+ }
85
+
86
+ public setOnLoudnessChange(callback: (value: number) => void) {
87
+ this.player.setOnLoudnessChange((loudness) => {
88
+ callback(loudness);
89
+ });
90
+ }
91
+ }