@rimori/client 1.1.10 → 1.3.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 (171) hide show
  1. package/README.md +189 -63
  2. package/dist/cli/scripts/init/dev-registration.d.ts +35 -0
  3. package/dist/cli/scripts/init/dev-registration.js +174 -0
  4. package/dist/cli/scripts/init/env-setup.d.ts +9 -0
  5. package/dist/cli/scripts/init/env-setup.js +43 -0
  6. package/dist/cli/scripts/init/file-operations.d.ts +4 -0
  7. package/dist/cli/scripts/init/file-operations.js +51 -0
  8. package/dist/cli/scripts/init/html-cleaner.d.ts +4 -0
  9. package/dist/cli/scripts/init/html-cleaner.js +38 -0
  10. package/dist/cli/scripts/init/main.d.ts +2 -0
  11. package/dist/cli/scripts/init/main.js +160 -0
  12. package/dist/cli/scripts/init/package-setup.d.ts +32 -0
  13. package/dist/cli/scripts/init/package-setup.js +75 -0
  14. package/dist/cli/scripts/init/router-transformer.d.ts +6 -0
  15. package/dist/cli/scripts/init/router-transformer.js +254 -0
  16. package/dist/cli/scripts/init/tailwind-config.d.ts +4 -0
  17. package/dist/cli/scripts/init/tailwind-config.js +56 -0
  18. package/dist/cli/scripts/init/vite-config.d.ts +20 -0
  19. package/dist/cli/scripts/init/vite-config.js +54 -0
  20. package/dist/cli/scripts/release/release-config-upload.d.ts +7 -0
  21. package/dist/cli/scripts/release/release-config-upload.js +116 -0
  22. package/dist/cli/scripts/release/release-db-update.d.ts +6 -0
  23. package/dist/cli/scripts/release/release-db-update.js +100 -0
  24. package/dist/cli/scripts/release/release-file-upload.d.ts +6 -0
  25. package/dist/cli/scripts/release/release-file-upload.js +136 -0
  26. package/dist/cli/scripts/release/release.d.ts +23 -0
  27. package/dist/cli/scripts/release/release.js +70 -0
  28. package/dist/cli/types/DatabaseTypes.d.ts +103 -0
  29. package/dist/cli/types/DatabaseTypes.js +2 -0
  30. package/dist/components/LoggerExample.d.ts +6 -0
  31. package/dist/components/LoggerExample.js +79 -0
  32. package/dist/components/ai/Assistant.js +5 -5
  33. package/dist/components/ai/Avatar.d.ts +3 -2
  34. package/dist/components/ai/Avatar.js +11 -6
  35. package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.js +1 -1
  36. package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.d.ts +1 -0
  37. package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.js +48 -33
  38. package/dist/components/ai/utils.js +0 -1
  39. package/dist/components/audio/Playbutton.js +4 -4
  40. package/dist/{core → components}/components/ContextMenu.js +50 -11
  41. package/dist/components.d.ts +5 -5
  42. package/dist/components.js +5 -5
  43. package/dist/core/controller/AIController.d.ts +15 -0
  44. package/dist/core/controller/AIController.js +253 -0
  45. package/dist/core/controller/AudioController.d.ts +0 -0
  46. package/dist/core/controller/AudioController.js +1 -0
  47. package/dist/{controller → core/controller}/ObjectController.d.ts +10 -2
  48. package/dist/{controller → core/controller}/ObjectController.js +8 -8
  49. package/dist/{controller → core/controller}/SettingsController.d.ts +28 -4
  50. package/dist/{controller → core/controller}/SettingsController.js +0 -25
  51. package/dist/{controller → core/controller}/SharedContentController.d.ts +31 -3
  52. package/dist/{controller → core/controller}/SharedContentController.js +77 -26
  53. package/dist/core/controller/VoiceController.d.ts +9 -0
  54. package/dist/{controller → core/controller}/VoiceController.js +11 -4
  55. package/dist/core/core.d.ts +14 -0
  56. package/dist/core/core.js +8 -0
  57. package/dist/{plugin/fromRimori → fromRimori}/EventBus.d.ts +3 -3
  58. package/dist/{plugin/fromRimori → fromRimori}/EventBus.js +26 -9
  59. package/dist/fromRimori/PluginTypes.d.ts +174 -0
  60. package/dist/hooks/UseChatHook.d.ts +2 -1
  61. package/dist/hooks/UseChatHook.js +6 -4
  62. package/dist/hooks/UseLogger.d.ts +30 -0
  63. package/dist/hooks/UseLogger.js +122 -0
  64. package/dist/index.d.ts +6 -3
  65. package/dist/index.js +5 -3
  66. package/dist/plugin/AccomplishmentHandler.d.ts +1 -1
  67. package/dist/plugin/AccomplishmentHandler.js +1 -1
  68. package/dist/plugin/AudioController.d.ts +37 -0
  69. package/dist/plugin/AudioController.js +68 -0
  70. package/dist/plugin/Logger.d.ts +68 -0
  71. package/dist/plugin/Logger.js +256 -0
  72. package/dist/plugin/LoggerExample.d.ts +16 -0
  73. package/dist/plugin/LoggerExample.js +140 -0
  74. package/dist/plugin/PluginController.d.ts +30 -5
  75. package/dist/plugin/PluginController.js +182 -53
  76. package/dist/plugin/RimoriClient.d.ts +68 -21
  77. package/dist/plugin/RimoriClient.js +88 -41
  78. package/dist/plugin/StandaloneClient.d.ts +1 -0
  79. package/dist/plugin/StandaloneClient.js +24 -10
  80. package/dist/plugin/ThemeSetter.d.ts +2 -1
  81. package/dist/plugin/ThemeSetter.js +13 -7
  82. package/dist/providers/PluginProvider.d.ts +4 -1
  83. package/dist/providers/PluginProvider.js +39 -13
  84. package/dist/utils/Language.d.ts +2 -1
  85. package/dist/utils/Language.js +4 -2
  86. package/dist/utils/audioFormats.d.ts +26 -0
  87. package/dist/utils/audioFormats.js +67 -0
  88. package/dist/utils/difficultyConverter.js +1 -1
  89. package/dist/utils/endpoint.d.ts +2 -0
  90. package/dist/utils/endpoint.js +2 -0
  91. package/dist/worker/WorkerSetup.d.ts +3 -2
  92. package/dist/worker/WorkerSetup.js +22 -65
  93. package/example/docs/devdocs.md +231 -0
  94. package/example/docs/overview.md +29 -0
  95. package/example/docs/userdocs.md +123 -0
  96. package/example/rimori.config.ts +89 -0
  97. package/example/worker/vite.config.ts +23 -0
  98. package/example/worker/worker.ts +11 -0
  99. package/package.json +16 -9
  100. package/src/cli/scripts/init/dev-registration.ts +192 -0
  101. package/src/cli/scripts/init/env-setup.ts +44 -0
  102. package/src/cli/scripts/init/file-operations.ts +58 -0
  103. package/src/cli/scripts/init/html-cleaner.ts +48 -0
  104. package/src/cli/scripts/init/main.ts +172 -0
  105. package/src/cli/scripts/init/package-setup.ts +117 -0
  106. package/src/cli/scripts/init/router-transformer.ts +329 -0
  107. package/src/cli/scripts/init/tailwind-config.ts +75 -0
  108. package/src/cli/scripts/init/vite-config.ts +73 -0
  109. package/src/cli/scripts/release/release-config-upload.ts +114 -0
  110. package/src/cli/scripts/release/release-db-update.ts +97 -0
  111. package/src/cli/scripts/release/release-file-upload.ts +138 -0
  112. package/src/cli/scripts/release/release.ts +69 -0
  113. package/src/cli/types/DatabaseTypes.ts +117 -0
  114. package/src/components/ai/Assistant.tsx +5 -5
  115. package/src/components/ai/Avatar.tsx +25 -8
  116. package/src/components/ai/EmbeddedAssistent/CircleAudioAvatar.tsx +1 -1
  117. package/src/components/ai/EmbeddedAssistent/VoiceRecoder.tsx +50 -35
  118. package/src/components/ai/utils.ts +0 -2
  119. package/src/components/audio/Playbutton.tsx +4 -4
  120. package/src/{core → components}/components/ContextMenu.tsx +56 -12
  121. package/src/components.ts +6 -6
  122. package/src/core/controller/AIController.ts +283 -0
  123. package/src/core/controller/ObjectController.ts +115 -0
  124. package/src/{controller → core/controller}/SettingsController.ts +29 -29
  125. package/src/{controller → core/controller}/SharedContentController.ts +91 -29
  126. package/src/core/controller/VoiceController.ts +31 -0
  127. package/src/core/core.ts +16 -0
  128. package/src/{plugin/fromRimori → fromRimori}/EventBus.ts +29 -11
  129. package/src/fromRimori/PluginTypes.ts +205 -0
  130. package/src/hooks/UseChatHook.ts +8 -5
  131. package/src/index.ts +6 -3
  132. package/src/plugin/AccomplishmentHandler.ts +1 -1
  133. package/src/plugin/AudioController.ts +58 -0
  134. package/src/plugin/Logger.ts +324 -0
  135. package/src/plugin/PluginController.ts +203 -63
  136. package/src/plugin/RimoriClient.ts +127 -55
  137. package/src/plugin/StandaloneClient.ts +30 -11
  138. package/src/plugin/ThemeSetter.ts +16 -9
  139. package/src/providers/PluginProvider.tsx +46 -13
  140. package/src/utils/Language.ts +4 -2
  141. package/src/utils/difficultyConverter.ts +3 -3
  142. package/src/utils/endpoint.ts +2 -0
  143. package/src/worker/WorkerSetup.ts +13 -60
  144. package/dist/components/PluginController.d.ts +0 -21
  145. package/dist/components/PluginController.js +0 -116
  146. package/dist/controller/AIController.d.ts +0 -23
  147. package/dist/controller/AIController.js +0 -93
  148. package/dist/controller/SidePluginController.d.ts +0 -3
  149. package/dist/controller/SidePluginController.js +0 -31
  150. package/dist/controller/VoiceController.d.ts +0 -10
  151. package/dist/core.d.ts +0 -7
  152. package/dist/core.js +0 -7
  153. package/dist/plugin/ContextMenu.d.ts +0 -17
  154. package/dist/plugin/ContextMenu.js +0 -45
  155. package/dist/plugin/fromRimori/PluginTypes.d.ts +0 -48
  156. package/dist/plugin/fromRimori/SupabaseHandler.d.ts +0 -13
  157. package/dist/plugin/fromRimori/SupabaseHandler.js +0 -55
  158. package/dist/providers/PluginController.d.ts +0 -21
  159. package/dist/providers/PluginController.js +0 -116
  160. package/dist/types/Actions.d.ts +0 -4
  161. package/dist/types/Actions.js +0 -1
  162. package/src/controller/AIController.ts +0 -112
  163. package/src/controller/ObjectController.ts +0 -107
  164. package/src/controller/SidePluginController.ts +0 -25
  165. package/src/controller/VoiceController.ts +0 -26
  166. package/src/core.ts +0 -8
  167. package/src/plugin/fromRimori/PluginTypes.ts +0 -64
  168. package/src/types/Actions.ts +0 -6
  169. /package/dist/{core → components}/components/ContextMenu.d.ts +0 -0
  170. /package/dist/{plugin/fromRimori → fromRimori}/PluginTypes.js +0 -0
  171. /package/src/{plugin/fromRimori → fromRimori}/readme.md +0 -0
