@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,30 +1,23 @@
1
1
  import { PostgrestQueryBuilder } from "@supabase/postgrest-js";
2
2
  import { SupabaseClient } from "@supabase/supabase-js";
3
3
  import { GenericSchema } from "@supabase/supabase-js/dist/module/lib/types";
4
- import { generateText, Message, OnLLMResponse, streamChatGPT, Tool } from "../controller/AIController";
5
- import { generateObject as generateObjectFunction, ObjectRequest } from "../controller/ObjectController";
6
- import { SettingsController, UserInfo } from "../controller/SettingsController";
7
- import { SharedContent, SharedContentController, SharedContentFilter, SharedContentObjectRequest } from "../controller/SharedContentController";
8
- import { getPlugins } from "../controller/SidePluginController";
9
- import { getSTTResponse, getTTSResponse } from "../controller/VoiceController";
4
+ import { generateText, Message, OnLLMResponse, streamChatGPT } from "../core/controller/AIController";
5
+ import { generateObject, ObjectRequest } from "../core/controller/ObjectController";
6
+ import { SettingsController, UserInfo } from "../core/controller/SettingsController";
7
+ import { SharedContent, SharedContentController, SharedContentFilter, SharedContentObjectRequest } from "../core/controller/SharedContentController";
8
+ import { getSTTResponse, getTTSResponse } from "../core/controller/VoiceController";
9
+ import { EventBus, EventBusMessage, EventHandler, EventPayload } from "../fromRimori/EventBus";
10
+ import { ActivePlugin, MainPanelAction, Plugin, Tool } from "../fromRimori/PluginTypes";
10
11
  import { AccomplishmentHandler, AccomplishmentPayload } from "./AccomplishmentHandler";
11
- import { EventBus, EventBusMessage, EventHandler, EventPayload } from "./fromRimori/EventBus";
12
- import { Plugin } from "./fromRimori/PluginTypes";
13
- import { PluginController } from "./PluginController";
12
+ import { PluginController, RimoriInfo } from "./PluginController";
14
13
 
15
- interface RimoriClientOptions {
16
- pluginController: PluginController;
17
- supabase: SupabaseClient;
18
- tablePrefix: string;
19
- pluginId: string;
20
- }
21
14
 
22
15
  interface Db {
23
16
  from: {
24
17
  <TableName extends string & keyof GenericSchema['Tables'], Table extends GenericSchema['Tables'][TableName]>(relation: TableName): PostgrestQueryBuilder<GenericSchema, Table, TableName>;
25
18
  <ViewName extends string & keyof GenericSchema['Views'], View extends GenericSchema['Views'][ViewName]>(relation: ViewName): PostgrestQueryBuilder<GenericSchema, View, ViewName>;
26
19
  };
27
- storage: SupabaseClient["storage"];
20
+ // storage: SupabaseClient["storage"];
28
21
 
29
22
  // functions: SupabaseClient["functions"];
30
23
  /**
@@ -51,11 +44,26 @@ interface PluginInterface {
51
44
  */
52
45
  getSettings: <T extends object>(defaultSettings: T) => Promise<T>;
53
46
  /**
54
- * Fetches all installed plugins.
55
- * @returns A promise that resolves to an array of plugins
56
- */
57
- getInstalled: () => Promise<Plugin[]>;
58
- getUserInfo: () => Promise<UserInfo>;
47
+ * Retrieves information about plugins, including:
48
+ * - All installed plugins
49
+ * - The currently active plugin in the main panel
50
+ * - The currently active plugin in the side panel
51
+ */
52
+ getPluginInfo: () => {
53
+ /**
54
+ * All installed plugins.
55
+ */
56
+ installedPlugins: Plugin[],
57
+ /**
58
+ * The plugin that is loaded in the main panel.
59
+ */
60
+ mainPanelPlugin?: ActivePlugin,
61
+ /**
62
+ * The plugin that is loaded in the side panel.
63
+ */
64
+ sidePanelPlugin?: ActivePlugin,
65
+ };
66
+ getUserInfo: () => UserInfo;
59
67
  }
60
68
 
