@rimori/client 1.4.0 → 1.4.3

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 (133) hide show
  1. package/README.md +77 -71
  2. package/dist/cli/scripts/init/dev-registration.d.ts +1 -1
  3. package/dist/cli/scripts/init/dev-registration.js +4 -4
  4. package/dist/cli/scripts/init/main.js +1 -1
  5. package/dist/cli/scripts/init/package-setup.d.ts +1 -1
  6. package/dist/cli/scripts/init/package-setup.js +3 -3
  7. package/dist/cli/scripts/init/router-transformer.js +19 -12
  8. package/dist/cli/scripts/init/vite-config.d.ts +2 -2
  9. package/dist/cli/scripts/init/vite-config.js +2 -2
  10. package/dist/cli/scripts/release/release-config-upload.js +9 -9
  11. package/dist/cli/scripts/release/release-db-update.d.ts +1 -1
  12. package/dist/cli/scripts/release/release-db-update.js +9 -9
  13. package/dist/cli/scripts/release/release-file-upload.js +1 -1
  14. package/dist/cli/scripts/release/release.js +2 -2
  15. package/dist/components/CRUDModal.d.ts +1 -1
  16. package/dist/components/CRUDModal.js +3 -3
  17. package/dist/components/MarkdownEditor.js +16 -16
  18. package/dist/components/Spinner.js +2 -2
  19. package/dist/components/ai/Assistant.js +7 -8
  20. package/dist/components/ai/Avatar.d.ts +2 -2
  21. package/dist/components/ai/Avatar.js +10 -5
  22. package/dist/components/ai/EmbeddedAssistent/AudioInputField.js +5 -6
  23. package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.d.ts +1 -1
  24. package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.js +1 -2
  25. package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.d.ts +1 -2
  26. package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.js +4 -2
  27. package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.js +2 -3
  28. package/dist/components/audio/Playbutton.js +10 -7
  29. package/dist/components/components/ContextMenu.d.ts +1 -1
  30. package/dist/components/components/ContextMenu.js +19 -16
  31. package/dist/components.d.ts +10 -10
  32. package/dist/components.js +10 -10
  33. package/dist/core/controller/AIController.d.ts +2 -2
  34. package/dist/core/controller/AIController.js +12 -12
  35. package/dist/core/controller/ExerciseController.d.ts +2 -2
  36. package/dist/core/controller/ExerciseController.js +2 -2
  37. package/dist/core/controller/ObjectController.js +5 -5
  38. package/dist/core/controller/SettingsController.d.ts +22 -7
  39. package/dist/core/controller/SettingsController.js +73 -8
  40. package/dist/core/controller/SharedContentController.d.ts +3 -3
  41. package/dist/core/controller/SharedContentController.js +38 -20
  42. package/dist/core/controller/VoiceController.js +6 -4
  43. package/dist/core/core.d.ts +15 -15
  44. package/dist/core/core.js +7 -7
  45. package/dist/fromRimori/EventBus.js +23 -23
  46. package/dist/fromRimori/PluginTypes.d.ts +4 -4
  47. package/dist/hooks/UseChatHook.d.ts +3 -3
  48. package/dist/hooks/UseChatHook.js +9 -3
  49. package/dist/index.d.ts +10 -10
  50. package/dist/index.js +9 -9
  51. package/dist/plugin/AccomplishmentHandler.d.ts +5 -5
  52. package/dist/plugin/AccomplishmentHandler.js +31 -27
  53. package/dist/plugin/AudioController.d.ts +1 -1
  54. package/dist/plugin/AudioController.js +6 -6
  55. package/dist/plugin/Logger.js +15 -13
  56. package/dist/plugin/PluginController.d.ts +7 -1
  57. package/dist/plugin/PluginController.js +32 -27
  58. package/dist/plugin/RimoriClient.d.ts +17 -18
  59. package/dist/plugin/RimoriClient.js +31 -31
  60. package/dist/plugin/StandaloneClient.d.ts +1 -1
  61. package/dist/plugin/StandaloneClient.js +35 -16
  62. package/dist/plugin/ThemeSetter.js +4 -4
  63. package/dist/providers/PluginProvider.js +44 -14
  64. package/dist/utils/Language.js +57 -57
  65. package/dist/utils/PluginUtils.js +3 -3
  66. package/dist/utils/difficultyConverter.d.ts +1 -1
  67. package/dist/utils/difficultyConverter.js +1 -1
  68. package/dist/utils/endpoint.js +2 -2
  69. package/dist/worker/WorkerSetup.d.ts +1 -1
  70. package/dist/worker/WorkerSetup.js +6 -6
  71. package/example/docs/devdocs.md +50 -40
  72. package/example/docs/overview.md +1 -1
  73. package/example/docs/userdocs.md +4 -1
  74. package/example/rimori.config.ts +51 -49
  75. package/example/worker/vite.config.ts +3 -3
  76. package/example/worker/worker.ts +2 -2
  77. package/package.json +14 -8
  78. package/prettier.config.js +1 -1
  79. package/src/cli/scripts/init/dev-registration.ts +5 -8
  80. package/src/cli/scripts/init/env-setup.ts +1 -1
  81. package/src/cli/scripts/init/file-operations.ts +1 -1
  82. package/src/cli/scripts/init/html-cleaner.ts +2 -5
  83. package/src/cli/scripts/init/main.ts +16 -13
  84. package/src/cli/scripts/init/package-setup.ts +11 -15
  85. package/src/cli/scripts/init/router-transformer.ts +40 -37
  86. package/src/cli/scripts/init/tailwind-config.ts +17 -26
  87. package/src/cli/scripts/init/vite-config.ts +3 -3
  88. package/src/cli/scripts/release/release-config-upload.ts +11 -11
  89. package/src/cli/scripts/release/release-db-update.ts +12 -12
  90. package/src/cli/scripts/release/release-file-upload.ts +2 -2
  91. package/src/cli/scripts/release/release.ts +4 -4
  92. package/src/cli/types/DatabaseTypes.ts +2 -10
  93. package/src/components/CRUDModal.tsx +64 -48
  94. package/src/components/MarkdownEditor.tsx +58 -27
  95. package/src/components/Spinner.tsx +24 -17
  96. package/src/components/ai/Assistant.tsx +70 -70
  97. package/src/components/ai/Avatar.tsx +17 -14
  98. package/src/components/ai/EmbeddedAssistent/AudioInputField.tsx +63 -54
  99. package/src/components/ai/EmbeddedAssistent/CircleAudioAvatar.tsx +14 -5
  100. package/src/components/ai/EmbeddedAssistent/TTS/MessageSender.ts +75 -74
  101. package/src/components/ai/EmbeddedAssistent/TTS/Player.ts +3 -4
  102. package/src/components/ai/EmbeddedAssistent/VoiceRecoder.tsx +109 -94
  103. package/src/components/ai/utils.ts +4 -4
  104. package/src/components/audio/Playbutton.tsx +101 -93
  105. package/src/components/components/ContextMenu.tsx +47 -35
  106. package/src/components.ts +10 -10
  107. package/src/core/controller/AIController.ts +29 -19
  108. package/src/core/controller/ExerciseController.ts +16 -23
  109. package/src/core/controller/ObjectController.ts +15 -10
  110. package/src/core/controller/SettingsController.ts +89 -16
  111. package/src/core/controller/SharedContentController.ts +80 -44
  112. package/src/core/controller/VoiceController.ts +10 -8
  113. package/src/core/core.ts +15 -16
  114. package/src/fromRimori/EventBus.ts +76 -47
  115. package/src/fromRimori/PluginTypes.ts +26 -17
  116. package/src/fromRimori/readme.md +2 -2
  117. package/src/hooks/UseChatHook.ts +25 -15
  118. package/src/index.ts +10 -10
  119. package/src/plugin/AccomplishmentHandler.ts +53 -35
  120. package/src/plugin/AudioController.ts +18 -12
  121. package/src/plugin/Logger.ts +28 -21
  122. package/src/plugin/PluginController.ts +60 -44
  123. package/src/plugin/RimoriClient.ts +102 -72
  124. package/src/plugin/StandaloneClient.ts +51 -24
  125. package/src/plugin/ThemeSetter.ts +5 -5
  126. package/src/providers/PluginProvider.tsx +90 -36
  127. package/src/style.scss +3 -3
  128. package/src/utils/Language.ts +58 -58
  129. package/src/utils/PluginUtils.ts +16 -20
  130. package/src/utils/difficultyConverter.ts +2 -2
  131. package/src/utils/endpoint.ts +3 -2
  132. package/src/worker/WorkerSetup.ts +8 -9
  133. package/tsconfig.json +2 -4