@@ -1,7 +1,7 @@
1
1
  import React, { useState, useEffect, useRef } from "react";
2
- import { EventBus } from "../../plugin/fromRimori/EventBus";
2
+ import { EventBus } from "../../fromRimori/EventBus";
3
3
  import { RimoriClient } from "../../plugin/RimoriClient";
4
- import { MenuEntry } from "../../plugin/fromRimori/PluginTypes";
4
+ import { MenuEntry } from "../../fromRimori/PluginTypes";
5
5
 
6
6
  export interface Position {
7
7
  x: number,
@@ -14,28 +14,64 @@ const ContextMenu = ({ client }: { client: RimoriClient }) => {
14
14
  const [actions, setActions] = useState<MenuEntry[]>([]);
15
15
  const [position, setPosition] = useState<Position>({ x: 0, y: 0 });
16
16
  const [openOnTextSelect, setOpenOnTextSelect] = useState(false);
17
+ const [menuWidth, setMenuWidth] = useState<number>(0);
17
18
  const menuRef = useRef<HTMLDivElement>(null);
19
+ const isMobile = window.innerWidth < 768;
20
+
21
+ /**
22
+ * Calculates position for mobile context menu based on selected text bounds.
23
+ * Centers the menu horizontally over the selected text and positions it 30px below the text's end.
24
+ * @param selectedText - The currently selected text
25
+ * @param menuWidth - The width of the menu to center properly
26
+ * @returns Position object with x and y coordinates
27
+ */
28
+ const calculateMobilePosition = (selectedText: string, menuWidth: number = 0): Position => {
29
+ const selection = window.getSelection();
30
+ if (!selection || !selectedText) {
31
+ return { x: 0, y: 0, text: selectedText };
32
+ }
33
+
34
+ const range = selection.getRangeAt(0);
35
+ const rect = range.getBoundingClientRect();
36
+
37
+ // Center horizontally over the selected text, accounting for menu width
38
+ const centerX = rect.left + (rect.width / 2) - (menuWidth / 2);
39
+
40
+ // Position 12px below where the text ends vertically
41
+ const textEndY = rect.bottom + 12;
42
+
43
+ return { x: centerX, y: textEndY, text: selectedText };
44
+ };
18
45
 
19
46
  useEffect(() => {
20
- client.plugin.getInstalled().then(plugins => {
21
- setActions(plugins.flatMap(p => p.context_menu_actions).filter(Boolean));
22
- });
47
+ const actions = client.plugin.getPluginInfo().installedPlugins.flatMap(p => p.context_menu_actions).filter(Boolean);
48
+ setActions(actions);
49
+ setOpenOnTextSelect(client.plugin.getUserInfo().context_menu_on_select);
23
50
 
24
- client.plugin.getUserInfo().then((userInfo) => {
25
- setOpenOnTextSelect(userInfo.context_menu_on_select);
26
- })
27
51
 
28
52
  EventBus.on<{ actions: MenuEntry[] }>("global.contextMenu.createActions", ({ data }) => {
29
53
  setActions([...data.actions, ...actions]);
30
54
  });
31
55
  }, []);
32
56
 
57
+ // Update menu width when menu is rendered
58
+ useEffect(() => {
59
+ if (isOpen && menuRef.current) {
60
+ setMenuWidth(menuRef.current.offsetWidth);
61
+ }
62
+ }, [isOpen, actions]);
63
+
33
64
  useEffect(() => {
34
65
  // Track mouse position globally
35
66
  const handleMouseMove = (e: MouseEvent) => {
36
67
  const selectedText = window.getSelection()?.toString().trim();
37
68
  if (isOpen && selectedText === position.text) return;
38
- setPosition({ x: e.clientX, y: e.clientY, text: selectedText });
69
+
70
+ if (isMobile && selectedText) {
71
+ setPosition(calculateMobilePosition(selectedText, menuWidth));
72
+ } else {
73
+ setPosition({ x: e.clientX, y: e.clientY, text: selectedText });
74
+ }
39
75
  };
40
76
 
41
77
  const handleMouseUp = (e: MouseEvent) => {
@@ -64,18 +100,26 @@ const ContextMenu = ({ client }: { client: RimoriClient }) => {
64
100
  if (e.button === 2) {
65
101
  e.preventDefault();
66
102
  }
67
- setPosition({ x: e.clientX, y: e.clientY, text: selectedText });
103
+
104
+ if (isMobile) {
105
+ setPosition(calculateMobilePosition(selectedText, menuWidth));
106
+ } else {
107
+ setPosition({ x: e.clientX, y: e.clientY, text: selectedText });
108
+ }
68
109
  setIsOpen(true);
69
110
  } else {
70
111
  setIsOpen(false);
71
112
  }
72
113
  };
73
114
 
74
- // Add selectionchange listener to close menu if selection is cleared
115
+ // Add selectionchange listener to close menu if selection is cleared and update position for mobile
75
116
  const handleSelectionChange = () => {
76
117
  const selectedText = window.getSelection()?.toString().trim();
77
118
  if (!selectedText && isOpen) {
78
119
  setIsOpen(false);
120
+ } else if (selectedText && isOpen && isMobile) {
121
+ // Update position in real-time as text selection changes on mobile
122
+ setPosition(calculateMobilePosition(selectedText, menuWidth));
79
123
  }
80
124
  };
81
125
 
@@ -105,7 +149,7 @@ const ContextMenu = ({ client }: { client: RimoriClient }) => {
105
149
  <MenuEntryItem key={index} icon={action.icon} text={action.text} onClick={() => {
106
150
  setIsOpen(false);
107
151
  window.getSelection()?.removeAllRanges();
108
- client.event.emitSidebarAction(action.pluginId, action.actionKey, position.text);
152
+ client.event.emitSidebarAction(action.plugin_id, action.action_key, position.text);
109
153
  }} />
110
154
  ))}
111
155
  </div>
package/src/components.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  // React components and hooks exports
2
- export * from "./components/MarkdownEditor";
2
+ export * from "./components/ai/Assistant";
3
+ export * from "./components/ai/Avatar";
4
+ export * from "./components/ai/EmbeddedAssistent/VoiceRecoder";
5
+ export * from "./components/audio/Playbutton";
3
6
  export * from "./components/CRUDModal";
7
+ export * from "./components/MarkdownEditor";
4
8
  export * from "./components/Spinner";
5
- export * from "./components/audio/Playbutton";
6
9
  export * from "./hooks/UseChatHook";
7
10
  export * from "./plugin/ThemeSetter";
8
- export * from "./providers/PluginProvider";
9
- export * from "./components/ai/Avatar";
10
- export * from "./components/ai/Assistant";
11
- export * from "./types/Actions";
11
+ export * from "./providers/PluginProvider";
@@ -0,0 +1,283 @@
1
+ import { Tool } from "../../fromRimori/PluginTypes";
2
+
3
+ export interface ToolInvocation {
4
+ toolCallId: string;
5
+ toolName: string;
6
+ args: Record<string, string>;
7
+ }
8
+
9
+ export interface Message {
10
+ id?: string;
11
+ role: "user" | "assistant" | "system"
12
+ content: string;
13
+ toolCalls?: ToolInvocation[];
14
+ }
15
+
16
+ export async function generateText(backendUrl: string, messages: Message[], tools: Tool[], token: string) {
17
+ const response = await fetch(`${backendUrl}/ai/llm`, {
18
+ method: 'POST',
19
+ body: JSON.stringify({ messages, tools }),
20
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
21
+ });
22
+
23
+ return await response.json();
24
+ }
25
+
26
+ export type OnLLMResponse = (id: string, response: string, finished: boolean, toolInvocations?: ToolInvocation[]) => void;
27
+
28
+ export async function streamChatGPT(backendUrl: string, messages: Message[], tools: Tool[], onResponse: OnLLMResponse, token: string) {
29
+ const messageId = Math.random().toString(36).substring(3);
30
+ let currentMessages: Message[] = [...messages];
31
+
32
+ console.log('Starting streamChatGPT with:', {
33
+ messageId,
34
+ messageCount: messages.length,
35
+ toolCount: tools.length,
36
+ backendUrl
37
+ });
38
+
39
+ while (true) {
40
+ const messagesForApi = currentMessages.map(({ id, ...rest }) => rest);
41
+
42
+ try {
43
+ const response = await fetch(`${backendUrl}/ai/llm`, {
44
+ method: 'POST',
45
+ body: JSON.stringify({ messages: messagesForApi, tools, stream: true }),
46
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
47
+ });
48
+
49
+ if (!response.ok) {
50
+ throw new Error(`HTTP error! status: ${response.status}`);
51
+ }
52
+
53
+ if (!response.body) {
54
+ console.error('No response body.');
55
+ return;
56
+ }
57
+
58
+ const reader = response.body.getReader();
59
+ const decoder = new TextDecoder('utf-8');
60
+
61
+ let content = "";
62
+ let done = false;
63
+ let toolInvocations: { toolCallId: string, toolName: string, args: any }[] = [];
64
+ let currentTextId = "";
65
+ let isToolCallMode = false;
66
+ let buffer = ""; // Buffer for incomplete chunks
67
+
68
+ while (!done) {
69
+ const { value, done: readerDone } = await reader.read();
70
+
71
+ if (value) {
72
+ const chunk = decoder.decode(value, { stream: true });
73
+ buffer += chunk;
74
+
75
+ // Split by lines, but handle incomplete lines
76
+ const lines = buffer.split('\n');
77
+
78
+ // Keep the last line in buffer if it's incomplete
79
+ if (lines.length > 1) {
80
+ buffer = lines.pop() || "";
81
+ }
82
+
83
+ for (const line of lines) {
84
+ if (line.trim() === '') continue;
85
+
86
+ // Handle the new streaming format
87
+ if (line.startsWith('data: ')) {
88
+ const dataStr = line.substring(6); // Remove 'data: ' prefix
89
+
90
+ // Handle [DONE] marker
91
+ if (dataStr === '[DONE]') {
92
+ done = true;
93
+ break;
94
+ }
95
+
96
+ try {
97
+ const data = JSON.parse(dataStr);
98
+
99
+ // Log the first message to understand the format
100
+ if (!content && !isToolCallMode) {
101
+ console.log('First stream message received:', data);
102
+ }
103
+
104
+ switch (data.type) {
105
+ case 'start':
106
+ // Stream started, no action needed
107
+ console.log('Stream started');
108
+ break;
109
+
110
+ case 'start-step':
111
+ // Step started, no action needed
112
+ console.log('Step started');
113
+ break;
114
+
115
+ case 'reasoning-start':
116
+ // Reasoning started, no action needed
117
+ console.log('Reasoning started:', data.id);
118
+ break;
119
+
120
+ case 'reasoning-end':
121
+ // Reasoning ended, no action needed
122
+ console.log('Reasoning ended:', data.id);
123
+ break;
124
+
125
+ case 'text-start':
126
+ // Text generation started, store the ID
127
+ currentTextId = data.id;
128
+ console.log('Text generation started:', data.id);
129
+ break;
130
+
131
+ case 'text-delta':
132
+ // Text delta received, append to content
133
+ if (data.delta) {
134
+ content += data.delta;
135
+ onResponse(messageId, content, false);
136
+ }
137
+ break;
138
+
139
+ case 'text-end':
140
+ // Text generation ended
141
+ console.log('Text generation ended:', data.id);
142
+ break;
143
+
144
+ case 'finish-step':
145
+ // Step finished, no action needed
146
+ console.log('Step finished');
147
+ break;
148
+
149
+ case 'finish':
150
+ // Stream finished
151
+ console.log('Stream finished');
152
+ done = true;
153
+ break;
154
+
155
+ // Additional message types that might be present in the AI library
156
+ case 'tool-call':
157
+ // Tool call initiated
158
+ console.log('Tool call initiated:', data);
159
+ isToolCallMode = true;
160
+ if (data.toolCallId && data.toolName && data.args) {
161
+ toolInvocations.push({
162
+ toolCallId: data.toolCallId,
163
+ toolName: data.toolName,
164
+ args: data.args
165
+ });
166
+ }
167
+ break;
168
+
169
+ case 'tool-call-delta':
170
+ // Tool call delta (for streaming tool calls)
171
+ console.log('Tool call delta:', data);
172
+ break;
173
+
174
+ case 'tool-call-end':
175
+ // Tool call completed
176
+ console.log('Tool call completed:', data);
177
+ break;
178
+
179
+ case 'tool-result':
180
+ // Tool execution result
181
+ console.log('Tool result:', data);
182
+ break;
183
+
184
+ case 'error':
185
+ // Error occurred
186
+ console.error('Stream error:', data);
187
+ break;
188
+
189
+ case 'usage':
190
+ // Usage information
191
+ console.log('Usage info:', data);
192
+ break;
193
+
194
+ case 'model':
195
+ // Model information
196
+ console.log('Model info:', data);
197
+ break;
198
+
199
+ case 'stop':
200
+ // Stop signal
201
+ console.log('Stop signal received');
202
+ done = true;
203
+ break;
204
+
205
+ default:
206
+ // Unknown type, log for debugging
207
+ console.log('Unknown stream type:', data.type, data);
208
+ break;
209
+ }
210
+ } catch (error) {
211
+ console.error('Error parsing stream data:', error, dataStr);
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ if (readerDone) {
218
+ done = true;
219
+ }
220
+ }
221
+
222
+ // Check if we have content or if this was a tool call response
223
+ if (content || toolInvocations.length > 0) {
224
+ currentMessages.push({
225
+ id: messageId,
226
+ role: "assistant",
227
+ content: content,
228
+ toolCalls: toolInvocations.length > 0 ? toolInvocations: undefined,
229
+ });
230
+ }
231
+
232
+ // Handle tool call scenario if tools were provided
233
+ if (tools.length > 0 && toolInvocations.length > 0) {
234
+ console.log('Tool calls detected, executing tools...');
235
+
236
+ const toolResults: Message[] = [];
237
+ for (const toolInvocation of toolInvocations) {
238
+ const tool = tools.find(t => t.name === toolInvocation.toolName);
239
+ if (tool && tool.execute) {
240
+ try {
241
+ const result = await tool.execute(toolInvocation.args);
242
+ toolResults.push({
243
+ id: Math.random().toString(36).substring(3),
244
+ role: "user",
245
+ content: `Tool '${toolInvocation.toolName}' returned: ${JSON.stringify(result)}`,
246
+ });
247
+ } catch (error) {
248
+ console.error(`Error executing tool ${toolInvocation.toolName}:`, error);
249
+ toolResults.push({
250
+ id: Math.random().toString(36).substring(3),
251
+ role: "user",
252
+ content: `Tool '${toolInvocation.toolName}' failed with error: ${error}`,
253
+ });
254
+ }
255
+ }
256
+ }
257
+
258
+ if (toolResults.length > 0) {
259
+ currentMessages.push(...toolResults);
260
+ // Continue the loop to handle the next response
261
+ continue;
262
+ }
263
+ }
264
+
265
+ // Since the new format doesn't seem to support tool calls in the same way,
266
+ // we'll assume the stream is complete when we reach the end
267
+ // If tools are provided and no content was generated, this might indicate a tool call
268
+ if (tools.length > 0 && !content && !isToolCallMode) {
269
+ // This might be a tool call scenario, but we need more information
270
+ // For now, we'll just finish the stream
271
+ console.log('No content generated, but tools provided - might be tool call scenario');
272
+ }
273
+
274
+ onResponse(messageId, content, true, toolInvocations);
275
+ return;
276
+
277
+ } catch (error) {
278
+ console.error('Error in streamChatGPT:', error);
279
+ onResponse(messageId, `Error: ${error instanceof Error ? error.message : String(error)}`, true, []);
280
+ return;
281
+ }
282
+ }
283
+ }
@@ -0,0 +1,115 @@
1
+ type PrimitiveType = 'string' | 'number' | 'boolean';
2
+
3
+ // This is the type that can appear in the `type` property
4
+ type ObjectToolParameterType =
5
+ | PrimitiveType
6
+ | { [key: string]: ObjectToolParameter } // for nested objects
7
+ | [{ [key: string]: ObjectToolParameter }]; // for arrays of objects (notice the tuple type)
8
+
9
+ interface ObjectToolParameter {
10
+ type: ObjectToolParameterType;
11
+ description?: string;
12
+ enum?: string[];
13
+ optional?: boolean;
14
+ }
15
+
16
+ /**
17
+ * The tools that the AI can use.
18
+ *
19
+ * The key is the name of the tool.
20
+ * The value is the parameter of the tool.
21
+ *
22
+ */
23
+ export type ObjectTool = {
24
+ [key: string]: ObjectToolParameter;
25
+ };
26
+
27
+ export interface ObjectRequest {
28
+ /**
29
+ * The tools that the AI can use.
30
+ */
31
+ tool: ObjectTool;
32
+ /**
33
+ * High level instructions for the AI to follow. Behaviour, tone, restrictions, etc.
34
+ * Example: "Act like a recipe writer."
35
+ */
36
+ behaviour?: string;
37
+ /**
38
+ * The specific instruction for the AI to follow.
39
+ * Example: "Generate a recipe using chicken, rice and vegetables."
40
+ */
41
+ instructions: string;
42
+ }
43
+
44
+ export async function generateObject(backendUrl: string, request: ObjectRequest, token: string) {
45
+ return await fetch(`${backendUrl}/ai/llm-object`, {
46
+ method: 'POST',
47
+ body: JSON.stringify({
48
+ stream: false,
49
+ tool: request.tool,
50
+ behaviour: request.behaviour,
51
+ instructions: request.instructions,
52
+ }),
53
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
54
+ }).then(response => response.json());
55
+ }
56
+
57
+ // TODO adjust stream to work with object
58
+ export type OnLLMResponse = (id: string, response: string, finished: boolean, toolInvocations?: any[]) => void;
59
+
60
+ export async function streamObject(backendUrl: string, request: ObjectRequest, onResponse: OnLLMResponse, token: string) {
61
+ const messageId = Math.random().toString(36).substring(3);
62
+ const response = await fetch(`${backendUrl}/ai/llm-object`, {
63
+ method: 'POST',
64
+ body: JSON.stringify({
65
+ stream: true,
66
+ tools: request.tool,
67
+ systemInstructions: request.behaviour,
68
+ secondaryInstructions: request.instructions,
69
+ }),
70
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
71
+ });
72
+
73
+ if (!response.body) {
74
+ console.error('No response body.');
75
+ return;
76
+ }
77
+
78
+ const reader = response.body.getReader();
79
+ const decoder = new TextDecoder('utf-8');
80
+
81
+ let content = "";
82
+ let done = false;
83
+ let toolInvocations: any[] = [];
84
+ while (!done) {
85
+ const { value } = await reader.read();
86
+
87
+ if (value) {
88
+ const chunk = decoder.decode(value, { stream: true });
89
+ const lines = chunk.split('\n').filter(line => line.trim() !== '');
90
+
91
+ for (const line of lines) {
92
+ const data = line.substring(3, line.length - 1);
93
+ const command = line.substring(0, 1);
94
+ // console.log("data: ", { line, data, command });
95
+
96
+ if (command === '0') {
97
+ content += data;
98
+ // console.log("AI response:", content);
99
+
100
+ //content \n\n should be real line break when message is displayed
101
+ onResponse(messageId, content.replace(/\\n/g, '\n').replace(/\\+"/g, '"'), false);
102
+ } else if (command === 'd') {
103
+ // console.log("AI usage:", JSON.parse(line.substring(2)));
104
+ done = true;
105
+ break;
106
+ } else if (command === '9') {
107
+ // console.log("tool call:", JSON.parse(line.substring(2)));
108
+ // console.log("tools", tools);
109
+ toolInvocations.push(JSON.parse(line.substring(2)));
110
+ }
111
+ }
112
+ }
113
+ }
114
+ onResponse(messageId, content.replace(/\\n/g, '\n').replace(/\\+"/g, '"'), true, toolInvocations);
115
+ }
@@ -1,6 +1,15 @@
1
1
  import { SupabaseClient } from "@supabase/supabase-js";
2
- import { LanguageLevel } from "../utils/difficultyConverter";
3
- import { Language } from "../utils/Language";
2
+ import { LanguageLevel } from "../../utils/difficultyConverter";
3
+ import { Language } from "../../utils/Language";
4
+
5
+ export interface Buddy {
6
+ id: string;
7
+ name: string;
8
+ description: string;
9
+ avatarUrl: string;
10
+ voiceId: string;
11
+ aiPersonality: string;
12
+ }
4
13
 
5
14
  export interface UserInfo {
6
15
  skill_level_reading: LanguageLevel;
@@ -11,13 +20,30 @@ export interface UserInfo {
11
20
  skill_level_understanding: LanguageLevel;
12
21
  goal_longterm: string;
13
22
  goal_weekly: string;
14
- study_buddy: string;
23
+ study_buddy: Buddy;
15
24
  story_genre: string;
16
25
  study_duration: number;
26
+ /**
27
+ * The 2 letter language code of the language the user speaks natively.
28
+ * With the function getLanguageName, the language name can be retrieved.
29
+ */
17
30
  mother_tongue: Language;
31
+ /**
32
+ * The language the user targets to learn.
33
+ */
34
+ target_language: Language;
18
35
  motivation_type: string;
19
36
  onboarding_completed: boolean;
20
37
  context_menu_on_select: boolean;
38
+ user_name?: string;
39
+ /**
40
+ * ISO 3166-1 alpha-2 country code of user's location (exposed to plugins)
41
+ */
42
+ location_country: string;
43
+ /**
44
+ * Optional: nearest big city (>100,000) near user's location
45
+ */
46
+ location_city?: string;
21
47
  }
22
48
 
23
49
  export class SettingsController {
@@ -43,32 +69,6 @@ export class SettingsController {
43
69
  await this.supabase.from("plugin_settings").upsert({ plugin_id: this.pluginId, settings });
44
70
  }
45
71
 
46
- public async getUserInfo(): Promise<UserInfo> {
47
- const { data } = await this.supabase.from("profiles").select("*");
48
-
49
- if (!data || data.length === 0) {
50
- return {
51
- mother_tongue: "en",
52
- skill_level_listening: "Pre-A1",
53
- skill_level_reading: "Pre-A1",
54
- skill_level_speaking: "Pre-A1",
55
- skill_level_writing: "Pre-A1",
56
- skill_level_understanding: "Pre-A1",
57
- skill_level_grammar: "Pre-A1",
58
- goal_longterm: "",
59
- goal_weekly: "",
60
- study_buddy: "clarence",
61
- story_genre: "adventure",
62
- study_duration: 30,
63
- motivation_type: "self-motivated",
64
- onboarding_completed: false,
65
- context_menu_on_select: false,
66
- }
67
- }
68
-
69
- return data[0].settings;
70
- }
71
-
72
72
  /**
73
73
  * Get the settings for the plugin. T can be any type of settings, UserSettings or SystemSettings.
74
74
  * @param defaultSettings The default settings to use if no settings are found.