@rimori/client 1.2.0 → 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 (75) hide show
  1. package/README.md +61 -18
  2. package/dist/cli/scripts/init/dev-registration.js +0 -1
  3. package/dist/cli/scripts/init/main.d.ts +1 -1
  4. package/dist/cli/scripts/init/main.js +1 -0
  5. package/dist/components/LoggerExample.d.ts +6 -0
  6. package/dist/components/LoggerExample.js +79 -0
  7. package/dist/components/ai/Assistant.js +2 -2
  8. package/dist/components/ai/Avatar.js +2 -2
  9. package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.js +41 -32
  10. package/dist/components/audio/Playbutton.js +2 -2
  11. package/dist/components/components/ContextMenu.js +48 -9
  12. package/dist/core/controller/AIController.js +202 -69
  13. package/dist/core/controller/AudioController.d.ts +0 -0
  14. package/dist/core/controller/AudioController.js +1 -0
  15. package/dist/core/controller/ObjectController.d.ts +2 -2
  16. package/dist/core/controller/ObjectController.js +8 -8
  17. package/dist/core/controller/SettingsController.d.ts +16 -0
  18. package/dist/core/controller/SharedContentController.d.ts +30 -2
  19. package/dist/core/controller/SharedContentController.js +74 -23
  20. package/dist/core/controller/VoiceController.d.ts +2 -3
  21. package/dist/core/controller/VoiceController.js +11 -4
  22. package/dist/core/core.d.ts +1 -0
  23. package/dist/fromRimori/EventBus.js +1 -1
  24. package/dist/fromRimori/PluginTypes.d.ts +7 -4
  25. package/dist/hooks/UseChatHook.js +6 -4
  26. package/dist/hooks/UseLogger.d.ts +30 -0
  27. package/dist/hooks/UseLogger.js +122 -0
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.js +1 -0
  30. package/dist/plugin/AudioController.d.ts +37 -0
  31. package/dist/plugin/AudioController.js +68 -0
  32. package/dist/plugin/Logger.d.ts +68 -0
  33. package/dist/plugin/Logger.js +256 -0
  34. package/dist/plugin/LoggerExample.d.ts +16 -0
  35. package/dist/plugin/LoggerExample.js +140 -0
  36. package/dist/plugin/PluginController.d.ts +15 -3
  37. package/dist/plugin/PluginController.js +162 -39
  38. package/dist/plugin/RimoriClient.d.ts +55 -13
  39. package/dist/plugin/RimoriClient.js +60 -23
  40. package/dist/plugin/StandaloneClient.d.ts +1 -0
  41. package/dist/plugin/StandaloneClient.js +16 -5
  42. package/dist/plugin/ThemeSetter.d.ts +2 -2
  43. package/dist/plugin/ThemeSetter.js +8 -5
  44. package/dist/providers/PluginProvider.d.ts +1 -1
  45. package/dist/providers/PluginProvider.js +36 -10
  46. package/dist/utils/audioFormats.d.ts +26 -0
  47. package/dist/utils/audioFormats.js +67 -0
  48. package/dist/worker/WorkerSetup.d.ts +3 -2
  49. package/dist/worker/WorkerSetup.js +22 -67
  50. package/package.json +2 -1
  51. package/src/cli/scripts/init/dev-registration.ts +0 -1
  52. package/src/cli/scripts/init/main.ts +1 -0
  53. package/src/components/ai/Assistant.tsx +2 -2
  54. package/src/components/ai/Avatar.tsx +2 -2
  55. package/src/components/ai/EmbeddedAssistent/VoiceRecoder.tsx +39 -32
  56. package/src/components/audio/Playbutton.tsx +2 -2
  57. package/src/components/components/ContextMenu.tsx +53 -9
  58. package/src/core/controller/AIController.ts +236 -75
  59. package/src/core/controller/ObjectController.ts +8 -8
  60. package/src/core/controller/SettingsController.ts +16 -0
  61. package/src/core/controller/SharedContentController.ts +87 -25
  62. package/src/core/controller/VoiceController.ts +24 -19
  63. package/src/core/core.ts +1 -0
  64. package/src/fromRimori/EventBus.ts +1 -1
  65. package/src/fromRimori/PluginTypes.ts +6 -4
  66. package/src/hooks/UseChatHook.ts +6 -4
  67. package/src/index.ts +1 -0
  68. package/src/plugin/AudioController.ts +58 -0
  69. package/src/plugin/Logger.ts +324 -0
  70. package/src/plugin/PluginController.ts +171 -43
  71. package/src/plugin/RimoriClient.ts +95 -30
  72. package/src/plugin/StandaloneClient.ts +22 -6
  73. package/src/plugin/ThemeSetter.ts +8 -5
  74. package/src/providers/PluginProvider.tsx +40 -10
  75. package/src/worker/WorkerSetup.ts +14 -63