@@ -1,4 +1,4 @@
1
- import { RimoriClient } from "../../plugin/RimoriClient";
1
+ import { RimoriClient } from '../../plugin/RimoriClient';
2
2
  export interface Position {
3
3
  x: number;
4
4
  y: number;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect, useRef } from "react";
3
- import { EventBus } from "../../fromRimori/EventBus";
2
+ import { useState, useEffect, useRef } from 'react';
3
+ import { EventBus } from '../../fromRimori/EventBus';
4
4
  const ContextMenu = ({ client }) => {
5
5
  const [isOpen, setIsOpen] = useState(false);
6
6
  const [actions, setActions] = useState([]);
@@ -24,16 +24,19 @@ const ContextMenu = ({ client }) => {
24
24
  const range = selection.getRangeAt(0);
25
25
  const rect = range.getBoundingClientRect();
26
26
  // Center horizontally over the selected text, accounting for menu width
27
- const centerX = rect.left + (rect.width / 2) - (menuWidth / 2);
27
+ const centerX = rect.left + rect.width / 2 - menuWidth / 2;
28
28
  // Position 12px below where the text ends vertically
29
29
  const textEndY = rect.bottom + 12;
30
30
  return { x: centerX, y: textEndY, text: selectedText };
31
31
  };
32
32
  useEffect(() => {
33
- const actions = client.plugin.getPluginInfo().installedPlugins.flatMap(p => p.context_menu_actions).filter(Boolean);
33
+ const actions = client.plugin
34
+ .getPluginInfo()
35
+ .installedPlugins.flatMap((p) => p.context_menu_actions)
36
+ .filter(Boolean);
34
37
  setActions(actions);
35
38
  setOpenOnTextSelect(client.plugin.getUserInfo().context_menu_on_select);
36
- EventBus.on("global.contextMenu.createActions", ({ data }) => {
39
+ EventBus.on('global.contextMenu.createActions', ({ data }) => {
37
40
  setActions([...data.actions, ...actions]);
38
41
  });
39
42
  }, []);
@@ -67,8 +70,8 @@ const ContextMenu = ({ client }) => {
67
70
  }
68
71
  // Prevent context menu on textarea or text input selection
69
72
  const target = e.target;
70
- const isTextInput = target && ((target.tagName === 'TEXTAREA') ||
71
- (target.tagName === 'INPUT' && target.type === 'text'));
73
+ const isTextInput = target &&
74
+ (target.tagName === 'TEXTAREA' || (target.tagName === 'INPUT' && target.type === 'text'));
72
75
  if (isTextInput) {
73
76
  setIsOpen(false);
74
77
  return;
@@ -105,15 +108,15 @@ const ContextMenu = ({ client }) => {
105
108
  setPosition(calculateMobilePosition(selectedText, menuWidth));
106
109
  }
107
110
  };
108
- document.addEventListener("mouseup", handleMouseUp);
109
- window.addEventListener("mousemove", handleMouseMove);
110
- document.addEventListener("contextmenu", handleMouseUp);
111
- document.addEventListener("selectionchange", handleSelectionChange);
111
+ document.addEventListener('mouseup', handleMouseUp);
112
+ window.addEventListener('mousemove', handleMouseMove);
113
+ document.addEventListener('contextmenu', handleMouseUp);
114
+ document.addEventListener('selectionchange', handleSelectionChange);
112
115
  return () => {
113
- document.removeEventListener("mouseup", handleMouseUp);
114
- window.removeEventListener("mousemove", handleMouseMove);
115
- document.removeEventListener("contextmenu", handleMouseUp);
116
- document.removeEventListener("selectionchange", handleSelectionChange);
116
+ document.removeEventListener('mouseup', handleMouseUp);
117
+ window.removeEventListener('mousemove', handleMouseMove);
118
+ document.removeEventListener('contextmenu', handleMouseUp);
119
+ document.removeEventListener('selectionchange', handleSelectionChange);
117
120
  };
118
121
  }, [openOnTextSelect, isOpen, position.text]);
119
122
  if (!isOpen) {
@@ -127,6 +130,6 @@ const ContextMenu = ({ client }) => {
127
130
  } }, index))) }));
