@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
@@ -8,7 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { generateText, streamChatGPT } from "../core/controller/AIController";
11
- import { generateObject as generateObjectFunction } from "../core/controller/ObjectController";
11
+ import { generateObject } from "../core/controller/ObjectController";
12
12
  import { SettingsController } from "../core/controller/SettingsController";
13
13
  import { SharedContentController } from "../core/controller/SharedContentController";
14
14
  import { getSTTResponse, getTTSResponse } from "../core/controller/VoiceController";
@@ -91,6 +91,11 @@ export class RimoriClient {
91
91
  */
92
92
  emitSidebarAction: (pluginId, actionKey, text) => {
93
93
  this.event.emit("global.sidebar.triggerAction", { plugin_id: pluginId, action_key: actionKey, text });
94
+ },
95
+ onMainPanelAction: (callback) => {
96
+ // this needs to be a emit and on because the main panel action is triggered by the user and not by the plugin
97
+ this.event.emit("action.requestMain");
98
+ this.event.on("action.requestMain", ({ data }) => callback(data));
94
99
  }
95
100
  };
96
101
  this.navigation = {
@@ -109,17 +114,24 @@ export class RimoriClient {
109
114
  }),
110
115
  getVoice: (text_1, ...args_1) => __awaiter(this, [text_1, ...args_1], void 0, function* (text, voice = "alloy", speed = 1, language) {
111
116
  const token = yield this.pluginController.getToken();
112
- return getTTSResponse(this.supabaseUrl, { input: text, voice, speed, language }, token);
117
+ return getTTSResponse(this.pluginController.getBackendUrl(), { input: text, voice, speed, language }, token);
118
+ }),
119
+ getTextFromVoice: (file) => __awaiter(this, void 0, void 0, function* () {
120
+ const token = yield this.pluginController.getToken();
121
+ return getSTTResponse(this.pluginController.getBackendUrl(), file, token);
113
122
  }),
114
- getTextFromVoice: (file) => {
115
- return getSTTResponse(this.superbase, file);
116
- },
117
123
  getObject: (request) => __awaiter(this, void 0, void 0, function* () {
118
124
  const token = yield this.pluginController.getToken();
119
- return generateObjectFunction(this.supabaseUrl, request, token);
125
+ return generateObject(this.pluginController.getBackendUrl(), request, token);
120
126
  }),
121
127
  // getSteamedObject: this.generateObjectStream,
122
128
  };