@@ -1,10 +1,11 @@
1
1
  import { createClient, SupabaseClient } from '@supabase/supabase-js';
2
2
  import { UserInfo } from '../core/controller/SettingsController';
3
3
  import { EventBus, EventBusMessage } from '../fromRimori/EventBus';
4
- import { Plugin } from '../fromRimori/PluginTypes';
4
+ import { ActivePlugin, Plugin } from '../fromRimori/PluginTypes';
5
5
  import { RimoriClient } from "./RimoriClient";
6
6
  import { StandaloneClient } from './StandaloneClient';
7
7
  import { setTheme } from './ThemeSetter';
8
+ import { Logger } from './Logger';
8
9
 
9
10
  // Add declaration for WorkerGlobalScope
10
11
  declare const WorkerGlobalScope: any;
@@ -19,46 +20,116 @@ export interface RimoriInfo {
19
20
  pluginId: string
20
21
  installedPlugins: Plugin[]
21
22
  profile: UserInfo
23
+ mainPanelPlugin?: ActivePlugin
24
+ sidePanelPlugin?: ActivePlugin
22
25
  }
23
26
 
24
27
  export class PluginController {
25
28
  private static client: RimoriClient;
26
29
  private static instance: PluginController;
27
- private communicationSecret: string | null = null;
30
+ private port: MessagePort | null = null;
31
+ private queryParams: Record<string, string> = {};
28
32
  private supabase: SupabaseClient | null = null;
29
33
  private rimoriInfo: RimoriInfo | null = null;
30
34
  private pluginId: string;
35
+ private isMessageChannelReady: boolean = false;
36
+ private pendingRequests: Array<() => void> = [];
31
37
 
32
38
  private constructor(pluginId: string, standalone: boolean) {
33
39
  this.pluginId = pluginId;
34
40
  this.getClient = this.getClient.bind(this);
35
41
 
36
42
  if (typeof WorkerGlobalScope === 'undefined') {
37
- setTheme();
43
+ // In standalone mode, use URL fallback. In iframe mode, theme will be set after MessageChannel init
44
+ if (standalone) {
45
+ setTheme();
46
+ }
38
47
  }
39
48
 
40
- //no need to forward messages to parent in standalone mode
49
+ //no need to forward messages to parent in standalone mode or worker context
41
50
  if (standalone) return;
42
51
 
43
- window.addEventListener("message", (event) => {
44
- // console.log("client: message received", event);
45
- const { topic, sender, data, eventId } = event.data.event as EventBusMessage;
52
+ this.initMessageChannel(typeof WorkerGlobalScope !== 'undefined');
53
+ }
54
+
55
+ private initMessageChannel(worker: boolean = false) {
56
+ const listener = (event: MessageEvent) => {
57
+ console.log("[PluginController] window message", { origin: event.origin, data: event.data });
58
+ const { type, pluginId, queryParams, rimoriInfo } = event.data || {};
59
+ const [transferredPort] = event.ports || [];
60
+
61
+ if (type !== "rimori:init" || !transferredPort || pluginId !== this.pluginId) {
62
+ console.log("[PluginController] message ignored (not init or wrong plugin)", { type, pluginId, hasPort: !!transferredPort });
63
+ return;
64
+ }
65
+
66
+ this.queryParams = queryParams || {};
67
+ this.port = transferredPort;
68
+
69
+ // Initialize Supabase client immediately with provided info
70
+ if (rimoriInfo) {
71
+ this.rimoriInfo = rimoriInfo;
72
+ this.supabase = createClient(rimoriInfo.url, rimoriInfo.key, {
73
+ accessToken: () => Promise.resolve(rimoriInfo.token)
74
+ });
75
+ }
76
+
77
+ // Handle messages from parent
78
+ this.port.onmessage = ({ data }) => {
79
+ const { event, type, eventId, response, error } = data || {};
80
+
81
+ // no idea why this is needed but it works for now
82
+ if (type === 'response' && eventId) {
83
+ EventBus.emit(this.pluginId, response.topic, response.data, eventId);
84
+ } else if (type === 'error' && eventId) {
85
+ EventBus.emit(this.pluginId, 'error', { error }, eventId);
86
+ } else if (event) {
87
+ const { topic, sender, data: eventData, eventId } = event as EventBusMessage;
88
+ if (sender !== this.pluginId) {
89
+ EventBus.emit(sender, topic, eventData, eventId);
90
+ }
91
+ }
92
+ };
46
93
 
47
- // skip forwarding messages from own plugin
48
- if (sender === pluginId) return;
94
+ // Set theme from MessageChannel query params
95
+ if (!worker) {
96
+ const theme = this.queryParams['rm_theme'];
97
+ setTheme(theme);
98
+ }
49
99
 
50
- EventBus.emit(sender, topic, data, eventId);
51
- });
100
+ // Forward plugin events to parent (only after MessageChannel is ready)
101
+ EventBus.on("*", (ev) => {
102
+ if (ev.sender === this.pluginId && !ev.topic.startsWith("self.")) {
103
+ this.port?.postMessage({ event: ev });
104
+ }
105
+ });
52
106
 
53
- const secret = this.getSecret();
107
+ // Mark MessageChannel as ready and process pending requests
108
+ this.isMessageChannelReady = true;
54
109
 
55
- EventBus.on("*", (event) => {
56
- // skip messages which are not from the own plugin
57
- if (event.sender !== this.pluginId) return;
58
- if (event.topic.startsWith("self.")) return;
59
- // console.log("sending event to parent", event);
60
- window.parent.postMessage({ event, secret }, "*")
61
- });
110
+ // Process any pending requests
111
+ this.pendingRequests.forEach(request => request());
112
+ this.pendingRequests = [];
113
+ };
114
+ if (worker) {
115
+ self.onmessage = listener;
116
+ } else {
117
+ window.addEventListener("message", listener);
118
+ }
119
+ this.sendHello(worker);
120
+ }
121
+
122
+ private sendHello(isWorker: boolean = false) {
123
+ try {
124
+ const payload = { type: "rimori:hello", pluginId: this.pluginId };
125
+ if (isWorker) {
126
+ self.postMessage(payload);
127
+ } else {
128
+ window.parent.postMessage(payload, "*");
129
+ }
130
+ } catch (e) {
131
+ console.error("[PluginController] Error sending hello:", e);
132
+ }
62
133
  }
63
134
 
64
135
  public static async getInstance(pluginId: string, standalone = false): Promise<RimoriClient> {
@@ -68,56 +139,113 @@ export class PluginController {
68
139
  }
69
140
  PluginController.instance = new PluginController(pluginId, standalone);
70
141
  PluginController.client = await RimoriClient.getInstance(PluginController.instance);
142
+
143
+ //only init logger in workers and on main plugin pages
144
+ if (PluginController.instance.getQueryParam("applicationMode") !== "sidebar") {
145
+ Logger.getInstance(PluginController.client);
146
+ }
71
147
  }
72
148
  return PluginController.client;
73
149
  }
