@rimori/client 1.3.1 → 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 (148) hide show
  1. package/.prettierignore +35 -0
  2. package/README.md +77 -71
  3. package/dist/cli/scripts/init/dev-registration.d.ts +1 -1
  4. package/dist/cli/scripts/init/dev-registration.js +4 -4
  5. package/dist/cli/scripts/init/main.js +1 -1
  6. package/dist/cli/scripts/init/package-setup.d.ts +1 -1
  7. package/dist/cli/scripts/init/package-setup.js +3 -3
  8. package/dist/cli/scripts/init/router-transformer.js +19 -12
  9. package/dist/cli/scripts/init/vite-config.d.ts +2 -2
  10. package/dist/cli/scripts/init/vite-config.js +2 -2
  11. package/dist/cli/scripts/release/release-config-upload.js +9 -9
  12. package/dist/cli/scripts/release/release-db-update.d.ts +1 -1
  13. package/dist/cli/scripts/release/release-db-update.js +9 -9
  14. package/dist/cli/scripts/release/release-file-upload.js +2 -2
  15. package/dist/cli/scripts/release/release.js +2 -2
  16. package/dist/cli/types/DatabaseTypes.d.ts +2 -2
  17. package/dist/components/CRUDModal.d.ts +1 -1
  18. package/dist/components/CRUDModal.js +3 -3
  19. package/dist/components/MarkdownEditor.js +16 -16
  20. package/dist/components/Spinner.js +2 -2
  21. package/dist/components/ai/Assistant.js +7 -8
  22. package/dist/components/ai/Avatar.d.ts +2 -2
  23. package/dist/components/ai/Avatar.js +14 -7
  24. package/dist/components/ai/EmbeddedAssistent/AudioInputField.js +5 -6
  25. package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.d.ts +1 -1
  26. package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.js +1 -2
  27. package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.d.ts +1 -2
  28. package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.js +4 -2
  29. package/dist/components/ai/EmbeddedAssistent/TTS/Player.js +1 -1
  30. package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.js +2 -3
  31. package/dist/components/audio/Playbutton.js +10 -7
  32. package/dist/components/components/ContextMenu.d.ts +1 -1
  33. package/dist/components/components/ContextMenu.js +19 -16
  34. package/dist/components.d.ts +10 -10
  35. package/dist/components.js +10 -10
  36. package/dist/core/controller/AIController.d.ts +2 -2
  37. package/dist/core/controller/AIController.js +20 -18
  38. package/dist/core/controller/ExerciseController.d.ts +52 -0
  39. package/dist/core/controller/ExerciseController.js +73 -0
  40. package/dist/core/controller/ObjectController.js +5 -5
  41. package/dist/core/controller/SettingsController.d.ts +22 -7
  42. package/dist/core/controller/SettingsController.js +73 -8
  43. package/dist/core/controller/SharedContentController.d.ts +3 -3
  44. package/dist/core/controller/SharedContentController.js +38 -20
  45. package/dist/core/controller/VoiceController.js +6 -4
  46. package/dist/core/core.d.ts +15 -14
  47. package/dist/core/core.js +7 -7
  48. package/dist/fromRimori/EventBus.js +23 -23
  49. package/dist/fromRimori/PluginTypes.d.ts +4 -4
  50. package/dist/hooks/UseChatHook.d.ts +3 -3
  51. package/dist/hooks/UseChatHook.js +9 -3
  52. package/dist/index.d.ts +10 -10
  53. package/dist/index.js +9 -9
  54. package/dist/plugin/AccomplishmentHandler.d.ts +5 -5
  55. package/dist/plugin/AccomplishmentHandler.js +31 -27
  56. package/dist/plugin/AudioController.d.ts +1 -1
  57. package/dist/plugin/AudioController.js +6 -6
  58. package/dist/plugin/Logger.d.ts +5 -0
  59. package/dist/plugin/Logger.js +65 -13
  60. package/dist/plugin/PluginController.d.ts +7 -1
  61. package/dist/plugin/PluginController.js +32 -27
  62. package/dist/plugin/RimoriClient.d.ts +39 -14
  63. package/dist/plugin/RimoriClient.js +60 -31
  64. package/dist/plugin/StandaloneClient.d.ts +1 -1
  65. package/dist/plugin/StandaloneClient.js +35 -16
  66. package/dist/plugin/ThemeSetter.js +4 -4
  67. package/dist/providers/PluginProvider.js +44 -14
  68. package/dist/utils/Language.js +57 -57
  69. package/dist/utils/PluginUtils.js +3 -3
  70. package/dist/utils/difficultyConverter.d.ts +1 -1
  71. package/dist/utils/difficultyConverter.js +1 -1
  72. package/dist/utils/endpoint.js +2 -2
  73. package/dist/worker/WorkerSetup.d.ts +1 -1
  74. package/dist/worker/WorkerSetup.js +6 -6
  75. package/eslint.config.js +53 -0
  76. package/example/docs/devdocs.md +50 -40
  77. package/example/docs/overview.md +1 -1
  78. package/example/docs/userdocs.md +4 -1
  79. package/example/rimori.config.ts +51 -49
  80. package/example/worker/vite.config.ts +3 -3
  81. package/example/worker/worker.ts +2 -2
  82. package/package.json +17 -4
  83. package/prettier.config.js +8 -0
  84. package/src/cli/scripts/init/dev-registration.ts +5 -8
  85. package/src/cli/scripts/init/env-setup.ts +1 -1
  86. package/src/cli/scripts/init/file-operations.ts +1 -1
  87. package/src/cli/scripts/init/html-cleaner.ts +2 -5
  88. package/src/cli/scripts/init/main.ts +16 -13
  89. package/src/cli/scripts/init/package-setup.ts +11 -15
  90. package/src/cli/scripts/init/router-transformer.ts +40 -37
  91. package/src/cli/scripts/init/tailwind-config.ts +17 -26
  92. package/src/cli/scripts/init/vite-config.ts +3 -3
  93. package/src/cli/scripts/release/release-config-upload.ts +11 -11
  94. package/src/cli/scripts/release/release-db-update.ts +12 -12
  95. package/src/cli/scripts/release/release-file-upload.ts +3 -3
  96. package/src/cli/scripts/release/release.ts +4 -4
  97. package/src/cli/types/DatabaseTypes.ts +7 -8
  98. package/src/components/CRUDModal.tsx +64 -48
  99. package/src/components/MarkdownEditor.tsx +58 -27
  100. package/src/components/Spinner.tsx +24 -17
  101. package/src/components/ai/Assistant.tsx +70 -70
  102. package/src/components/ai/Avatar.tsx +20 -16
  103. package/src/components/ai/EmbeddedAssistent/AudioInputField.tsx +63 -54
  104. package/src/components/ai/EmbeddedAssistent/CircleAudioAvatar.tsx +14 -5
  105. package/src/components/ai/EmbeddedAssistent/TTS/MessageSender.ts +75 -74
  106. package/src/components/ai/EmbeddedAssistent/TTS/Player.ts +177 -178
  107. package/src/components/ai/EmbeddedAssistent/VoiceRecoder.tsx +109 -94
  108. package/src/components/ai/utils.ts +4 -4
  109. package/src/components/audio/Playbutton.tsx +101 -93
  110. package/src/components/components/ContextMenu.tsx +47 -35
  111. package/src/components.ts +10 -10
  112. package/src/core/controller/AIController.ts +62 -50
  113. package/src/core/controller/ExerciseController.ts +98 -0
  114. package/src/core/controller/ObjectController.ts +15 -10
  115. package/src/core/controller/SettingsController.ts +89 -16
  116. package/src/core/controller/SharedContentController.ts +80 -44
  117. package/src/core/controller/VoiceController.ts +10 -8
  118. package/src/core/core.ts +15 -15
  119. package/src/fromRimori/EventBus.ts +76 -47
  120. package/src/fromRimori/PluginTypes.ts +26 -17
  121. package/src/fromRimori/readme.md +2 -2
  122. package/src/hooks/UseChatHook.ts +25 -15
  123. package/src/index.ts +10 -10
  124. package/src/plugin/AccomplishmentHandler.ts +53 -35
  125. package/src/plugin/AudioController.ts +18 -12
  126. package/src/plugin/Logger.ts +77 -19
  127. package/src/plugin/PluginController.ts +60 -44
  128. package/src/plugin/RimoriClient.ts +133 -69
  129. package/src/plugin/StandaloneClient.ts +51 -24
  130. package/src/plugin/ThemeSetter.ts +5 -5
  131. package/src/providers/PluginProvider.tsx +90 -36
  132. package/src/style.scss +3 -3
  133. package/src/utils/Language.ts +58 -58
  134. package/src/utils/PluginUtils.ts +16 -20
  135. package/src/utils/difficultyConverter.ts +2 -2
  136. package/src/utils/endpoint.ts +3 -2
  137. package/src/worker/WorkerSetup.ts +8 -9
  138. package/tsconfig.json +2 -4
  139. package/dist/components/LoggerExample.d.ts +0 -6
  140. package/dist/components/LoggerExample.js +0 -79
  141. package/dist/core/controller/AudioController.d.ts +0 -0
  142. package/dist/core/controller/AudioController.js +0 -1
  143. package/dist/hooks/UseLogger.d.ts +0 -30
  144. package/dist/hooks/UseLogger.js +0 -122
  145. package/dist/plugin/LoggerExample.d.ts +0 -16
  146. package/dist/plugin/LoggerExample.js +0 -140
  147. package/dist/utils/audioFormats.d.ts +0 -26
  148. package/dist/utils/audioFormats.js +0 -67