129
+ this.runtime = {
130
+ fetchBackend: (url, options) => __awaiter(this, void 0, void 0, function* () {
131
+ const token = yield this.pluginController.getToken();
132
+ return fetch(this.pluginController.getBackendUrl() + url, Object.assign(Object.assign({}, options), { headers: Object.assign(Object.assign({}, options.headers), { 'Authorization': `Bearer ${token}` }) }));
133
+ })
134
+ };
123
135
  this.community = {
124
136
  /**
125
137
  * Shared content is a way to share completable content with other users using this plugin.
@@ -151,11 +163,15 @@ export class RimoriClient {
151
163
  * @param contentType The type of shared content to fetch. E.g. assignments, exercises, etc.
152
164
  * @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.
153
165
  * @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.
154
- * @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.
166
+ * @param options An optional object with options for the new shared content.
167
+ * @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.
168
+ * @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.
169
+ * @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.
170
+ * @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.
155
171
  * @returns The new shared content.
156
172
  */
157
- getNew: (contentType, generatorInstructions, filter, privateTopic) => __awaiter(this, void 0, void 0, function* () {
158
- return yield this.sharedContentController.getNewSharedContent(contentType, generatorInstructions, filter, privateTopic);
173
+ getNew: (contentType, generatorInstructions, filter, options) => __awaiter(this, void 0, void 0, function* () {
174
+ return yield this.sharedContentController.getNewSharedContent(contentType, generatorInstructions, filter, options);
159
175
  }),
160
176
  /**
161
177
  * Create a new shared content item.
@@ -182,6 +198,14 @@ export class RimoriClient {
182
198
  complete: (contentType, assignmentId) => __awaiter(this, void 0, void 0, function* () {
183
199
  return yield this.sharedContentController.completeSharedContent(contentType, assignmentId);
184
200
  }),
201
+ /**
202
+ /**
203
+ * Update the state of a shared content item for a specific user.
204
+ * Useful for marking content as completed, ongoing, hidden, liked, disliked, or bookmarked.
205
+ */
206
+ updateState: (params) => __awaiter(this, void 0, void 0, function* () {
207
+ return yield this.sharedContentController.updateSharedContentState(params);
208
+ }),
185
209
  /**
186
210
  * Remove a shared content item.
187
211
  * @param id The id of the shared content item to remove.
@@ -192,18 +216,16 @@ export class RimoriClient {
192
216
  })
193
217
  }
194
218
  };
219
+ this.rimoriInfo = info;
195
220
  this.superbase = supabase;
196
221
  this.pluginController = pluginController;
197
222
  this.settingsController = new SettingsController(supabase, info.pluginId);
198
223
  this.sharedContentController = new SharedContentController(this.superbase, this);
199
- this.supabaseUrl = this.pluginController.getSupabaseUrl();
200
224
  this.accomplishmentHandler = new AccomplishmentHandler(info.pluginId);
201
- this.installedPlugins = info.installedPlugins;
202
- this.profile = info.profile;
203
225
  this.from = this.from.bind(this);
204
226
  this.db = {
205
227
  from: this.from,
206
- storage: this.superbase.storage,
228
+ // storage: this.superbase.storage,
207
229
  // functions: this.superbase.functions,
208
230
  tablePrefix: info.tablePrefix,
209
231
  getTableName: this.getTableName.bind(this),
@@ -216,14 +238,26 @@ export class RimoriClient {
216
238
  getSettings: (defaultSettings) => __awaiter(this, void 0, void 0, function* () {
217
239
  return yield this.settingsController.getSettings(defaultSettings);
218
240
  }),
219
- getInstalled: () => __awaiter(this, void 0, void 0, function* () {
220
- return this.installedPlugins;
221
- }),
222
- getUserInfo: () => __awaiter(this, void 0, void 0, function* () {
223
- return this.profile;
224
- })
241
+ getUserInfo: () => {
242
+ return this.rimoriInfo.profile;
243
+ },
244
+ getPluginInfo: () => {
245
+ return {
246
+ installedPlugins: this.rimoriInfo.installedPlugins,
247
+ mainPanelPlugin: this.rimoriInfo.mainPanelPlugin,
248
+ sidePanelPlugin: this.rimoriInfo.sidePanelPlugin,
249
+ };
250
+ }
225
251
  };
226
252
  }
253
+ /**
254
+ * Get a query parameter value that was passed via MessageChannel
255
+ * @param key The query parameter key
256
+ * @returns The query parameter value or null if not found
257
+ */
258
+ getQueryParam(key) {
259
+ return this.pluginController.getQueryParam(key);
260
+ }
227
261
  static getInstance(pluginController) {
228
262
  return __awaiter(this, void 0, void 0, function* () {
229
263
  if (!RimoriClient.instance) {
@@ -236,10 +270,13 @@ export class RimoriClient {
236
270
  from(relation) {
237
271
  return this.superbase.from(this.getTableName(relation));
238
272
  }
239
- getTableName(type) {
240
- if (type.startsWith("global_")) {
241
- return type.replace("global_", "");
273
+ getTableName(table) {
274
+ if (/[A-Z]/.test(table)) {
275
+ throw new Error("Table name cannot include uppercase letters. Please use snake_case for table names.");
276
+ }
277
+ if (table.startsWith("global_")) {
278
+ return table.replace("global_", "");
242
279
  }
243
- return this.db.tablePrefix + "_" + type;
280
+ return this.db.tablePrefix + "_" + table;
244
281
  }
245
282
  }
@@ -2,6 +2,7 @@ import { SupabaseClient } from "@supabase/supabase-js";
2
2
  export interface StandaloneConfig {
3
3
  url: string;
4
4
  key: string;
5
+ backendUrl?: string;
5
6
  }
6
7
  export declare class StandaloneClient {
7
8
  private static instance;
@@ -24,6 +24,7 @@ export class StandaloneClient {
24
24
  StandaloneClient.instance = new StandaloneClient({
25
25
  url: (config === null || config === void 0 ? void 0 : config.SUPABASE_URL) || DEFAULT_ENDPOINT,
26
26
  key: (config === null || config === void 0 ? void 0 : config.SUPABASE_ANON_KEY) || DEFAULT_ANON_KEY,
27
+ backendUrl: (config === null || config === void 0 ? void 0 : config.BACKEND_URL) || 'https://api.rimori.se',
27
28
  });
28
29
  }
29
30
  return StandaloneClient.instance;
@@ -61,18 +62,28 @@ export class StandaloneClient {
61
62
  var _a;
62
63
  const session = yield supabase.auth.getSession();
63
64
  console.log("session", session);
64
- const { data, error } = yield supabase.functions.invoke("plugin-token", {
65
- body: { pluginId },
66
- headers: { authorization: `Bearer ${(_a = session.data.session) === null || _a === void 0 ? void 0 : _a.access_token}` },
65
+ // Call the NestJS backend endpoint instead of the Supabase edge function
66
+ const response = yield fetch(`${config.backendUrl}/plugin/token`, {
67
+ method: 'POST',
68
+ headers: {
69
+ 'Content-Type': 'application/json',
70
+ 'Authorization': `Bearer ${(_a = session.data.session) === null || _a === void 0 ? void 0 : _a.access_token}`
71
+ },
72
+ body: JSON.stringify({
73
+ pluginId: pluginId
74
+ })
67
75
  });
68
- if (error) {
69
- throw new Error("Failed to get plugin token. " + error.message);
76
+ if (!response.ok) {
77
+ const errorText = yield response.text();
78
+ throw new Error(`Failed to get plugin token. ${response.status}: ${errorText}`);
70
79
  }
80
+ const data = yield response.json();
71
81
  return {
72
82
  token: data.token,
73
83
  pluginId: pluginId,
74
84
  url: config.url,
75
85
  key: config.key,
86
+ backendUrl: config.backendUrl,
76
87
  tablePrefix: pluginId,
77
88
  expiration: new Date(Date.now() + 1000 * 60 * 60 * 1.5), // 1.5 hours
78
89
  };
@@ -1,2 +1,2 @@
1
- export declare function setTheme(): void;
2
- export declare function isDarkTheme(): boolean;
1
+ export declare function setTheme(theme?: string | null): void;
2
+ export declare function isDarkTheme(theme?: string | null): boolean;
@@ -1,14 +1,17 @@
1
- export function setTheme() {
1
+ export function setTheme(theme) {
2
2
  document.documentElement.classList.add("dark:text-gray-200");
3
- if (isDarkTheme()) {
3
+ if (isDarkTheme(theme)) {
4
4
  document.documentElement.setAttribute("data-theme", "dark");
5
5
  document.documentElement.classList.add('dark', "dark:bg-gray-950");
6
6
  document.documentElement.style.background = "hsl(var(--background))";
7
7
  }
8
8
  }
9
- export function isDarkTheme() {
10
- const urlParams = new URLSearchParams(window.location.search);
11
- let theme = urlParams.get('theme');
9
+ export function isDarkTheme(theme) {
10
+ // If no theme provided, try to get from URL as fallback (for standalone mode)
11
+ if (!theme) {
12
+ const urlParams = new URLSearchParams(window.location.search);
13
+ theme = urlParams.get('theme');
14
+ }
12
15
  if (!theme || theme === 'system') {
13
16
  return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
14
17
  }
@@ -8,5 +8,5 @@ interface PluginProviderProps {
8
8
  };
9
9
  }
10
10
  export declare const PluginProvider: React.FC<PluginProviderProps>;
11
- export declare const usePlugin: () => RimoriClient;
11
+ export declare const useRimori: () => RimoriClient;
12
12
  export {};
@@ -17,27 +17,40 @@ const PluginContext = createContext(null);
17
17
  export const PluginProvider = ({ children, pluginId, settings }) => {
18
18
  const [plugin, setPlugin] = useState(null);
19
19
  const [standaloneClient, setStandaloneClient] = useState(false);
20
+ const [applicationMode, setApplicationMode] = useState(null);
21
+ const [theme, setTheme] = useState(null);
22
+ const isSidebar = applicationMode === "sidebar";
23
+ const isSettings = applicationMode === "settings";
20
24
  useEffect(() => {
21
25
  initEventBus(pluginId);
22
- const standaloneDetected = new URLSearchParams(window.location.search).get("secret") === null;
26
+ // Check if we're in an iframe context - if not, we're standalone
27
+ const standaloneDetected = window === window.parent;
23
28
  if (standaloneDetected && !standaloneClient) {
24
29
  StandaloneClient.getInstance().then(client => {
25
30
  client.needsLogin().then((needLogin) => setStandaloneClient(needLogin ? client : true));
26
31
  });
27
32
  }
28
33
  if ((!standaloneDetected && !plugin) || (standaloneDetected && standaloneClient === true)) {
29
- PluginController.getInstance(pluginId, standaloneDetected).then(setPlugin);
34
+ PluginController.getInstance(pluginId, standaloneDetected).then(client => {
35
+ setPlugin(client);
36
+ // Get applicationMode and theme from MessageChannel query params
37
+ if (!standaloneDetected) {
38
+ const mode = client.getQueryParam("applicationMode");
39
+ const themeParam = client.getQueryParam("rm_theme");
40
+ setApplicationMode(mode);
41
+ setTheme(themeParam);
42
+ }
43
+ });
30
44
  }
31
45
  }, [pluginId, standaloneClient]);
32
46
  //route change
33
47
  useEffect(() => {
34
48
  if (!plugin)
35
49
  return;
36
- const url = new URL(window.location.href);
37
50
  //sidebar pages should not report url changes
38
- if (url.searchParams.get("applicationMode") === "sidebar")
51
+ if (isSidebar)
39
52
  return;
40
- let lastHash = url.hash;
53
+ let lastHash = window.location.hash;
41
54
  const emitUrlChange = (url) => plugin.event.emit('session.triggerUrlChange', { url });
42
55
  const interval = setInterval(() => {
43
56
  if (lastHash === window.location.hash)
@@ -66,18 +79,31 @@ export const PluginProvider = ({ children, pluginId, settings }) => {
66
79
  if (!plugin) {
67
80
  return "";
68
81
  }
69
- return (_jsxs(PluginContext.Provider, { value: plugin, children: [!(settings === null || settings === void 0 ? void 0 : settings.disableContextMenu) && _jsx(ContextMenu, { client: plugin }), children] }));
82
+ return (_jsxs(PluginContext.Provider, { value: plugin, children: [!(settings === null || settings === void 0 ? void 0 : settings.disableContextMenu) && !isSidebar && !isSettings && _jsx(ContextMenu, { client: plugin }), children] }));
70
83
  };
71
- export const usePlugin = () => {
84
+ export const useRimori = () => {
72
85
  const context = useContext(PluginContext);
73
86
  if (context === null) {
74
- throw new Error('usePlugin must be used within an PluginProvider');
87
+ throw new Error('useRimori must be used within an PluginProvider');
75
88
  }
76
89
  return context;
77
90
  };
78
- function initEventBus(pluginId) {
91
+ function getUrlParam(name) {
92
+ // First try to get from URL hash query params (for compatibility)
93
+ const hashParts = window.location.hash.split('?');
94
+ if (hashParts.length > 1) {
95
+ const hashParams = new URLSearchParams(hashParts[1]);
96
+ const hashValue = hashParams.get(name);
97
+ if (hashValue)
98
+ return hashValue;
99
+ }
100
+ // Fallback to regular URL search params
79
101
  const url = new URL(window.location.href);
80
- const isSidebar = url.searchParams.get("applicationMode") === "sidebar";
102
+ return url.searchParams.get(name);
103
+ }
104
+ function initEventBus(pluginId) {
105
+ // For now, use URL fallback for EventBus naming - this will be updated once MessageChannel is ready
106
+ const isSidebar = getUrlParam("applicationMode") === "sidebar";
81
107
  EventBusHandler.getInstance("Plugin EventBus " + pluginId + " " + (isSidebar ? "sidebar" : "main"));
82
108
  }
83
109
  function StandaloneAuth({ onLogin }) {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Maps audio MIME types to their corresponding file extensions
3
+ * OpenAI supports: mp3, mp4, mpeg, mpga, m4a, wav, webm
4
+ */
5
+ export declare const AUDIO_MIME_TO_EXTENSION: Record<string, string>;
6
+ /**
7
+ * OpenAI supported audio formats for STT API
8
+ */
9
+ export declare const OPENAI_SUPPORTED_FORMATS: string[];
10
+ /**
11
+ * Determines the appropriate file extension for an audio MIME type
12
+ * @param mimeType - The MIME type of the audio
13
+ * @returns The file extension (without dot) or null if unsupported
14
+ */
15
+ export declare function getAudioFormatFromMimeType(mimeType: string): string | null;
16
+ /**
17
+ * Checks if an audio MIME type is supported by OpenAI
18
+ * @param mimeType - The MIME type to check
19
+ * @returns True if supported, false otherwise
20
+ */
21
+ export declare function isAudioFormatSupported(mimeType: string): boolean;
22
+ /**
23
+ * Gets a human-readable list of supported audio formats
24
+ * @returns Formatted string of supported formats
25
+ */
26
+ export declare function getSupportedFormatsList(): string;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Maps audio MIME types to their corresponding file extensions
3
+ * OpenAI supports: mp3, mp4, mpeg, mpga, m4a, wav, webm
4
+ */
5
+ export const AUDIO_MIME_TO_EXTENSION = {
6
+ 'audio/mpeg': 'mp3',
7
+ 'audio/mp3': 'mp3',
8
+ 'audio/mp4': 'mp4',
9
+ 'audio/m4a': 'm4a',
10
+ 'audio/wav': 'wav',
11
+ 'audio/wave': 'wav',
12
+ 'audio/x-wav': 'wav',
13
+ 'audio/webm': 'webm',
14
+ 'audio/ogg': 'webm', // Convert ogg to webm for OpenAI compatibility
15
+ 'audio/aac': 'm4a', // Convert aac to m4a for OpenAI compatibility
16
+ 'audio/x-aac': 'm4a',
17
+ 'audio/3gpp': 'mp4', // Convert 3gpp to mp4
18
+ 'audio/3gpp2': 'mp4',
19
+ 'audio/amr': 'mp4', // Convert amr to mp4
20
+ 'audio/amr-wb': 'mp4',
21
+ 'audio/flac': 'wav', // Convert flac to wav
22
+ 'audio/x-flac': 'wav',
23
+ 'audio/opus': 'webm', // Convert opus to webm
24
+ 'audio/x-opus': 'webm',
25
+ };
26
+ /**
27
+ * OpenAI supported audio formats for STT API
28
+ */
29
+ export const OPENAI_SUPPORTED_FORMATS = ['mp3', 'mp4', 'mpeg', 'mpga', 'm4a', 'wav', 'webm'];
30
+ /**
31
+ * Determines the appropriate file extension for an audio MIME type
32
+ * @param mimeType - The MIME type of the audio
33
+ * @returns The file extension (without dot) or null if unsupported
34
+ */
35
+ export function getAudioFormatFromMimeType(mimeType) {
36
+ const normalizedMimeType = mimeType.toLowerCase();
37
+ // Try to get format from MIME type mapping first
38
+ let format = AUDIO_MIME_TO_EXTENSION[normalizedMimeType];
39
+ // If no mapping found, try to extract from MIME type
40
+ if (!format && normalizedMimeType.includes('audio/')) {
41
+ format = normalizedMimeType.replace('audio/', '');
42
+ }
43
+ // Handle special cases
44
+ if (format === 'mpeg') {
45
+ format = 'mp3'; // OpenAI expects mp3, not mpeg
46
+ }
47
+ // Validate the format
48
+ if (!format || !OPENAI_SUPPORTED_FORMATS.includes(format)) {
49
+ return null;
50
+ }
51
+ return format;
52
+ }
53
+ /**
54
+ * Checks if an audio MIME type is supported by OpenAI
55
+ * @param mimeType - The MIME type to check
56
+ * @returns True if supported, false otherwise
57
+ */
58
+ export function isAudioFormatSupported(mimeType) {
59
+ return getAudioFormatFromMimeType(mimeType) !== null;
60
+ }
61
+ /**
62
+ * Gets a human-readable list of supported audio formats
63
+ * @returns Formatted string of supported formats
64
+ */
65
+ export function getSupportedFormatsList() {
66
+ return OPENAI_SUPPORTED_FORMATS.join(', ');
67
+ }
@@ -1,6 +1,7 @@
1
1
  import { RimoriClient } from "../plugin/RimoriClient";
2
2
  /**
3
3
  * Sets up the web worker for the plugin to be able receive and send messages to Rimori.
4
- * @param init - The function containing the subscription logic.
4
+ * @param pluginId - The id of the plugin to setup the worker for.
5
+ * @param init - The function containing the initialization logic.
5
6
  */
6
- export declare function setupWorker(init: (controller: RimoriClient) => void | Promise<void>): void;
7
+ export declare function setupWorker(pluginId: string, init: (client: RimoriClient) => void | Promise<void>): Promise<void>;
@@ -7,76 +7,31 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { EventBus, EventBusHandler } from "../fromRimori/EventBus";
10
+ import { EventBusHandler } from "../fromRimori/EventBus";
11
11
  import { PluginController } from "../plugin/PluginController";
12
- let controller = null;
13
- const listeners = [];
14
- let debugEnabled = false;
15
12
  /**
16
13
  * Sets up the web worker for the plugin to be able receive and send messages to Rimori.
17
- * @param init - The function containing the subscription logic.
14
+ * @param pluginId - The id of the plugin to setup the worker for.
15
+ * @param init - The function containing the initialization logic.
18
16
  */
19
- export function setupWorker(init) {
20
- // Mock of the window object for the worker context to be able to use the PluginController.
21
- const mockWindow = {
22
- isWorker: true,
23
- location: { search: '?secret=123' },
24
- parent: {
25
- postMessage: (message) => {
26
- message.event.sender = "worker." + message.event.sender;
27
- checkDebugMode(message.event);
28
- logIfDebug('sending event to Rimori', message.event);
29
- self.postMessage(message);
30
- }
31
- },
32
- addEventListener: (_, listener) => {
33
- listeners.push(listener);
34
- },
35
- APP_CONFIG: {
36
- SUPABASE_URL: 'NOT_SET',
37
- SUPABASE_ANON_KEY: 'NOT_SET',
38
- BACKEND_URL: 'NOT_SET',
39
- },
40
- };
41
- // Assign the mock to globalThis.
42
- Object.assign(globalThis, { window: mockWindow });
43
- EventBusHandler.getInstance("Worker EventBus");
44
- // Handle init message from Rimori.
45
- self.onmessage = (response) => __awaiter(this, void 0, void 0, function* () {
46
- checkDebugMode(response.data);
47
- logIfDebug('Message received', response.data);
48
- const event = response.data;
49
- if (event.topic === 'global.worker.requestInit') {
50
- if (!controller) {
51
- mockWindow.APP_CONFIG.SUPABASE_URL = event.data.supabaseUrl;
52
- mockWindow.APP_CONFIG.SUPABASE_ANON_KEY = event.data.supabaseAnonKey;
53
- mockWindow.APP_CONFIG.BACKEND_URL = event.data.backendUrl;
54
- controller = yield PluginController.getInstance(event.data.pluginId);
55
- logIfDebug('Worker initialized.');
56
- yield init(controller);
57
- logIfDebug('Plugin listeners initialized.');
58
- }
59
- const initEvent = {
60
- timestamp: new Date().toISOString(),
61
- eventId: event.eventId,
62
- sender: "worker." + event.sender,
63
- topic: 'global.worker.requestInit',
64
- data: { success: true },
65
- debug: debugEnabled
66
- };
67
- return self.postMessage({ secret: "123", event: initEvent });
68
- }
69
- listeners.forEach(listener => listener({ data: { event: response.data, secret: "123" } }));
17
+ export function setupWorker(pluginId, init) {
18
+ return __awaiter(this, void 0, void 0, function* () {
19
+ // Mock of the window object for the worker context to be able to use the PluginController.
20
+ const mockWindow = {
21
+ isWorker: true,
22
+ location: {},
23
+ parent: {
24
+ postMessage: () => { }
25
+ },
26
+ addEventListener: () => { }
27
+ };
28
+ // Assign the mock to globalThis.
29
+ Object.assign(globalThis, { window: mockWindow });
30
+ EventBusHandler.getInstance("Worker EventBus");
31
+ const rimoriClient = yield PluginController.getInstance(pluginId);
32
+ console.debug('[Worker] RimoriClient initialized.');
33
+ yield init(rimoriClient);
34
+ console.debug('[Worker] Worker initialized.');
35
+ self.postMessage({ type: "rimori:acknowledged", pluginId: pluginId });
70
36
  });
71
37
  }
72
- function checkDebugMode(event) {
73
- if (event.topic === 'global.system.requestDebug' || event.debug) {
74
- debugEnabled = true;
75
- EventBus.emit("worker", "global.system.requestDebug");
76
- }
77
- }
78
- function logIfDebug(...args) {
79
- if (debugEnabled) {
80
- console.debug('[Worker] ' + args[0], ...args.slice(1));
81
- }
82
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimori/client",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "bin": {
@@ -35,6 +35,7 @@
35
35
  "@supabase/supabase-js": "^2.48.1",
36
36
  "@tiptap/react": "2.10.3",
37
37
  "@tiptap/starter-kit": "2.10.3",
38
+ "html2canvas": "^1.4.1",
38
39
  "react-icons": "^5.4.0",
39
40
  "react-markdown": "^10.1.0",
40
41
  "tiptap-markdown": "^0.8.10"
@@ -157,7 +157,6 @@ export async function registerDeveloper(jwtToken: string, port: number): Promise
157
157
  console.log('🚀 Registering developer and creating plugin...');
158
158
 
159
159
  try {
160
- console.log('port', port, typeof port);
161
160
  const currentFolderName = path.basename(process.cwd());
162
161
  const body: any = { port, pluginName: currentFolderName };
163
162
  const backendUrl = process.env.RIMORI_BACKEND_URL || "https://api.rimori.se";
@@ -15,6 +15,7 @@ import { updatePackageJson, type PackageJson } from './package-setup.js';
15
15
  import { transformAppRouter } from './router-transformer.js';
16
16
  import { updateTailwindConfig } from './tailwind-config.js';
17
17
  import { updateViteConfigBase } from './vite-config.js';
18
+ import 'dotenv/config';
18
19
 
19
20
  /**
20
21
  * Main function that handles the complete plugin setup flow.
@@ -4,7 +4,7 @@ import { AudioInputField } from './EmbeddedAssistent/AudioInputField';
4
4
  import { MessageSender } from './EmbeddedAssistent/TTS/MessageSender';
5
5
  import Markdown from 'react-markdown';
6
6
  import { useChat } from '../../hooks/UseChatHook';
7
- import { usePlugin } from '../../components';
7
+ import { useRimori } from '../../components';
8
8
  import { FirstMessages, getFirstMessages } from './utils';
9
9
 
10
10
  interface Props {
@@ -16,7 +16,7 @@ interface Props {
16
16
 
17
17
  export function AssistantChat({ avatarImageUrl, voiceId, onComplete, autoStartConversation }: Props) {
18
18
  const [oralCommunication, setOralCommunication] = React.useState(true);
19
- const { ai: llm, event } = usePlugin();
19
+ const { ai: llm, event } = useRimori();
20
20
  const sender = useMemo(() => new MessageSender(llm.getVoice, voiceId), []);
21
21
  const { messages, append, isLoading, setMessages } = useChat();
22
22
 
@@ -4,7 +4,7 @@ import { MessageSender } from './EmbeddedAssistent/TTS/MessageSender';
4
4
  import { CircleAudioAvatar } from './EmbeddedAssistent/CircleAudioAvatar';
5
5
  import { Tool } from '../../fromRimori/PluginTypes';
6
6
  import { useChat } from '../../hooks/UseChatHook';
7
- import { usePlugin } from '../../components';
7
+ import { useRimori } from '../../components';
8
8
  import { getFirstMessages } from './utils';
9
9
  import { FirstMessages } from './utils';
10
10
 
@@ -29,7 +29,7 @@ export function Avatar({
29
29
  circleSize = "300px",
30
30
  className
31
31
  }: Props) {
32
- const { ai, event } = usePlugin();
32
+ const { ai, event } = useRimori();
33
33
  const [agentReplying, setAgentReplying] = useState(false);
34
34
  const [isProcessingMessage, setIsProcessingMessage] = useState(false);
35
35
  const sender = useMemo(() => new MessageSender(ai.getVoice, voiceId), [voiceId]);