128
131
  };
129
132
  function MenuEntryItem(props) {
130
- return _jsxs("button", { onClick: props.onClick, className: "px-4 py-2 text-left hover:bg-gray-500 dark:hover:bg-gray-600 w-full flex flex-row", children: [_jsx("span", { className: "flex-grow", children: props.icon }), _jsx("span", { className: "flex-grow", children: props.text })] });
133
+ return (_jsxs("button", { onClick: props.onClick, className: "px-4 py-2 text-left hover:bg-gray-500 dark:hover:bg-gray-600 w-full flex flex-row", children: [_jsx("span", { className: "flex-grow", children: props.icon }), _jsx("span", { className: "flex-grow", children: props.text })] }));
131
134
  }
132
135
  export default ContextMenu;
@@ -1,10 +1,10 @@
1
- export * from "./components/ai/Assistant";
2
- export * from "./components/ai/Avatar";
3
- export * from "./components/ai/EmbeddedAssistent/VoiceRecoder";
4
- export * from "./components/audio/Playbutton";
5
- export * from "./components/CRUDModal";
6
- export * from "./components/MarkdownEditor";
7
- export * from "./components/Spinner";
8
- export * from "./hooks/UseChatHook";
9
- export * from "./plugin/ThemeSetter";
10
- export * from "./providers/PluginProvider";
1
+ export * from './components/ai/Assistant';
2
+ export * from './components/ai/Avatar';
3
+ export * from './components/ai/EmbeddedAssistent/VoiceRecoder';
4
+ export * from './components/audio/Playbutton';
5
+ export * from './components/CRUDModal';
6
+ export * from './components/MarkdownEditor';
7
+ export * from './components/Spinner';
8
+ export * from './hooks/UseChatHook';
9
+ export * from './plugin/ThemeSetter';
10
+ export * from './providers/PluginProvider';
@@ -1,11 +1,11 @@
1
1
  // React components and hooks exports
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";
6
- export * from "./components/CRUDModal";
7
- export * from "./components/MarkdownEditor";
8
- export * from "./components/Spinner";
9
- export * from "./hooks/UseChatHook";
10
- export * from "./plugin/ThemeSetter";
11
- export * from "./providers/PluginProvider";
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';
6
+ export * from './components/CRUDModal';
7
+ export * from './components/MarkdownEditor';
8
+ export * from './components/Spinner';
9
+ export * from './hooks/UseChatHook';
10
+ export * from './plugin/ThemeSetter';
11
+ export * from './providers/PluginProvider';
@@ -1,4 +1,4 @@
1
- import { Tool } from "../../fromRimori/PluginTypes";
1
+ import { Tool } from '../../fromRimori/PluginTypes';
2
2
  export interface ToolInvocation {
3
3
  toolCallId: string;
4
4
  toolName: string;
@@ -6,7 +6,7 @@ export interface ToolInvocation {
6
6
  }
7
7
  export interface Message {
8
8
  id?: string;
9
- role: "user" | "assistant" | "system";
9
+ role: 'user' | 'assistant' | 'system';
10
10
  content: string;
11
11
  toolCalls?: ToolInvocation[];
12
12
  }
@@ -23,7 +23,7 @@ export function generateText(backendUrl, messages, tools, token) {
23
23
  const response = yield fetch(`${backendUrl}/ai/llm`, {
24
24
  method: 'POST',
25
25
  body: JSON.stringify({ messages, tools }),
26
- headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
26
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
27
27
  });
28
28
  return yield response.json();
29
29
  });