@@ -1,4 +1,4 @@
1
- import { Tool } from "../../fromRimori/PluginTypes";
1
+ import { Tool } from '../../fromRimori/PluginTypes';
2
2
 
3
3
  export interface ToolInvocation {
4
4
  toolCallId: string;
@@ -8,7 +8,7 @@ export interface ToolInvocation {
8
8
 
9
9
  export interface Message {
10
10
  id?: string;
11
- role: "user" | "assistant" | "system"
11
+ role: 'user' | 'assistant' | 'system';
12
12
  content: string;
13
13
  toolCalls?: ToolInvocation[];
14
14
  }
@@ -17,15 +17,26 @@ export async function generateText(backendUrl: string, messages: Message[], tool
17
17
  const response = await fetch(`${backendUrl}/ai/llm`, {
18
18
  method: 'POST',
19
19
  body: JSON.stringify({ messages, tools }),
20
- headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
20
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
21
21
  });
22
22
 
23
23
  return await response.json();
24
24
  }
25
25
 
26
- export type OnLLMResponse = (id: string, response: string, finished: boolean, toolInvocations?: ToolInvocation[]) => void;
26
+ export type OnLLMResponse = (
27
+ id: string,
28
+ response: string,
29
+ finished: boolean,
30
+ toolInvocations?: ToolInvocation[],
31
+ ) => void;
27
32
 
28
- export async function streamChatGPT(backendUrl: string, messages: Message[], tools: Tool[], onResponse: OnLLMResponse, token: string) {
33
+ export async function streamChatGPT(
34
+ backendUrl: string,
35
+ messages: Message[],
36
+ tools: Tool[],
37
+ onResponse: OnLLMResponse,
38
+ token: string,
39
+ ) {
29
40
  const messageId = Math.random().toString(36).substring(3);
30
41
  let currentMessages: Message[] = [...messages];
31
42
 
@@ -33,7 +44,7 @@ export async function streamChatGPT(backendUrl: string, messages: Message[], too
33
44
  messageId,
34
45
  messageCount: messages.length,
35
46
  toolCount: tools.length,
36
- backendUrl
47
+ backendUrl,
37
48
  });
38
49
 
39
50
  while (true) {
@@ -43,7 +54,7 @@ export async function streamChatGPT(backendUrl: string, messages: Message[], too
43
54
  const response = await fetch(`${backendUrl}/ai/llm`, {
44
55
  method: 'POST',
45
56
  body: JSON.stringify({ messages: messagesForApi, tools, stream: true }),
46
- headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
57
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
47
58
  });
48
59
 
49
60
  if (!response.ok) {
@@ -58,12 +69,12 @@ export async function streamChatGPT(backendUrl: string, messages: Message[], too
58
69
  const reader = response.body.getReader();
59
70
  const decoder = new TextDecoder('utf-8');
60
71
 
61
- let content = "";
72
+ let content = '';
62
73
  let done = false;
63
- let toolInvocations: { toolCallId: string, toolName: string, args: any }[] = [];
64
- let currentTextId = "";
74
+ let toolInvocations: { toolCallId: string; toolName: string; args: any }[] = [];
75
+ let currentTextId = '';
65
76
  let isToolCallMode = false;
66
- let buffer = ""; // Buffer for incomplete chunks
77
+ let buffer = ''; // Buffer for incomplete chunks
67
78
 
68
79
  while (!done) {
69
80
  const { value, done: readerDone } = await reader.read();
@@ -71,22 +82,22 @@ export async function streamChatGPT(backendUrl: string, messages: Message[], too
71
82
  if (value) {
72
83
  const chunk = decoder.decode(value, { stream: true });
73
84
  buffer += chunk;
74
-
85
+
75
86
  // Split by lines, but handle incomplete lines
76
87
  const lines = buffer.split('\n');
77
-
88
+
78
89
  // Keep the last line in buffer if it's incomplete
79
90
  if (lines.length > 1) {
80
- buffer = lines.pop() || "";
91
+ buffer = lines.pop() || '';
81
92
  }
82
93
 
83
94
  for (const line of lines) {
84
95
  if (line.trim() === '') continue;
85
-
96
+
86
97
  // Handle the new streaming format
87
98
  if (line.startsWith('data: ')) {
88
99
  const dataStr = line.substring(6); // Remove 'data: ' prefix
89
-
100
+
90
101
  // Handle [DONE] marker
91
102
  if (dataStr === '[DONE]') {
92
103
  done = true;
@@ -95,39 +106,39 @@ export async function streamChatGPT(backendUrl: string, messages: Message[], too
95
106
 
96
107
  try {
97
108
  const data = JSON.parse(dataStr);
98
-
109
+
99
110
  // Log the first message to understand the format
100
111
  if (!content && !isToolCallMode) {
101
- console.log('First stream message received:', data);
112
+ // console.log('First stream message received:', data);
102
113
  }
103
-
114
+
104
115
  switch (data.type) {
105
116
  case 'start':
106
117
  // Stream started, no action needed
107
- console.log('Stream started');
118
+ // console.log('Stream started');
108
119
  break;
109
-
120
+
110
121
  case 'start-step':
111
122
  // Step started, no action needed
112
- console.log('Step started');
123
+ // console.log('Step started');
113
124
  break;
114
-
125
+
115
126
  case 'reasoning-start':
116
127
  // Reasoning started, no action needed
117
128
  console.log('Reasoning started:', data.id);
118
129
  break;
119
-
130
+
120
131
  case 'reasoning-end':
121
132
  // Reasoning ended, no action needed
122
133
  console.log('Reasoning ended:', data.id);
123
134
  break;
124
-
135
+
125
136
  case 'text-start':
126
137
  // Text generation started, store the ID
127
138
  currentTextId = data.id;
128
139
  console.log('Text generation started:', data.id);
129
140
  break;
130
-
141
+
131
142
  case 'text-delta':
132
143
  // Text delta received, append to content
133
144
  if (data.delta) {
@@ -135,73 +146,75 @@ export async function streamChatGPT(backendUrl: string, messages: Message[], too
135
146
  onResponse(messageId, content, false);
136
147
  }
137
148
  break;
138
-
149
+
139
150
  case 'text-end':
140
151
  // Text generation ended
141
152
  console.log('Text generation ended:', data.id);
142
153
  break;
143
-
154
+
144
155
  case 'finish-step':
145
156
  // Step finished, no action needed
146
- console.log('Step finished');
157
+ // console.log('Step finished');
147
158
  break;
148
-
159
+
149
160
  case 'finish':
150
161
  // Stream finished
151
- console.log('Stream finished');
162
+ // console.log('Stream finished');
152
163
  done = true;
153
164
  break;
154
-
165
+
155
166
  // Additional message types that might be present in the AI library
156
167
  case 'tool-call':
168
+ case 'tool-input-available': //for now input calls should be handled the same way as tool calls
157
169
  // Tool call initiated
158
170
  console.log('Tool call initiated:', data);
159
171
  isToolCallMode = true;
160
- if (data.toolCallId && data.toolName && data.args) {
172
+ if (data.toolCallId && data.toolName && (data.args || data.input)) {
161
173
  toolInvocations.push({
162
174
  toolCallId: data.toolCallId,
163
175
  toolName: data.toolName,
164
- args: data.args
176
+ args: data.args || data.input,
165
177
  });
166
178
  }
167
179
  break;
168
-
180
+
181
+ case 'tool-input-delta': //for now input calls should be handled the same way as tool calls
169
182
  case 'tool-call-delta':
170
183
  // Tool call delta (for streaming tool calls)
171
184
  console.log('Tool call delta:', data);
172
185
  break;
173
-
186
+
174
187
  case 'tool-call-end':
175
188
  // Tool call completed
176
189
  console.log('Tool call completed:', data);
177
190
  break;
178
-
191
+
179
192
  case 'tool-result':
180
193
  // Tool execution result
181
194
  console.log('Tool result:', data);
182
195
  break;
183
-
196
+
184
197
  case 'error':
185
198
  // Error occurred
186
199
  console.error('Stream error:', data);
187
200
  break;
188
-
201
+
189
202
  case 'usage':
190
203
  // Usage information
191
204
  console.log('Usage info:', data);
192
205
  break;
193
-
206
+
194
207
  case 'model':
195
208
  // Model information
196
209
  console.log('Model info:', data);
197
210
  break;
198
-
211
+
199
212
  case 'stop':
200
213
  // Stop signal
201
214
  console.log('Stop signal received');
202
215
  done = true;
203
216
  break;
204
-
217
+
205
218
  default:
206
219
  // Unknown type, log for debugging
207
220
  console.log('Unknown stream type:', data.type, data);
@@ -223,38 +236,38 @@ export async function streamChatGPT(backendUrl: string, messages: Message[], too
223
236
  if (content || toolInvocations.length > 0) {
224
237
  currentMessages.push({
225
238
  id: messageId,
226
- role: "assistant",
239
+ role: 'assistant',
227
240
  content: content,
228
- toolCalls: toolInvocations.length > 0 ? toolInvocations: undefined,
241
+ toolCalls: toolInvocations.length > 0 ? toolInvocations : undefined,
229
242
  });
230
243
  }
231
244
 
232
245
  // Handle tool call scenario if tools were provided
233
246
  if (tools.length > 0 && toolInvocations.length > 0) {
234
247
  console.log('Tool calls detected, executing tools...');
235
-
248
+
236
249
  const toolResults: Message[] = [];
237
250
  for (const toolInvocation of toolInvocations) {
238
- const tool = tools.find(t => t.name === toolInvocation.toolName);
251
+ const tool = tools.find((t) => t.name === toolInvocation.toolName);
239
252
  if (tool && tool.execute) {
240
253
  try {
241
254
  const result = await tool.execute(toolInvocation.args);
242
255
  toolResults.push({
243
256
  id: Math.random().toString(36).substring(3),
244
- role: "user",
257
+ role: 'user',
245
258
  content: `Tool '${toolInvocation.toolName}' returned: ${JSON.stringify(result)}`,
246
259
  });
247
260
  } catch (error) {
248
261
  console.error(`Error executing tool ${toolInvocation.toolName}:`, error);
249
262
  toolResults.push({
250
263
  id: Math.random().toString(36).substring(3),
251
- role: "user",
264
+ role: 'user',
252
265
  content: `Tool '${toolInvocation.toolName}' failed with error: ${error}`,
253
266
  });
254
267
  }
255
268
  }
256
269
  }
257
-
270
+
258
271
  if (toolResults.length > 0) {
259
272
  currentMessages.push(...toolResults);
260
273
  // Continue the loop to handle the next response
@@ -273,7 +286,6 @@ export async function streamChatGPT(backendUrl: string, messages: Message[], too
273
286
 
274
287
  onResponse(messageId, content, true, toolInvocations);
275
288
  return;
276
-
277
289
  } catch (error) {
278
290
  console.error('Error in streamChatGPT:', error);
279
291
  onResponse(messageId, `Error: ${error instanceof Error ? error.message : String(error)}`, true, []);
@@ -0,0 +1,98 @@
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+ import { PluginController } from '../../plugin/PluginController';
3
+
4
+ export type TriggerAction = { action_key: string } & Record<string, string | number | boolean>;
5
+
6
+ export interface CreateExerciseParams {
7
+ plugin_id: string;
8
+ start_date: string;
9
+ end_date: string;
10
+ trigger_action: TriggerAction;
11
+ name: string;
12
+ description: string;
13
+ estimated_duration: number;
14
+ }
15
+
16
+ export interface Exercise {
17
+ id: string;
18
+ plugin_id: string;
19
+ start_date: string;
20
+ end_date: string;
21
+ trigger_action: TriggerAction;
22
+ name: string;
23
+ description: string;
24
+ estimated_duration: number;
25
+ created_at?: string;
26
+ updated_at?: string;
27
+ }
28
+
29
+ export class ExerciseController {
30
+ private supabase: SupabaseClient;
31
+ private pluginController: PluginController;
32
+
33
+ constructor(supabase: SupabaseClient, pluginController: PluginController) {
34
+ this.supabase = supabase;
35
+ this.pluginController = pluginController;
36
+ }
37
+
38
+ /**
39
+ * Fetches weekly exercises from the weekly_exercises view.
40
+ * Shows exercises for the current week that haven't expired.
41
+ * @returns Array of exercise objects.
42
+ */
43
+ public async viewWeeklyExercises(): Promise<Exercise[]> {
44
+ const { data, error } = await this.supabase.from('weekly_exercises').select('*');
45
+
46
+ if (error) {
47
+ throw new Error(`Failed to fetch weekly exercises: ${error.message}`);
48
+ }
49
+
50
+ return data || [];
51
+ }
52
+
53
+ /**
54
+ * Creates a new exercise via the backend API.
55
+ * @param params Exercise creation parameters.
56
+ * @returns Created exercise object.
57
+ */
58
+ public async addExercise(params: CreateExerciseParams): Promise<Exercise> {
59
+ const token = await this.pluginController.getToken();
60
+ const response = await fetch(`${this.pluginController.getBackendUrl()}/exercises`, {
61
+ method: 'POST',
62
+ headers: {
63
+ 'Content-Type': 'application/json',
64
+ Authorization: `Bearer ${token}`,
65
+ },
66
+ body: JSON.stringify(params),
67
+ });
68
+
69
+ if (!response.ok) {
70
+ const errorText = await response.text();
71
+ throw new Error(`Failed to create exercise: ${errorText}`);
72
+ }
73
+
74
+ return await response.json();
75
+ }
76
+
77
+ /**
78
+ * Deletes an exercise via the backend API.
79
+ * @param id The exercise ID to delete.
80
+ * @returns Success status.
81
+ */
82
+ public async deleteExercise(id: string): Promise<{ success: boolean; message: string }> {
83
+ const token = await this.pluginController.getToken();
84
+ const response = await fetch(`${this.pluginController.getBackendUrl()}/exercises/${id}`, {
85
+ method: 'DELETE',
86
+ headers: {
87
+ Authorization: `Bearer ${token}`,
88
+ },
89
+ });
90
+
91
+ if (!response.ok) {
92
+ const errorText = await response.text();
93
+ throw new Error(`Failed to delete exercise: ${errorText}`);
94
+ }
95
+
96
+ return await response.json();
97
+ }
98
+ }
@@ -3,8 +3,8 @@ type PrimitiveType = 'string' | 'number' | 'boolean';
3
3
  // This is the type that can appear in the `type` property
4
4
  type ObjectToolParameterType =
5
5
  | PrimitiveType
6
- | { [key: string]: ObjectToolParameter } // for nested objects
7
- | [{ [key: string]: ObjectToolParameter }]; // for arrays of objects (notice the tuple type)
6
+ | { [key: string]: ObjectToolParameter } // for nested objects
7
+ | [{ [key: string]: ObjectToolParameter }]; // for arrays of objects (notice the tuple type)
8
8
 
9
9
  interface ObjectToolParameter {
10
10
  type: ObjectToolParameterType;
@@ -15,10 +15,10 @@ interface ObjectToolParameter {
15
15
 
16
16
  /**
17
17
  * The tools that the AI can use.
18
- *
18
+ *
19
19
  * The key is the name of the tool.
20
20
  * The value is the parameter of the tool.
21
- *
21
+ *
22
22
  */
23
23
  export type ObjectTool = {
24
24
  [key: string]: ObjectToolParameter;
@@ -50,14 +50,19 @@ export async function generateObject(backendUrl: string, request: ObjectRequest,
50
50
  behaviour: request.behaviour,
51
51
  instructions: request.instructions,
52
52
  }),
53
- headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
54
- }).then(response => response.json());
53
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
54
+ }).then((response) => response.json());
55
55
  }
56
56
 
57
57
  // TODO adjust stream to work with object
58
58
  export type OnLLMResponse = (id: string, response: string, finished: boolean, toolInvocations?: any[]) => void;
59
59
 
60
- export async function streamObject(backendUrl: string, request: ObjectRequest, onResponse: OnLLMResponse, token: string) {
60
+ export async function streamObject(
61
+ backendUrl: string,
62
+ request: ObjectRequest,
63
+ onResponse: OnLLMResponse,
64
+ token: string,
65
+ ) {
61
66
  const messageId = Math.random().toString(36).substring(3);
62
67
  const response = await fetch(`${backendUrl}/ai/llm-object`, {
63
68
  method: 'POST',
@@ -67,7 +72,7 @@ export async function streamObject(backendUrl: string, request: ObjectRequest, o
67
72
  systemInstructions: request.behaviour,
68
73
  secondaryInstructions: request.instructions,
69
74
  }),
70
- headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
75
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
71
76
  });
72
77
 
73
78
  if (!response.body) {
@@ -78,7 +83,7 @@ export async function streamObject(backendUrl: string, request: ObjectRequest, o
78
83
  const reader = response.body.getReader();
79
84
  const decoder = new TextDecoder('utf-8');
80
85
 
81
- let content = "";
86
+ let content = '';
82
87
  let done = false;
83
88
  let toolInvocations: any[] = [];
84
89
  while (!done) {
@@ -86,7 +91,7 @@ export async function streamObject(backendUrl: string, request: ObjectRequest, o
86
91
 
87
92
  if (value) {
88
93
  const chunk = decoder.decode(value, { stream: true });
89
- const lines = chunk.split('\n').filter(line => line.trim() !== '');
94
+ const lines = chunk.split('\n').filter((line) => line.trim() !== '');
90
95
 
91
96
  for (const line of lines) {
92
97
  const data = line.substring(3, line.length - 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
 
5
6
  export interface Buddy {
6
7
  id: string;
@@ -37,45 +38,117 @@ export interface UserInfo {
37
38
  context_menu_on_select: boolean;
38
39
  user_name?: string;
39
40
  /**
40
- * ISO 3166-1 alpha-2 country code of user's location (exposed to plugins)
41
+ * ISO 3166-1 alpha-2 country code of user's target location (exposed to plugins)
41
42
  */
42
- location_country: string;
43
+ target_country: string;
43
44
  /**
44
45
  * Optional: nearest big city (>100,000) near user's location
45
46
  */
46
- location_city?: string;
47
+ target_city?: string;
47
48
  }
48
49
 
49
50
  export class SettingsController {
50
51
  private pluginId: string;
51
52
  private supabase: SupabaseClient;
53
+ private guild: Guild;
52
54
 
53
- constructor(supabase: SupabaseClient, pluginId: string) {
55
+ constructor(supabase: SupabaseClient, pluginId: string, guild: Guild) {
54
56
  this.supabase = supabase;
55
57
  this.pluginId = pluginId;
58
+ this.guild = guild;
56
59
  }
57
60
 
61
+ /**
62
+ * Fetches settings based on guild configuration.
63
+ * If guild doesn't allow user settings, fetches guild-level settings.
64
+ * Otherwise, fetches user-specific settings.
65
+ * @returns The settings object or null if not found.
66
+ */
58
67
  private async fetchSettings(): Promise<any | null> {
59
- const { data } = await this.supabase.from("plugin_settings").select("*").eq("plugin_id", this.pluginId)
68
+ const isGuildSetting = !this.guild.allowUserPluginSettings;
60
69
 
61
- if (!data || data.length === 0) {
62
- return null;
63
- }
70
+ const { data } = await this.supabase
71
+ .from('plugin_settings')
72
+ .select('*')
73
+ .eq('plugin_id', this.pluginId)
74
+ .eq('guild_id', this.guild.id)
75
+ .eq('is_guild_setting', isGuildSetting)
76
+ .maybeSingle();
64
77
 
65
- return data[0].settings;
78
+ return data?.settings ?? null;
66
79
  }
67
80
 
81
+ /**
82
+ * Sets settings for the plugin.
83
+ * Automatically saves as guild settings if guild doesn't allow user settings,
84
+ * otherwise saves as user-specific settings.
85
+ * @param settings - The settings object to save.
86
+ * @throws {Error} if RLS blocks the operation.
87
+ */
68
88
  public async setSettings(settings: any): Promise<void> {
69
- await this.supabase.from("plugin_settings").upsert({ plugin_id: this.pluginId, settings });
89
+ const isGuildSetting = !this.guild.allowUserPluginSettings;
90
+
91
+ const payload: any = {
92
+ plugin_id: this.pluginId,
93
+ settings,
94
+ guild_id: this.guild.id,
95
+ is_guild_setting: isGuildSetting,
96
+ };
97
+
98
+ if (isGuildSetting) {
99
+ payload.user_id = null;
100
+ }
101
+
102
+ // Try UPDATE first (safe with RLS). If nothing updated, INSERT.
103
+ const updateQuery = this.supabase
104
+ .from('plugin_settings')
105
+ .update({ settings })
106
+ .eq('plugin_id', this.pluginId)
107
+ .eq('guild_id', this.guild.id)
108
+ .eq('is_guild_setting', isGuildSetting);
109
+
110
+ const { data: updatedRows, error: updateError } = await (isGuildSetting
111
+ ? updateQuery.is('user_id', null).select('id')
112
+ : updateQuery.select('id'));
113
+
114
+ if (updateError) {
115
+ if (updateError.code === '42501' || updateError.message?.includes('policy')) {
116
+ throw new Error(`Cannot set ${isGuildSetting ? 'guild' : 'user'} settings: Permission denied.`);
117
+ }
118
+ // proceed to try insert in case of other issues
119
+ }
120
+
121
+ if (updatedRows && updatedRows.length > 0) {
122
+ return; // updated successfully
123
+ }
124
+
125
+ // No row updated -> INSERT
126
+ const { error: insertError } = await this.supabase.from('plugin_settings').insert(payload);
127
+
128
+ if (insertError) {
129
+ // In case of race condition (duplicate), try one more UPDATE
130
+ if (insertError.code === '23505' /* unique_violation */) {
131
+ const retry = this.supabase
132
+ .from('plugin_settings')
133
+ .update({ settings })
134
+ .eq('plugin_id', this.pluginId)
135
+ .eq('guild_id', this.guild.id)
136
+ .eq('is_guild_setting', isGuildSetting);
137
+ const { error: retryError } = await (isGuildSetting ? retry.is('user_id', null) : retry);
138
+ if (!retryError) return;
139
+ }
140
+
141
+ throw insertError;
142
+ }
70
143
  }
71
144
 
72
145
  /**
73
146
  * Get the settings for the plugin. T can be any type of settings, UserSettings or SystemSettings.
74
147
  * @param defaultSettings The default settings to use if no settings are found.
75
- * @returns The settings for the plugin.
148
+ * @returns The settings for the plugin.
76
149
  */
77
150
  public async getSettings<T extends object>(defaultSettings: T): Promise<T> {
78
- const storedSettings = await this.fetchSettings() as T | null;
151
+ const storedSettings = (await this.fetchSettings()) as T | null;
79
152
 
80
153
  if (!storedSettings) {
81
154
  await this.setSettings(defaultSettings);
@@ -88,7 +161,7 @@ export class SettingsController {
88
161
 
89
162
  if (storedKeys.length !== defaultKeys.length) {
90
163
  const validStoredSettings = Object.fromEntries(
91
- Object.entries(storedSettings).filter(([key]) => defaultKeys.includes(key))
164
+ Object.entries(storedSettings).filter(([key]) => defaultKeys.includes(key)),
92
165
  );
93
166
  const mergedSettings = { ...defaultSettings, ...validStoredSettings } as T;
94
167