61
69
  export class RimoriClient {
@@ -65,40 +73,44 @@ export class RimoriClient {
65
73
  private settingsController: SettingsController;
66
74
  private sharedContentController: SharedContentController;
67
75
  private accomplishmentHandler: AccomplishmentHandler;
68
- private supabaseUrl: string;
69
- public db: Db;
76
+ private rimoriInfo: RimoriInfo;
70
77
  public plugin: PluginInterface;
78
+ public db: Db;
71
79
 
72
- private constructor(options: RimoriClientOptions) {
73
- this.superbase = options.supabase;
74
- this.pluginController = options.pluginController;
75
- this.settingsController = new SettingsController(options.supabase, options.pluginId);
80
+ private constructor(supabase: SupabaseClient, info: RimoriInfo, pluginController: PluginController) {
81
+ this.rimoriInfo = info;
82
+ this.superbase = supabase;
83
+ this.pluginController = pluginController;
84
+ this.settingsController = new SettingsController(supabase, info.pluginId);
76
85
  this.sharedContentController = new SharedContentController(this.superbase, this);
77
- this.supabaseUrl = this.pluginController.getSupabaseUrl();
78
- this.accomplishmentHandler = new AccomplishmentHandler(options.pluginId);
86
+ this.accomplishmentHandler = new AccomplishmentHandler(info.pluginId);
79
87
 
80
88
  this.from = this.from.bind(this);
81
89
 
82
90
  this.db = {
83
91
  from: this.from,
84
- storage: this.superbase.storage,
92
+ // storage: this.superbase.storage,
85
93
  // functions: this.superbase.functions,
86
- tablePrefix: options.tablePrefix,
94
+ tablePrefix: info.tablePrefix,
87
95
  getTableName: this.getTableName.bind(this),
88
96
  }
89
97
  this.plugin = {
90
- pluginId: options.pluginId,
98
+ pluginId: info.pluginId,
91
99
  setSettings: async (settings: any) => {
92
100
  await this.settingsController.setSettings(settings);
93
101
  },
94
102
  getSettings: async <T extends object>(defaultSettings: T): Promise<T> => {
95
103
  return await this.settingsController.getSettings<T>(defaultSettings);
96
104
  },
97
- getInstalled: async (): Promise<Plugin[]> => {
98
- return getPlugins(this.superbase);
105
+ getUserInfo: (): UserInfo => {
106
+ return this.rimoriInfo.profile;
99
107
  },
100
- getUserInfo: async (): Promise<UserInfo> => {
101
- return this.settingsController.getUserInfo();
108
+ getPluginInfo: () => {
109
+ return {
110
+ installedPlugins: this.rimoriInfo.installedPlugins,
111
+ mainPanelPlugin: this.rimoriInfo.mainPanelPlugin,
112
+ sidePanelPlugin: this.rimoriInfo.sidePanelPlugin,
113
+ }
102
114
  }
103
115
  }
104
116
  }
@@ -132,11 +144,11 @@ export class RimoriClient {
132
144
  * Subscribe to an event.
133
145
  * @param topic The topic to subscribe to.
134
146
  * @param callback The callback to call when the event is emitted.
135
- * @returns The unsubscribe ids.
147
+ * @returns An EventListener object containing an off() method to unsubscribe the listeners.
136
148
  */
137
149
  on: <T = EventPayload>(topic: string | string[], callback: EventHandler<T>) => {
138
150
  const topics = Array.isArray(topic) ? topic : [topic];
139
- return topics.map(topic => EventBus.on<T>(this.pluginController.getGlobalEventTopic(topic), callback));
151
+ return EventBus.on<T>(topics.map(t => this.pluginController.getGlobalEventTopic(t)), callback);
140
152
  },
141
153
  /**
142
154
  * Subscribe to an event once.
@@ -151,8 +163,9 @@ export class RimoriClient {
151
163
  * @param topic The topic to respond to.
152
164
  * @param data The data to respond with.
153
165
  */
154
- respond: <T = EventPayload>(topic: string, data: EventPayload | ((data: EventBusMessage<T>) => EventPayload | Promise<EventPayload>)) => {
155
- EventBus.respond(this.plugin.pluginId, this.pluginController.getGlobalEventTopic(topic), data);
166
+ respond: <T = EventPayload>(topic: string | string[], data: EventPayload | ((data: EventBusMessage<T>) => EventPayload | Promise<EventPayload>)) => {
167
+ const topics = Array.isArray(topic) ? topic : [topic];
168
+ EventBus.respond(this.plugin.pluginId, topics.map(t => this.pluginController.getGlobalEventTopic(t)), data);
156
169
  },
157
170
  /**
158
171
  * Emit an accomplishment.
@@ -176,14 +189,35 @@ export class RimoriClient {
176
189
  * @param text Optional text to be used for the action like for example text that the translator would look up.
177
190
  */
178
191
  emitSidebarAction: (pluginId: string, actionKey: string, text?: string) => {
179
- this.event.emit("global.sidebar.triggerAction", { pluginId, actionKey, text });
192
+ this.event.emit("global.sidebar.triggerAction", { plugin_id: pluginId, action_key: actionKey, text });
193
+ },
194
+
195
+ onMainPanelAction: (callback: (data: MainPanelAction) => void) => {
196
+ // this needs to be a emit and on because the main panel action is triggered by the user and not by the plugin
197
+ this.event.emit("action.requestMain")
198
+ this.event.on<MainPanelAction>("action.requestMain", ({ data }) => callback(data));
180
199
  }
181
200
  }
182
201
 
202
+ public navigation = {
203
+ toDashboard: () => {
204
+ this.event.emit("global.navigation.triggerToDashboard");
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Get a query parameter value that was passed via MessageChannel
210
+ * @param key The query parameter key
211
+ * @returns The query parameter value or null if not found
212
+ */
213
+ public getQueryParam(key: string): string | null {
214
+ return this.pluginController.getQueryParam(key);
215
+ }
216
+
183
217
  public static async getInstance(pluginController: PluginController): Promise<RimoriClient> {
184
218
  if (!RimoriClient.instance) {
185
- const { supabase, tablePrefix, pluginId } = await pluginController.getClient();
186
- RimoriClient.instance = new RimoriClient({ pluginController, supabase, tablePrefix, pluginId });
219
+ const client = await pluginController.getClient();
220
+ RimoriClient.instance = new RimoriClient(client.supabase, client.info, pluginController);
187
221
  }
188
222
  return RimoriClient.instance;
189
223
  }
@@ -200,33 +234,53 @@ export class RimoriClient {
200
234
  return this.superbase.from(this.getTableName(relation));
201
235
  }
202
236
 
203
- private getTableName(type: string) {
204
- return this.db.tablePrefix + "_" + type;
237
+ private getTableName(table: string) {
238
+ if (/[A-Z]/.test(table)) {
239
+ throw new Error("Table name cannot include uppercase letters. Please use snake_case for table names.");
240
+ }
241
+ if (table.startsWith("global_")) {
242
+ return table.replace("global_", "");
243
+ }
244
+ return this.db.tablePrefix + "_" + table;
205
245
  }
206
246
 
207
- public llm = {
247
+ public ai = {
208
248
  getText: async (messages: Message[], tools?: Tool[]): Promise<string> => {
209
249
  const token = await this.pluginController.getToken();
210
- return generateText(this.supabaseUrl, messages, tools || [], token).then(({ messages }) => messages[0].content[0].text);
250
+ return generateText(this.pluginController.getBackendUrl(), messages, tools || [], token).then(({ messages }) => messages[0].content[0].text);
211
251
  },
212
252
  getSteamedText: async (messages: Message[], onMessage: OnLLMResponse, tools?: Tool[]) => {
213
253
  const token = await this.pluginController.getToken();
214
- streamChatGPT(this.supabaseUrl, messages, tools || [], onMessage, token);
254
+ streamChatGPT(this.pluginController.getBackendUrl(), messages, tools || [], onMessage, token);
215
255
  },
216
256
  getVoice: async (text: string, voice = "alloy", speed = 1, language?: string): Promise<Blob> => {
217
257
  const token = await this.pluginController.getToken();
218
- return getTTSResponse(this.supabaseUrl, { input: text, voice, speed, language }, token);
258
+ return getTTSResponse(this.pluginController.getBackendUrl(), { input: text, voice, speed, language }, token);
219
259
  },
220
- getTextFromVoice: (file: Blob): Promise<string> => {
221
- return getSTTResponse(this.superbase, file);
260
+ getTextFromVoice: async (file: Blob): Promise<string> => {
261
+ const token = await this.pluginController.getToken();
262
+ return getSTTResponse(this.pluginController.getBackendUrl(), file, token);
222
263
  },
223
264
  getObject: async (request: ObjectRequest): Promise<any> => {
224
265
  const token = await this.pluginController.getToken();
225
- return generateObjectFunction(this.supabaseUrl, request, token);
266
+ return generateObject(this.pluginController.getBackendUrl(), request, token);
226
267
  },
227
268
  // getSteamedObject: this.generateObjectStream,
228
269
  }
229
270
 
271
+ public runtime = {
272
+ fetchBackend: async (url: string, options: RequestInit) => {
273
+ const token = await this.pluginController.getToken();
274
+ return fetch(this.pluginController.getBackendUrl() + url, {
275
+ ...options,
276
+ headers: {
277
+ ...options.headers,
278
+ 'Authorization': `Bearer ${token}`
279
+ }
280
+ });
281
+ }
282
+ }
283
+
230
284
  public community = {
231
285
  /**
232
286
  * Shared content is a way to share completable content with other users using this plugin.
@@ -258,16 +312,20 @@ export class RimoriClient {
258
312
  * @param contentType The type of shared content to fetch. E.g. assignments, exercises, etc.
259
313
  * @param generatorInstructions The instructions for the creation of new shared content. The object will automatically be extended with a tool property with a topic and keywords property to let a new unique topic be generated.
260
314
  * @param filter The optional additional filter for checking new shared content based on a column and value. This is useful if the aditional information stored on the shared content is used to further narrow down the kind of shared content wanted to be received. E.g. only adjective grammar exercises.
261
- * @param privateTopic An optional flag to indicate if the topic should be private and only be visible to the user. This is useful if the topic is not meant to be shared with other users. Like for personal topics or if the content is based on the personal study goal.
315
+ * @param options An optional object with options for the new shared content.
316
+ * @param options.privateTopic An optional flag to indicate if the topic should be private and only be visible to the user. This is useful if the topic is not meant to be shared with other users. Like for personal topics or if the content is based on the personal study goal.
317
+ * @param options.skipDbSave An optional flag to indicate if the new shared content should not be saved to the database. This is useful if the new shared content is not meant to be saved to the database.
318
+ * @param options.alwaysGenerateNew An optional flag to indicate if the new shared content should always be generated even if there is already a content with the same filter. This is useful if the new shared content is not meant to be saved to the database.
319
+ * @param options.excludeIds An optional list of ids to exclude from the selection. This is useful if the new shared content is not meant to be saved to the database.
262
320
  * @returns The new shared content.
263
321
  */
264
322
  getNew: async <T = any>(
265
323
  contentType: string,
266
324
  generatorInstructions: SharedContentObjectRequest,
267
325
  filter?: SharedContentFilter,
268
- privateTopic?: boolean,
326
+ options?: { privateTopic?: boolean; skipDbSave?: boolean; alwaysGenerateNew?: boolean; excludeIds?: string[] },
269
327
  ): Promise<SharedContent<T>> => {
270
- return await this.sharedContentController.getNewSharedContent(contentType, generatorInstructions, filter, privateTopic);
328
+ return await this.sharedContentController.getNewSharedContent(contentType, generatorInstructions, filter, options);
271
329
  },
272
330
  /**
273
331
  * Create a new shared content item.
@@ -294,6 +352,20 @@ export class RimoriClient {
294
352
  complete: async (contentType: string, assignmentId: string) => {
295
353
  return await this.sharedContentController.completeSharedContent(contentType, assignmentId);
296
354
  },
355
+ /**
356
+ /**
357
+ * Update the state of a shared content item for a specific user.
358
+ * Useful for marking content as completed, ongoing, hidden, liked, disliked, or bookmarked.
359
+ */
360
+ updateState: async (params: {
361
+ contentType: string
362
+ id: string
363
+ state?: 'completed' | 'ongoing' | 'hidden'
364
+ reaction?: 'liked' | 'disliked' | null
365
+ bookmarked?: boolean
366
+ }): Promise<void> => {
367
+ return await this.sharedContentController.updateSharedContentState(params);
368
+ },
297
369
  /**
298
370
  * Remove a shared content item.
299
371
  * @param id The id of the shared content item to remove.
@@ -1,9 +1,11 @@
1
- import { EventBus } from "./fromRimori/EventBus";
2
1
  import { createClient, SupabaseClient } from "@supabase/supabase-js";
2
+ import { EventBus } from "../fromRimori/EventBus";
3
+ import { DEFAULT_ANON_KEY, DEFAULT_ENDPOINT } from "../utils/endpoint";
3
4
 
4
5
  export interface StandaloneConfig {
5
6
  url: string,
6
- key: string
7
+ key: string,
8
+ backendUrl?: string
7
9
  }
8
10
 
9
11
  export class StandaloneClient {
@@ -18,14 +20,14 @@ export class StandaloneClient {
18
20
 
19
21
  public static async getInstance(): Promise<StandaloneClient> {
20
22
  if (!StandaloneClient.instance) {
21
- const config = await fetch("http://localhost:3000/config.json").then(res => res.json()).catch(err => {
23
+ const config = await fetch("https://app.rimori.se/config.json").then(res => res.json()).catch(err => {
22
24
  console.warn("Error fetching config.json, using default values", err);
23
- return {
24
- SUPABASE_URL: "https://pheptqdoqsdnadgoihvr.supabase.co",
25
- SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBoZXB0cWRvcXNkbmFkZ29paHZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzE2OTY2ODcsImV4cCI6MjA0NzI3MjY4N30.4GPFAXTF8685FaXISdAPNCIM-H3RGLo8GbyhQpu1mP0",
26
- }
27
25
  });
28
- StandaloneClient.instance = new StandaloneClient({ url: config.SUPABASE_URL, key: config.SUPABASE_ANON_KEY });
26
+ StandaloneClient.instance = new StandaloneClient({
27
+ url: config?.SUPABASE_URL || DEFAULT_ENDPOINT,
28
+ key: config?.SUPABASE_ANON_KEY || DEFAULT_ANON_KEY,
29
+ backendUrl: config?.BACKEND_URL || 'https://api.rimori.se',
30
+ });
29
31
  }
30
32
  return StandaloneClient.instance;
31
33
  }
@@ -58,15 +60,32 @@ export class StandaloneClient {
58
60
  EventBus.respond("standalone", "global.supabase.requestAccess", async () => {
59
61
  const session = await supabase.auth.getSession();
60
62
  console.log("session", session);
61
- const { data, error } = await supabase.functions.invoke("plugin-token", { headers: { authorization: `Bearer ${session.data.session?.access_token}` } });
62
- if (error) {
63
- throw new Error("Failed to get plugin token. " + error.message);
63
+
64
+ // Call the NestJS backend endpoint instead of the Supabase edge function
65
+ const response = await fetch(`${config.backendUrl}/plugin/token`, {
66
+ method: 'POST',
67
+ headers: {
68
+ 'Content-Type': 'application/json',
69
+ 'Authorization': `Bearer ${session.data.session?.access_token}`
70
+ },
71
+ body: JSON.stringify({
72
+ pluginId: pluginId
73
+ })
74
+ });
75
+
76
+ if (!response.ok) {
77
+ const errorText = await response.text();
78
+ throw new Error(`Failed to get plugin token. ${response.status}: ${errorText}`);
64
79
  }
80
+
81
+ const data = await response.json();
82
+
65
83
  return {
66
84
  token: data.token,
67
85
  pluginId: pluginId,
68
86
  url: config.url,
69
87
  key: config.key,
88
+ backendUrl: config.backendUrl,
70
89
  tablePrefix: pluginId,
71
90
  expiration: new Date(Date.now() + 1000 * 60 * 60 * 1.5), // 1.5 hours
72
91
  }
@@ -1,16 +1,23 @@
1
- export function setTheme() {
2
- const urlParams = new URLSearchParams(window.location.search);
3
-
4
- let theme = urlParams.get('theme');
5
- if (!theme || theme === 'system') {
6
- theme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
7
- }
8
-
1
+ export function setTheme(theme?: string | null) {
9
2
  document.documentElement.classList.add("dark:text-gray-200");
10
3
 
11
- if (theme === 'dark') {
4
+ if (isDarkTheme(theme)) {
12
5
  document.documentElement.setAttribute("data-theme", "dark");
13
6
  document.documentElement.classList.add('dark', "dark:bg-gray-950");
14
7
  document.documentElement.style.background = "hsl(var(--background))";
15
8
  }
9
+ }
10
+
11
+ export function isDarkTheme(theme?: string | null): boolean {
12
+ // If no theme provided, try to get from URL as fallback (for standalone mode)
13
+ if (!theme) {
14
+ const urlParams = new URLSearchParams(window.location.search);
15
+ theme = urlParams.get('theme');
16
+ }
17
+
18
+ if (!theme || theme === 'system') {
19
+ return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
20
+ }
21
+
22
+ return theme === 'dark';
16
23
  }
@@ -1,24 +1,35 @@
1
1
  import React, { createContext, useContext, ReactNode, useEffect, useState } from 'react';
2
2
  import { PluginController } from '../plugin/PluginController';
3
3
  import { RimoriClient } from '../plugin/RimoriClient';
4
- import { EventBusHandler } from '../plugin/fromRimori/EventBus';
5
- import ContextMenu from '../core/components/ContextMenu';
4
+ import { EventBusHandler } from '../fromRimori/EventBus';
5
+ import ContextMenu from '../components/components/ContextMenu';
6
6
  import { StandaloneClient } from '../plugin/StandaloneClient';
7
7
 
8
8
  interface PluginProviderProps {
9
9
  children: ReactNode;
10
10
  pluginId: string;
11
+ settings?: {
12
+ disableContextMenu?: boolean;
13
+ }
11
14
  }
12
15
 
13
16
  const PluginContext = createContext<RimoriClient | null>(null);
14
17
 
15
- export const PluginProvider: React.FC<PluginProviderProps> = ({ children, pluginId }) => {
18
+ export const PluginProvider: React.FC<PluginProviderProps> = ({ children, pluginId, settings }) => {
16
19
  const [plugin, setPlugin] = useState<RimoriClient | null>(null);
17
20
  const [standaloneClient, setStandaloneClient] = useState<StandaloneClient | boolean>(false);
21
+ const [applicationMode, setApplicationMode] = useState<string | null>(null);
22
+ const [theme, setTheme] = useState<string | null>(null);
23
+
24
+ const isSidebar = applicationMode === "sidebar";
25
+ const isSettings = applicationMode === "settings";
18
26
 
19
27
  useEffect(() => {
20
28
  initEventBus(pluginId);
21
- const standaloneDetected = new URLSearchParams(window.location.search).get("secret") === null;
29
+
30
+ // Check if we're in an iframe context - if not, we're standalone
31
+ const standaloneDetected = window === window.parent;
32
+
22
33
  if (standaloneDetected && !standaloneClient) {
23
34
  StandaloneClient.getInstance().then(client => {
24
35
  client.needsLogin().then((needLogin) => setStandaloneClient(needLogin ? client : true));
@@ -26,7 +37,16 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children, plugin
26
37
  }
27
38
 
28
39
  if ((!standaloneDetected && !plugin) || (standaloneDetected && standaloneClient === true)) {
29
- PluginController.getInstance(pluginId, standaloneDetected).then(setPlugin);
40
+ PluginController.getInstance(pluginId, standaloneDetected).then(client => {
41
+ setPlugin(client);
42
+ // Get applicationMode and theme from MessageChannel query params
43
+ if (!standaloneDetected) {
44
+ const mode = client.getQueryParam("applicationMode");
45
+ const themeParam = client.getQueryParam("rm_theme");
46
+ setApplicationMode(mode);
47
+ setTheme(themeParam);
48
+ }
49
+ });
30
50
  }
31
51
  }, [pluginId, standaloneClient]);
32
52
 
@@ -34,11 +54,10 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children, plugin
34
54
  useEffect(() => {
35
55
  if (!plugin) return;
36
56
 
37
- const url = new URL(window.location.href);
38
57
  //sidebar pages should not report url changes
39
- if (url.searchParams.get("applicationMode") === "sidebar") return;
58
+ if (isSidebar) return;
40
59
 
41
- let lastHash = url.hash;
60
+ let lastHash = window.location.hash;
42
61
  const emitUrlChange = (url: string) => plugin.event.emit('session.triggerUrlChange', { url });
43
62
 
44
63
  const interval = setInterval(() => {
@@ -73,23 +92,37 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children, plugin
73
92
 
74
93
  return (
75
94
  <PluginContext.Provider value={plugin}>
76
- <ContextMenu client={plugin} />
95
+ {!settings?.disableContextMenu && !isSidebar && !isSettings && <ContextMenu client={plugin} />}
77
96
  {children}
78
97
  </PluginContext.Provider>
79
98
  );
80
99
  };
81
100
 
82
- export const usePlugin = () => {
101
+ export const useRimori = () => {
83
102
  const context = useContext(PluginContext);
84
103
  if (context === null) {
85
- throw new Error('usePlugin must be used within an PluginProvider');
104
+ throw new Error('useRimori must be used within an PluginProvider');
86
105
  }
87
106
  return context;
88
107
  };
89
108
 
90
- function initEventBus(pluginId: string) {
109
+ function getUrlParam(name: string) {
110
+ // First try to get from URL hash query params (for compatibility)
111
+ const hashParts = window.location.hash.split('?');
112
+ if (hashParts.length > 1) {
113
+ const hashParams = new URLSearchParams(hashParts[1]);
114
+ const hashValue = hashParams.get(name);
115
+ if (hashValue) return hashValue;
116
+ }
117
+
118
+ // Fallback to regular URL search params
91
119
  const url = new URL(window.location.href);
92
- const isSidebar = url.searchParams.get("applicationMode") === "sidebar";
120
+ return url.searchParams.get(name);
121
+ }
122
+
123
+ function initEventBus(pluginId: string) {
124
+ // For now, use URL fallback for EventBus naming - this will be updated once MessageChannel is ready
125
+ const isSidebar = getUrlParam("applicationMode") === "sidebar";
93
126
  EventBusHandler.getInstance("Plugin EventBus " + pluginId + " " + (isSidebar ? "sidebar" : "main"));
94
127
  }
95
128
 
@@ -63,8 +63,10 @@ export type Language = keyof typeof languageKeys;
63
63
  /**
64
64
  * Get the language name from the language code
65
65
  * @param languageCode The code of the language
66
+ * @param capitalize Whether to capitalize the first letter of the language name
66
67
  * @returns The language name
67
68
  */
68
- export function getLanguageName(languageCode: Language): string {
69
- return languageKeys[languageCode];
69
+ export function getLanguageName(languageCode: Language, capitalize: boolean = false): string {
70
+ const lang = languageKeys[languageCode];
71
+ return capitalize ? lang.charAt(0).toUpperCase() + lang.slice(1) : lang;
70
72
  }
@@ -3,13 +3,13 @@ const codes = ["Pre-A1", "A1", "A2", "B1", "B2", "C1", "C2", "Post-C2"];
3
3
  export type LanguageLevel = "Pre-A1" | "A1" | "A2" | "B1" | "B2" | "C1" | "C2" | "Post-C2";
4
4
 
5
5
  export function getDifficultyLevel(difficulty: LanguageLevel): number {
6
- return codes.indexOf(difficulty) + 1;
6
+ return codes.indexOf(difficulty) + 1;
7
7
  }
8
8
 
9
9
  export function getDifficultyLabel(difficulty: number): LanguageLevel {
10
- return codes[difficulty] as LanguageLevel;
10
+ return codes[difficulty] as LanguageLevel;
11
11
  }
12
12
 
13
13
  export function getNeighborDifficultyLevel(difficulty: LanguageLevel, difficultyAdjustment: number): LanguageLevel {
14
- return getDifficultyLabel(getDifficultyLevel(difficulty) + difficultyAdjustment);
14
+ return getDifficultyLabel(getDifficultyLevel(difficulty) + difficultyAdjustment - 1);
15
15
  }
@@ -0,0 +1,2 @@
1
+ export const DEFAULT_ENDPOINT = "https://pheptqdoqsdnadgoihvr.supabase.co";
2
+ export const DEFAULT_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBoZXB0cWRvcXNkbmFkZ29paHZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzE2OTY2ODcsImV4cCI6MjA0NzI3MjY4N30.4GPFAXTF8685FaXISdAPNCIM-H3RGLo8GbyhQpu1mP0";
@@ -1,35 +1,22 @@
1
1
  import { RimoriClient } from "../plugin/RimoriClient";
2
+ import { EventBusHandler } from "../fromRimori/EventBus";
2
3
  import { PluginController } from "../plugin/PluginController";
3
- import { EventBus, EventBusHandler, EventBusMessage } from "../plugin/fromRimori/EventBus";
4
-
5
- let controller: RimoriClient | null = null;
6
- const listeners: ((event: { data: { event: EventBusMessage, secret: string } }) => void)[] = [];
7
- let debugEnabled = false;
8
4
 
9
5
  /**
10
6
  * Sets up the web worker for the plugin to be able receive and send messages to Rimori.
11
- * @param init - The function containing the subscription logic.
7
+ * @param pluginId - The id of the plugin to setup the worker for.
8
+ * @param init - The function containing the initialization logic.
12
9
  */
13
- export function setupWorker(init: (controller: RimoriClient) => void | Promise<void>) {
10
+ export async function setupWorker(pluginId: string, init: (client: RimoriClient) => void | Promise<void>) {
11
+
14
12
  // Mock of the window object for the worker context to be able to use the PluginController.
15
13
  const mockWindow = {
16
14
  isWorker: true,
17
- location: { search: '?secret=123' },
15
+ location: {},
18
16
  parent: {
19
- postMessage: (message: { event: EventBusMessage }) => {
20
- message.event.sender = "worker." + message.event.sender;
21
- checkDebugMode(message.event);
22
- logIfDebug('sending event to Rimori', message.event);
23
- self.postMessage(message)
24
- }
25
- },
26
- addEventListener: (_: string, listener: any) => {
27
- listeners.push(listener);
28
- },
29
- APP_CONFIG: {
30
- SUPABASE_URL: 'NOT_SET',
31
- SUPABASE_ANON_KEY: 'NOT_SET',
17
+ postMessage: () => { }
32
18
  },
19
+ addEventListener: () => { }
33
20
  };
34
21
 
35
22
  // Assign the mock to globalThis.
@@ -37,45 +24,11 @@ export function setupWorker(init: (controller: RimoriClient) => void | Promise<v
37
24
 
38
25
  EventBusHandler.getInstance("Worker EventBus");
39
26
 
40
- // Handle init message from Rimori.
41
- self.onmessage = async (response: MessageEvent) => {
42
- checkDebugMode(response.data);
43
- logIfDebug('Message received', response.data);
44
-
45
- const event = response.data as EventBusMessage;
46
-
47
- if (event.topic === 'global.worker.requestInit') {
48
- if (!controller) {
49
- mockWindow.APP_CONFIG.SUPABASE_URL = event.data.supabaseUrl;
50
- mockWindow.APP_CONFIG.SUPABASE_ANON_KEY = event.data.supabaseAnonKey;
51
- controller = await PluginController.getInstance(event.data.pluginId);
52
- logIfDebug('Worker initialized.');
53
- await init(controller);
54
- logIfDebug('Plugin listeners initialized.');
55
- }
56
- const initEvent: EventBusMessage = {
57
- timestamp: new Date().toISOString(),
58
- eventId: event.eventId,
59
- sender: "worker." + event.sender,
60
- topic: 'global.worker.requestInit',
61
- data: { success: true },
62
- debug: debugEnabled
63
- };
64
- return self.postMessage({ secret: "123", event: initEvent });
65
- }
66
- listeners.forEach(listener => listener({ data: { event: response.data, secret: "123" } }));
67
- };
68
- }
27
+ const rimoriClient = await PluginController.getInstance(pluginId);
28
+ console.debug('[Worker] RimoriClient initialized.');
69
29
 
70
- function checkDebugMode(event: EventBusMessage) {
71
- if (event.topic === 'global.system.requestDebug' || event.debug) {
72
- debugEnabled = true;
73
- EventBus.emit("worker", "global.system.requestDebug");
74
- }
75
- }
30
+ await init(rimoriClient);
31
+ console.debug('[Worker] Worker initialized.');
76
32
 
77
- function logIfDebug(...args: any[]) {
78
- if (debugEnabled) {
79
- console.debug('[Worker] ' + args[0], ...args.slice(1));
80
- }
33
+ self.postMessage({ type: "rimori:acknowledged", pluginId: pluginId });
81
34
  }