@@ -36,7 +36,7 @@ export function streamChatGPT(backendUrl, messages, tools, onResponse, token) {
36
36
  messageId,
37
37
  messageCount: messages.length,
38
38
  toolCount: tools.length,
39
- backendUrl
39
+ backendUrl,
40
40
  });
41
41
  while (true) {
42
42
  const messagesForApi = currentMessages.map((_a) => {
@@ -47,7 +47,7 @@ export function streamChatGPT(backendUrl, messages, tools, onResponse, token) {
47
47
  const response = yield fetch(`${backendUrl}/ai/llm`, {
48
48
  method: 'POST',
49
49
  body: JSON.stringify({ messages: messagesForApi, tools, stream: true }),
50
- headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
50
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
51
51
  });
52
52
  if (!response.ok) {
53
53
  throw new Error(`HTTP error! status: ${response.status}`);
@@ -58,12 +58,12 @@ export function streamChatGPT(backendUrl, messages, tools, onResponse, token) {
58
58
  }
59
59
  const reader = response.body.getReader();
60
60
  const decoder = new TextDecoder('utf-8');
61
- let content = "";
61
+ let content = '';
62
62
  let done = false;
63
63
  let toolInvocations = [];
64
- let currentTextId = "";
64
+ let currentTextId = '';
65
65
  let isToolCallMode = false;
66
- let buffer = ""; // Buffer for incomplete chunks
66
+ let buffer = ''; // Buffer for incomplete chunks
67
67
  while (!done) {
68
68
  const { value, done: readerDone } = yield reader.read();
69
69
  if (value) {
@@ -73,7 +73,7 @@ export function streamChatGPT(backendUrl, messages, tools, onResponse, token) {
73
73
  const lines = buffer.split('\n');
74
74
  // Keep the last line in buffer if it's incomplete
75
75
  if (lines.length > 1) {
76
- buffer = lines.pop() || "";
76
+ buffer = lines.pop() || '';
77
77
  }
78
78
  for (const line of lines) {
79
79
  if (line.trim() === '')
@@ -144,7 +144,7 @@ export function streamChatGPT(backendUrl, messages, tools, onResponse, token) {
144
144
  toolInvocations.push({
145
145
  toolCallId: data.toolCallId,
146
146
  toolName: data.toolName,
147
- args: data.args || data.input
147
+ args: data.args || data.input,
148
148
  });
149
149
  }
150
150
  break;
@@ -198,7 +198,7 @@ export function streamChatGPT(backendUrl, messages, tools, onResponse, token) {
198
198
  if (content || toolInvocations.length > 0) {
199
199
  currentMessages.push({
200
200
  id: messageId,
201
- role: "assistant",
201
+ role: 'assistant',
202
202
  content: content,
203
203
  toolCalls: toolInvocations.length > 0 ? toolInvocations : undefined,
204
204
  });
@@ -208,13 +208,13 @@ export function streamChatGPT(backendUrl, messages, tools, onResponse, token) {
208
208
  console.log('Tool calls detected, executing tools...');
209
209
  const toolResults = [];
210
210
  for (const toolInvocation of toolInvocations) {
211
- const tool = tools.find(t => t.name === toolInvocation.toolName);
211
+ const tool = tools.find((t) => t.name === toolInvocation.toolName);
212
212
  if (tool && tool.execute) {
213
213
  try {
214
214
  const result = yield tool.execute(toolInvocation.args);
215
215
  toolResults.push({
216
216
  id: Math.random().toString(36).substring(3),
217
- role: "user",
217
+ role: 'user',
218
218
  content: `Tool '${toolInvocation.toolName}' returned: ${JSON.stringify(result)}`,
219
219
  });
220
220
  }
@@ -222,7 +222,7 @@ export function streamChatGPT(backendUrl, messages, tools, onResponse, token) {
222
222
  console.error(`Error executing tool ${toolInvocation.toolName}:`, error);
223
223
  toolResults.push({
224
224
  id: Math.random().toString(36).substring(3),
225
- role: "user",
225
+ role: 'user',
226
226
  content: `Tool '${toolInvocation.toolName}' failed with error: ${error}`,
227
227
  });
228
228
  }
@@ -1,5 +1,5 @@
1
- import { SupabaseClient } from "@supabase/supabase-js";
2
- import { PluginController } from "../../plugin/PluginController";
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+ import { PluginController } from '../../plugin/PluginController';
3
3
  export type TriggerAction = {
4
4
  action_key: string;
5
5
  } & Record<string, string | number | boolean>;
@@ -38,7 +38,7 @@ export class ExerciseController {
38
38
  method: 'POST',
39
39
  headers: {
40
40
  'Content-Type': 'application/json',
41
- 'Authorization': `Bearer ${token}`,
41
+ Authorization: `Bearer ${token}`,
42
42
  },
43
43
  body: JSON.stringify(params),
44
44
  });
@@ -60,7 +60,7 @@ export class ExerciseController {
60
60
  const response = yield fetch(`${this.pluginController.getBackendUrl()}/exercises/${id}`, {
61
61
  method: 'DELETE',
62
62
  headers: {
63
- 'Authorization': `Bearer ${token}`,
63
+ Authorization: `Bearer ${token}`,
64
64
  },
65
65
  });
66
66
  if (!response.ok) {
@@ -17,8 +17,8 @@ export function generateObject(backendUrl, request, token) {
17
17
  behaviour: request.behaviour,
18
18
  instructions: request.instructions,
19
19
  }),
20
- headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
21
- }).then(response => response.json());
20
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
21
+ }).then((response) => response.json());
22
22
  });
23
23
  }
24
24
  export function streamObject(backendUrl, request, onResponse, token) {
@@ -32,7 +32,7 @@ export function streamObject(backendUrl, request, onResponse, token) {
32
32
  systemInstructions: request.behaviour,
33
33
  secondaryInstructions: request.instructions,
34
34
  }),
35
- headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
35
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
36
36
  });
37
37
  if (!response.body) {
38
38
  console.error('No response body.');
@@ -40,14 +40,14 @@ export function streamObject(backendUrl, request, onResponse, token) {
40
40
  }
41
41
  const reader = response.body.getReader();
42
42
  const decoder = new TextDecoder('utf-8');
43
- let content = "";
43
+ let content = '';
44
44
  let done = false;
45
45
  let toolInvocations = [];
46
46
  while (!done) {
47
47
  const { value } = yield reader.read();
48
48
  if (value) {
49
49
  const chunk = decoder.decode(value, { stream: true });
50
- const lines = chunk.split('\n').filter(line => line.trim() !== '');
50
+ const lines = chunk.split('\n').filter((line) => line.trim() !== '');
51
51
  for (const line of lines) {
52
52
  const data = line.substring(3, line.length - 1);
53
53
  const command = line.substring(0, 1);
@@ -1,6 +1,7 @@
1
- import { SupabaseClient } from "@supabase/supabase-js";
2
- import { LanguageLevel } from "../../utils/difficultyConverter";
3
- import { Language } from "../../utils/Language";
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+ import { LanguageLevel } from '../../utils/difficultyConverter';
3
+ import { Language } from '../../utils/Language';
4
+ import { Guild } from '../core';
4
5
  export interface Buddy {
5
6
  id: string;
6
7
  name: string;
@@ -35,19 +36,33 @@ export interface UserInfo {
35
36
  context_menu_on_select: boolean;
36
37
  user_name?: string;
37
38
  /**
38
- * ISO 3166-1 alpha-2 country code of user's location (exposed to plugins)
39
+ * ISO 3166-1 alpha-2 country code of user's target location (exposed to plugins)
39
40
  */
40
- location_country: string;
41
+ target_country: string;
41
42
  /**
42
43
  * Optional: nearest big city (>100,000) near user's location
43
44
  */
44
- location_city?: string;
45
+ target_city?: string;
45
46
  }
46
47
  export declare class SettingsController {
47
48
  private pluginId;
48
49
  private supabase;
49
- constructor(supabase: SupabaseClient, pluginId: string);
50
+ private guild;
51
+ constructor(supabase: SupabaseClient, pluginId: string, guild: Guild);
52
+ /**
53
+ * Fetches settings based on guild configuration.
54
+ * If guild doesn't allow user settings, fetches guild-level settings.
55
+ * Otherwise, fetches user-specific settings.
56
+ * @returns The settings object or null if not found.
57
+ */
50
58
  private fetchSettings;
59
+ /**
60
+ * Sets settings for the plugin.
61
+ * Automatically saves as guild settings if guild doesn't allow user settings,
62
+ * otherwise saves as user-specific settings.
63
+ * @param settings - The settings object to save.
64
+ * @throws {Error} if RLS blocks the operation.
65
+ */
51
66
  setSettings(settings: any): Promise<void>;
52
67
  /**
53
68
  * Get the settings for the plugin. T can be any type of settings, UserSettings or SystemSettings.
@@ -8,22 +8,87 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  export class SettingsController {
11
- constructor(supabase, pluginId) {
11
+ constructor(supabase, pluginId, guild) {
12
12
  this.supabase = supabase;
13
13
  this.pluginId = pluginId;
14
+ this.guild = guild;
14
15
  }
16
+ /**
17
+ * Fetches settings based on guild configuration.
18
+ * If guild doesn't allow user settings, fetches guild-level settings.
19
+ * Otherwise, fetches user-specific settings.
20
+ * @returns The settings object or null if not found.
21
+ */
15
22
  fetchSettings() {
16
23
  return __awaiter(this, void 0, void 0, function* () {
17
- const { data } = yield this.supabase.from("plugin_settings").select("*").eq("plugin_id", this.pluginId);
18
- if (!data || data.length === 0) {
19
- return null;
20
- }
21
- return data[0].settings;
24
+ var _a;
25
+ const isGuildSetting = !this.guild.allowUserPluginSettings;
26
+ const { data } = yield this.supabase
27
+ .from('plugin_settings')
28
+ .select('*')
29
+ .eq('plugin_id', this.pluginId)
30
+ .eq('guild_id', this.guild.id)
31
+ .eq('is_guild_setting', isGuildSetting)
32
+ .maybeSingle();
33
+ return (_a = data === null || data === void 0 ? void 0 : data.settings) !== null && _a !== void 0 ? _a : null;
22
34
  });
23
35
  }
36
+ /**
37
+ * Sets settings for the plugin.
38
+ * Automatically saves as guild settings if guild doesn't allow user settings,
39
+ * otherwise saves as user-specific settings.
40
+ * @param settings - The settings object to save.
41
+ * @throws {Error} if RLS blocks the operation.
42
+ */
24
43
  setSettings(settings) {
25
44
  return __awaiter(this, void 0, void 0, function* () {
26
- yield this.supabase.from("plugin_settings").upsert({ plugin_id: this.pluginId, settings });
45
+ var _a;
46
+ const isGuildSetting = !this.guild.allowUserPluginSettings;
47
+ const payload = {
48
+ plugin_id: this.pluginId,
49
+ settings,
50
+ guild_id: this.guild.id,
51
+ is_guild_setting: isGuildSetting,
52
+ };
53
+ if (isGuildSetting) {
54
+ payload.user_id = null;
55
+ }
56
+ // Try UPDATE first (safe with RLS). If nothing updated, INSERT.
57
+ const updateQuery = this.supabase
58
+ .from('plugin_settings')
59
+ .update({ settings })
60
+ .eq('plugin_id', this.pluginId)
61
+ .eq('guild_id', this.guild.id)
62
+ .eq('is_guild_setting', isGuildSetting);
63
+ const { data: updatedRows, error: updateError } = yield (isGuildSetting
64
+ ? updateQuery.is('user_id', null).select('id')
65
+ : updateQuery.select('id'));
66
+ if (updateError) {
67
+ if (updateError.code === '42501' || ((_a = updateError.message) === null || _a === void 0 ? void 0 : _a.includes('policy'))) {
68
+ throw new Error(`Cannot set ${isGuildSetting ? 'guild' : 'user'} settings: Permission denied.`);
69
+ }
70
+ // proceed to try insert in case of other issues
71
+ }
72
+ if (updatedRows && updatedRows.length > 0) {
73
+ return; // updated successfully
74
+ }
75
+ // No row updated -> INSERT
76
+ const { error: insertError } = yield this.supabase.from('plugin_settings').insert(payload);
77
+ if (insertError) {
78
+ // In case of race condition (duplicate), try one more UPDATE
79
+ if (insertError.code === '23505' /* unique_violation */) {
80
+ const retry = this.supabase
81
+ .from('plugin_settings')
82
+ .update({ settings })
83
+ .eq('plugin_id', this.pluginId)
84
+ .eq('guild_id', this.guild.id)
85
+ .eq('is_guild_setting', isGuildSetting);
86
+ const { error: retryError } = yield (isGuildSetting ? retry.is('user_id', null) : retry);
87
+ if (!retryError)
88
+ return;
89
+ }
90
+ throw insertError;
91
+ }
27
92
  });
28
93
  }
29
94
  /**
@@ -33,7 +98,7 @@ export class SettingsController {
33
98
  */
34
99
  getSettings(defaultSettings) {
35
100
  return __awaiter(this, void 0, void 0, function* () {
36
- const storedSettings = yield this.fetchSettings();
101
+ const storedSettings = (yield this.fetchSettings());
37
102
  if (!storedSettings) {
38
103
  yield this.setSettings(defaultSettings);
39
104
  return defaultSettings;
@@ -1,6 +1,6 @@
1
1
  import { SupabaseClient } from '@supabase/supabase-js';
2
- import { RimoriClient } from "../../plugin/RimoriClient";
3
- import { ObjectRequest } from "./ObjectController";
2
+ import { RimoriClient } from '../../plugin/RimoriClient';
3
+ import { ObjectRequest } from './ObjectController';
4
4
  export interface SharedContentObjectRequest extends ObjectRequest {
5
5
  fixedProperties?: Record<string, string | number | boolean>;
6
6
  }
@@ -69,7 +69,7 @@ export declare class SharedContentController {
69
69
  * @returns The inserted shared content.
70
70
  * @throws {Error} if insertion fails.
71
71
  */
72
- createSharedContent<T>({ contentType, title, keywords, data, privateTopic }: Omit<SharedContent<T>, 'id'>): Promise<SharedContent<T>>;
72
+ createSharedContent<T>({ contentType, title, keywords, data, privateTopic, }: Omit<SharedContent<T>, 'id'>): Promise<SharedContent<T>>;
73
73
  /**
74
74
  * Update existing shared content in the database.
75
75
  * @param id - The ID of the content to update.
@@ -28,8 +28,9 @@ export class SharedContentController {
28
28
  //this filter is there if the content should be filtered additionally by a column and value
29
29
  filter, options) {
30
30
  return __awaiter(this, void 0, void 0, function* () {
31
- let query = this.supabase.from("shared_content")
32
- .select("*, scc:shared_content_completed(id, state)")
31
+ let query = this.supabase
32
+ .from('shared_content')
33
+ .select('*, scc:shared_content_completed(id, state)')
33
34
  .eq('content_type', contentType)
34
35
  .not('scc.state', 'in', '("completed","ongoing","hidden")')
35
36
  .is('deleted_at', null);
@@ -56,7 +57,7 @@ export class SharedContentController {
56
57
  console.log('instructions:', instructions);
57
58
  //create the shared content object
58
59
  const data = {
59
- id: "internal-temp-id-" + Math.random().toString(36).substring(2, 15),
60
+ id: 'internal-temp-id-' + Math.random().toString(36).substring(2, 15),
60
61
  contentType,
61
62
  title: instructions.title,
62
63
  keywords: instructions.keywords.map(({ text }) => text),
@@ -82,20 +83,21 @@ export class SharedContentController {
82
83
  generatorInstructions.instructions += `
83
84
  The following topics are already taken: ${completedTopics.join(', ')}`;
84
85
  generatorInstructions.tool.title = {
85
- type: "string",
86
- description: "What the topic is about. Short. ",
86
+ type: 'string',
87
+ description: 'What the topic is about. Short. ',
87
88
  };
88
89
  generatorInstructions.tool.keywords = {
89
- type: [{ text: { type: "string" } }],
90
- description: "Keywords around the topic of the assignment.",
90
+ type: [{ text: { type: 'string' } }],
91
+ description: 'Keywords around the topic of the assignment.',
91
92
  };
92
93
  return generatorInstructions;
93
94
  });
94
95
  }
95
96
  getCompletedTopics(contentType, filter) {
96
97
  return __awaiter(this, void 0, void 0, function* () {
97
- const query = this.supabase.from("shared_content")
98
- .select("title, keywords, scc:shared_content_completed(id)")
98
+ const query = this.supabase
99
+ .from('shared_content')
100
+ .select('title, keywords, scc:shared_content_completed(id)')
99
101
  .eq('content_type', contentType)
100
102
  .not('scc.id', 'is', null)
101
103
  .is('deleted_at', null);
@@ -112,7 +114,13 @@ export class SharedContentController {
112
114
  }
113
115
  getSharedContent(contentType, id) {
114
116
  return __awaiter(this, void 0, void 0, function* () {
115
- const { data, error } = yield this.supabase.from("shared_content").select().eq('content_type', contentType).eq('id', id).is('deleted_at', null).single();
117
+ const { data, error } = yield this.supabase
118
+ .from('shared_content')
119
+ .select()
120
+ .eq('content_type', contentType)
121
+ .eq('id', id)
122
+ .is('deleted_at', null)
123
+ .single();
116
124
  if (error) {
117
125
  console.error('error fetching shared content:', error);
118
126
  throw new Error('error fetching shared content');
@@ -124,7 +132,7 @@ export class SharedContentController {
124
132
  return __awaiter(this, void 0, void 0, function* () {
125
133
  // Idempotent completion: upsert on (id, user_id) so repeated calls don't fail
126
134
  const { error } = yield this.supabase
127
- .from("shared_content_completed")
135
+ .from('shared_content_completed')
128
136
  .upsert({ content_type: contentType, id: assignmentId }, { onConflict: 'id' });
129
137
  if (error) {
130
138
  console.error('error completing shared content:', error);
@@ -153,9 +161,7 @@ export class SharedContentController {
153
161
  if (bookmarked !== undefined)
154
162
  payload.bookmarked = bookmarked;
155
163
  // Prefer upsert, fall back to insert/update if upsert not allowed
156
- const { error } = yield this.supabase
157
- .from('shared_content_completed')
158
- .upsert(payload, { onConflict: 'id' });
164
+ const { error } = yield this.supabase.from('shared_content_completed').upsert(payload, { onConflict: 'id' });
159
165
  if (error) {
160
166
  console.error('error updating shared content state:', error);
161
167
  throw new Error('error updating shared content state');
@@ -171,7 +177,12 @@ export class SharedContentController {
171
177
  */
172
178
  getSharedContentList(contentType, filter, limit) {
173
179
  return __awaiter(this, void 0, void 0, function* () {
174
- const query = this.supabase.from("shared_content").select("*").eq('content_type', contentType).is('deleted_at', null).limit(limit !== null && limit !== void 0 ? limit : 30);
180
+ const query = this.supabase
181
+ .from('shared_content')
182
+ .select('*')
183
+ .eq('content_type', contentType)
184
+ .is('deleted_at', null)
185
+ .limit(limit !== null && limit !== void 0 ? limit : 30);
175
186
  if (filter) {
176
187
  query.contains('data', filter);
177
188
  }
@@ -195,14 +206,17 @@ export class SharedContentController {
195
206
  * @throws {Error} if insertion fails.
196
207
  */
197
208
  createSharedContent(_a) {
198
- return __awaiter(this, arguments, void 0, function* ({ contentType, title, keywords, data, privateTopic }) {
199
- const { data: newContent, error } = yield this.supabase.from("shared_content").insert({
209
+ return __awaiter(this, arguments, void 0, function* ({ contentType, title, keywords, data, privateTopic, }) {
210
+ const { data: newContent, error } = yield this.supabase
211
+ .from('shared_content')
212
+ .insert({
200
213
  private: privateTopic,
201
214
  content_type: contentType,
202
215
  title,
203
216
  keywords,
204
217
  data,
205
- }).select();
218
+ })
219
+ .select();
206
220
  if (error) {
207
221
  console.error('error inserting shared content:', error);
208
222
  throw new Error('error inserting shared content');
@@ -230,7 +244,11 @@ export class SharedContentController {
230
244
  updateData.data = updates.data;
231
245
  if (updates.privateTopic !== undefined)
232
246
  updateData.private = updates.privateTopic;
233
- const { data: updatedContent, error } = yield this.supabase.from("shared_content").update(updateData).eq('id', id).select();
247
+ const { data: updatedContent, error } = yield this.supabase
248
+ .from('shared_content')
249
+ .update(updateData)
250
+ .eq('id', id)
251
+ .select();
234
252
  if (error) {
235
253
  console.error('error updating shared content:', error);
236
254
  throw new Error('error updating shared content');
@@ -250,7 +268,7 @@ export class SharedContentController {
250
268
  removeSharedContent(id) {
251
269
  return __awaiter(this, void 0, void 0, function* () {
252
270
  const { data: deletedContent, error } = yield this.supabase
253
- .from("shared_content")
271
+ .from('shared_content')
254
272
  .update({ deleted_at: new Date().toISOString() })
255
273
  .eq('id', id)
256
274
  .select();
@@ -13,9 +13,11 @@ export function getSTTResponse(backendUrl, audio, token) {
13
13
  formData.append('file', audio);
14
14
  return yield fetch(`${backendUrl}/voice/stt`, {
15
15
  method: 'POST',
16
- headers: { 'Authorization': `Bearer ${token}` },
16
+ headers: { Authorization: `Bearer ${token}` },
17
17
  body: formData,
18
- }).then(r => r.json()).then(r => {
18
+ })
19
+ .then((r) => r.json())
20
+ .then((r) => {
19
21
  // console.log("STT response: ", r);
20
22
  return r.text;
21
23
  });
@@ -27,9 +29,9 @@ export function getTTSResponse(backendUrl, request, token) {
27
29
  method: 'POST',
28
30
  headers: {
29
31
  'Content-Type': 'application/json',
30
- 'Authorization': `Bearer ${token}`
32
+ Authorization: `Bearer ${token}`,
31
33
  },
32
34
  body: JSON.stringify(request),
33
- }).then(r => r.blob());
35
+ }).then((r) => r.blob());
34
36
  });
35
37
  }