74
150
 
75
- private getSecret(): string | null {
76
- if (!this.communicationSecret) {
77
- const secret = new URLSearchParams(window.location.search).get("secret");
78
- if (!secret) {
79
- console.info("Communication secret not found in URL as query parameter");
80
- }
81
- this.communicationSecret = secret;
82
- }
83
- return this.communicationSecret;
151
+ public getQueryParam(key: string): string | null {
152
+ return this.queryParams[key] || null;
84
153
  }
85
154
 
86
155
  public async getClient(): Promise<{ supabase: SupabaseClient, info: RimoriInfo }> {
87
- if (
88
- this.supabase &&
89
- this.rimoriInfo &&
90
- this.rimoriInfo.expiration > new Date()
91
- ) {
156
+ // Return cached client if valid
157
+ if (this.supabase && this.rimoriInfo && this.rimoriInfo.expiration > new Date()) {
92
158
  return { supabase: this.supabase, info: this.rimoriInfo };
93
159
  }
94
160
 
95
- const { data } = await EventBus.request<RimoriInfo>(this.pluginId, "global.supabase.requestAccess");
96
- this.rimoriInfo = data;
97
- this.supabase = createClient(this.rimoriInfo.url, this.rimoriInfo.key, {
98
- accessToken: () => Promise.resolve(this.getToken())
99
- });
161
+ // If MessageChannel is not ready yet, queue the request
162
+ if (!this.isMessageChannelReady) {
163
+ return new Promise<{ supabase: SupabaseClient, info: RimoriInfo }>((resolve) => {
164
+ this.pendingRequests.push(async () => {
165
+ const result = await this.getClient();
166
+ resolve(result);
167
+ });
168
+ });
169
+ }
170
+
171
+ // If we have rimoriInfo from MessageChannel init, use it directly
172
+ if (this.rimoriInfo && this.supabase) {
173
+ return { supabase: this.supabase, info: this.rimoriInfo };
174
+ }
175
+
176
+ // Fallback: request from parent
177
+ if (!this.rimoriInfo) {
178
+ if (typeof WorkerGlobalScope !== 'undefined') {
179
+ // In worker context, send request via self.postMessage to WorkerHandler
180
+ const eventId = Math.floor(Math.random() * 1000000000);
181
+ const requestEvent = {
182
+ event: {
183
+ timestamp: new Date().toISOString(),
184
+ eventId,
185
+ sender: this.pluginId,
186
+ topic: 'global.supabase.requestAccess',
187
+ data: {},
188
+ debug: false
189
+ }
190
+ };
191
+
192
+ return new Promise<{ supabase: SupabaseClient, info: RimoriInfo }>((resolve) => {
193
+ // Listen for the response
194
+ const originalOnMessage = self.onmessage;
195
+ self.onmessage = (event) => {
196
+ if (event.data?.topic === 'global.supabase.requestAccess' && event.data?.eventId === eventId) {
197
+ this.rimoriInfo = event.data.data;
198
+ this.supabase = createClient(this.rimoriInfo!.url, this.rimoriInfo!.key, {
199
+ accessToken: () => Promise.resolve(this.getToken())
200
+ });
201
+ self.onmessage = originalOnMessage; // Restore original handler
202
+ resolve({ supabase: this.supabase, info: this.rimoriInfo! });
203
+ } else if (originalOnMessage) {
204
+ originalOnMessage.call(self, event);
205
+ }
206
+ };
100
207
 
101
- return { supabase: this.supabase, info: this.rimoriInfo };
208
+ // Send the request
209
+ self.postMessage(requestEvent);
210
+ });
211
+ } else {
212
+ // In main thread context, use EventBus
213
+ const { data } = await EventBus.request<RimoriInfo>(this.pluginId, "global.supabase.requestAccess");
214
+ this.rimoriInfo = data;
215
+ this.supabase = createClient(this.rimoriInfo.url, this.rimoriInfo.key, {
216
+ accessToken: () => Promise.resolve(this.getToken())
217
+ });
218
+ }
219
+ }
220
+
221
+ return { supabase: this.supabase!, info: this.rimoriInfo };
102
222
  }
103
223
 
104
- public async getToken() {
224
+ public async getToken(): Promise<string> {
105
225
  if (this.rimoriInfo && this.rimoriInfo.expiration && this.rimoriInfo.expiration > new Date()) {
106
226
  return this.rimoriInfo.token;
107
227
  }
108
228
 
109
- const { data } = await EventBus.request<{ token: string, expiration: Date }>(this.pluginId, "global.supabase.requestAccess");
110
-
229
+ // If we don't have rimoriInfo, request it
111
230
  if (!this.rimoriInfo) {
112
- throw new Error("Supabase info not found");
231
+ const { data } = await EventBus.request<RimoriInfo>(this.pluginId, "global.supabase.requestAccess");
232
+ this.rimoriInfo = data;
233
+ return this.rimoriInfo.token;
113
234
  }
114
235
 
236
+ // If token is expired, request fresh access
237
+ const { data } = await EventBus.request<{ token: string, expiration: Date }>(this.pluginId, "global.supabase.requestAccess");
115
238
  this.rimoriInfo.token = data.token;
116
239
  this.rimoriInfo.expiration = data.expiration;
117
240
 
118
241
  return this.rimoriInfo.token;
119
242
  }
120
243
 
244
+ /**
245
+ * Gets the Supabase URL.
246
+ * @returns The Supabase URL.
247
+ * @deprecated All endpoints should use the backend URL instead.
248
+ */
121
249
  public getSupabaseUrl() {
122
250
  if (!this.rimoriInfo) {
123
251
  throw new Error("Supabase info not found");
@@ -2,12 +2,12 @@ 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
4
  import { generateText, Message, OnLLMResponse, streamChatGPT } from "../core/controller/AIController";
5
- import { generateObject as generateObjectFunction, ObjectRequest } from "../core/controller/ObjectController";
5
+ import { generateObject, ObjectRequest } from "../core/controller/ObjectController";
6
6
  import { SettingsController, UserInfo } from "../core/controller/SettingsController";
7
7
  import { SharedContent, SharedContentController, SharedContentFilter, SharedContentObjectRequest } from "../core/controller/SharedContentController";
8
8
  import { getSTTResponse, getTTSResponse } from "../core/controller/VoiceController";
9
9
  import { EventBus, EventBusMessage, EventHandler, EventPayload } from "../fromRimori/EventBus";
10
- import { Plugin, Tool } from "../fromRimori/PluginTypes";
10
+ import { ActivePlugin, MainPanelAction, Plugin, Tool } from "../fromRimori/PluginTypes";
11
11
  import { AccomplishmentHandler, AccomplishmentPayload } from "./AccomplishmentHandler";
12
12
  import { PluginController, RimoriInfo } from "./PluginController";
13
13
 
@@ -17,7 +17,7 @@ interface Db {
17
17
  <TableName extends string & keyof GenericSchema['Tables'], Table extends GenericSchema['Tables'][TableName]>(relation: TableName): PostgrestQueryBuilder<GenericSchema, Table, TableName>;
18
18
  <ViewName extends string & keyof GenericSchema['Views'], View extends GenericSchema['Views'][ViewName]>(relation: ViewName): PostgrestQueryBuilder<GenericSchema, View, ViewName>;
19
19
  };
20
- storage: SupabaseClient["storage"];
20
+ // storage: SupabaseClient["storage"];
21
21
 
22
22
  // functions: SupabaseClient["functions"];
23
23
  /**
@@ -44,11 +44,26 @@ interface PluginInterface {
44
44
  */
45
45
  getSettings: <T extends object>(defaultSettings: T) => Promise<T>;
46
46
  /**
47
- * Fetches all installed plugins.
48
- * @returns A promise that resolves to an array of plugins
49
- */
50
- getInstalled: () => Promise<Plugin[]>;
51
- 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;
52
67
  }
53
68
 
54
69
  export class RimoriClient {
@@ -58,27 +73,23 @@ export class RimoriClient {
58
73
  private settingsController: SettingsController;
59
74
  private sharedContentController: SharedContentController;
60
75
  private accomplishmentHandler: AccomplishmentHandler;
61
- private supabaseUrl: string;
62
- private installedPlugins: Plugin[];
63
- private profile: UserInfo;
76
+ private rimoriInfo: RimoriInfo;
64
77
  public plugin: PluginInterface;
65
78
  public db: Db;
66
79
 
67
80
  private constructor(supabase: SupabaseClient, info: RimoriInfo, pluginController: PluginController) {
81
+ this.rimoriInfo = info;
68
82
  this.superbase = supabase;
69
83
  this.pluginController = pluginController;
70
84
  this.settingsController = new SettingsController(supabase, info.pluginId);
71
85
  this.sharedContentController = new SharedContentController(this.superbase, this);
72
- this.supabaseUrl = this.pluginController.getSupabaseUrl();
73
86
  this.accomplishmentHandler = new AccomplishmentHandler(info.pluginId);
74
- this.installedPlugins = info.installedPlugins;
75
- this.profile = info.profile;
76
87
 
77
88
  this.from = this.from.bind(this);
78
89
 
79
90
  this.db = {
80
91
  from: this.from,
81
- storage: this.superbase.storage,
92
+ // storage: this.superbase.storage,
82
93
  // functions: this.superbase.functions,
83
94
  tablePrefix: info.tablePrefix,
84
95
  getTableName: this.getTableName.bind(this),
@@ -91,11 +102,15 @@ export class RimoriClient {
91
102
  getSettings: async <T extends object>(defaultSettings: T): Promise<T> => {
92
103
  return await this.settingsController.getSettings<T>(defaultSettings);
93
104
  },
94
- getInstalled: async (): Promise<Plugin[]> => {
95
- return this.installedPlugins;
105
+ getUserInfo: (): UserInfo => {
106
+ return this.rimoriInfo.profile;
96
107
  },
97
- getUserInfo: async (): Promise<UserInfo> => {
98
- return this.profile;
108
+ getPluginInfo: () => {
109
+ return {
110
+ installedPlugins: this.rimoriInfo.installedPlugins,
111
+ mainPanelPlugin: this.rimoriInfo.mainPanelPlugin,
112
+ sidePanelPlugin: this.rimoriInfo.sidePanelPlugin,
113
+ }
99
114
  }
100
115
  }
101
116
  }
@@ -175,6 +190,12 @@ export class RimoriClient {
175
190
  */
176
191
  emitSidebarAction: (pluginId: string, actionKey: string, text?: string) => {
177
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));
178
199
  }
179
200
  }
180
201
 
@@ -184,6 +205,15 @@ export class RimoriClient {
184
205
  }
185
206
  }
186
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
+
187
217
  public static async getInstance(pluginController: PluginController): Promise<RimoriClient> {
188
218
  if (!RimoriClient.instance) {
189
219
  const client = await pluginController.getClient();
@@ -204,11 +234,14 @@ export class RimoriClient {
204
234
  return this.superbase.from(this.getTableName(relation));
205
235
  }
206
236
 
207
- private getTableName(type: string) {
208
- if (type.startsWith("global_")) {
209
- return type.replace("global_", "");
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_", "");
210
243
  }
211
- return this.db.tablePrefix + "_" + type;
244
+ return this.db.tablePrefix + "_" + table;
212
245
  }
213
246
 
214
247
  public ai = {
@@ -222,18 +255,32 @@ export class RimoriClient {
222
255
  },
223
256
  getVoice: async (text: string, voice = "alloy", speed = 1, language?: string): Promise<Blob> => {
224
257
  const token = await this.pluginController.getToken();
225
- return getTTSResponse(this.supabaseUrl, { input: text, voice, speed, language }, token);
258
+ return getTTSResponse(this.pluginController.getBackendUrl(), { input: text, voice, speed, language }, token);
226
259
  },
227
- getTextFromVoice: (file: Blob): Promise<string> => {
228
- 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);
229
263
  },
230
264
  getObject: async (request: ObjectRequest): Promise<any> => {
231
265
  const token = await this.pluginController.getToken();
232
- return generateObjectFunction(this.supabaseUrl, request, token);
266
+ return generateObject(this.pluginController.getBackendUrl(), request, token);
233
267
  },
234
268
  // getSteamedObject: this.generateObjectStream,
235
269
  }
236
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
+
237
284
  public community = {
238
285
  /**
239
286
  * Shared content is a way to share completable content with other users using this plugin.
@@ -265,16 +312,20 @@ export class RimoriClient {
265
312
  * @param contentType The type of shared content to fetch. E.g. assignments, exercises, etc.
266
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.
267
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.
268
- * @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.
269
320
  * @returns The new shared content.
270
321
  */
271
322
  getNew: async <T = any>(
272
323
  contentType: string,
273
324
  generatorInstructions: SharedContentObjectRequest,
274
325
  filter?: SharedContentFilter,
275
- privateTopic?: boolean,
326
+ options?: { privateTopic?: boolean; skipDbSave?: boolean; alwaysGenerateNew?: boolean; excludeIds?: string[] },
276
327
  ): Promise<SharedContent<T>> => {
277
- return await this.sharedContentController.getNewSharedContent(contentType, generatorInstructions, filter, privateTopic);
328
+ return await this.sharedContentController.getNewSharedContent(contentType, generatorInstructions, filter, options);
278
329
  },
279
330
  /**
280
331
  * Create a new shared content item.
@@ -301,6 +352,20 @@ export class RimoriClient {
301
352
  complete: async (contentType: string, assignmentId: string) => {
302
353
  return await this.sharedContentController.completeSharedContent(contentType, assignmentId);
303
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
+ },
304
369
  /**
305
370
  * Remove a shared content item.
306
371
  * @param id The id of the shared content item to remove.
@@ -4,7 +4,8 @@ import { DEFAULT_ANON_KEY, DEFAULT_ENDPOINT } from "../utils/endpoint";
4
4
 
5
5
  export interface StandaloneConfig {
6
6
  url: string,
7
- key: string
7
+ key: string,
8
+ backendUrl?: string
8
9
  }
9
10
 
10
11
  export class StandaloneClient {
@@ -25,6 +26,7 @@ export class StandaloneClient {
25
26
  StandaloneClient.instance = new StandaloneClient({
26
27
  url: config?.SUPABASE_URL || DEFAULT_ENDPOINT,
27
28
  key: config?.SUPABASE_ANON_KEY || DEFAULT_ANON_KEY,
29
+ backendUrl: config?.BACKEND_URL || 'https://api.rimori.se',
28
30
  });
29
31
  }
30
32
  return StandaloneClient.instance;
@@ -58,18 +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", {
62
- body: { pluginId },
63
- headers: { authorization: `Bearer ${session.data.session?.access_token}` },
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
+ })
64
74
  });
65
- if (error) {
66
- throw new Error("Failed to get plugin token. " + error.message);
75
+
76
+ if (!response.ok) {
77
+ const errorText = await response.text();
78
+ throw new Error(`Failed to get plugin token. ${response.status}: ${errorText}`);
67
79
  }
80
+
81
+ const data = await response.json();
82
+
68
83
  return {
69
84
  token: data.token,
70
85
  pluginId: pluginId,
71
86
  url: config.url,
72
87
  key: config.key,
88
+ backendUrl: config.backendUrl,
73
89
  tablePrefix: pluginId,
74
90
  expiration: new Date(Date.now() + 1000 * 60 * 60 * 1.5), // 1.5 hours
75
91
  }
@@ -1,17 +1,20 @@
1
- export function setTheme() {
1
+ export function setTheme(theme?: string | null) {
2
2
  document.documentElement.classList.add("dark:text-gray-200");
3
3
 
4
- if (isDarkTheme()) {
4
+ if (isDarkTheme(theme)) {
5
5
  document.documentElement.setAttribute("data-theme", "dark");
6
6
  document.documentElement.classList.add('dark', "dark:bg-gray-950");
7
7
  document.documentElement.style.background = "hsl(var(--background))";
8
8
  }
9
9
  }
10
10
 
11
- export function isDarkTheme(): boolean {
12
- const urlParams = new URLSearchParams(window.location.search);
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
+ }
13
17
 
14
- let theme = urlParams.get('theme');
15
18
  if (!theme || theme === 'system') {
16
19
  return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
17
20
  }