@rimori/client 2.5.28 → 2.5.29-next.1

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.
package/README.md CHANGED
@@ -140,7 +140,7 @@ Helpers:
140
140
  The `client.ai` controller surfaces AI capabilities:
141
141
 
142
142
  - `getText(messages, tools?)` – chat completion (string result).
143
- - `getSteamedText(messages, onMessage, tools?)` – streamed responses.
143
+ - `getStreamedText(messages, onMessage, tools?)` – streamed responses.
144
144
  - `getObject(request)` – structured JSON generation.
145
145
  - `getVoice(text, voice?, speed?, language?)` – text-to-speech (returns `Blob`).
146
146
  - `getTextFromVoice(file)` – speech-to-text transcription.
@@ -1,4 +1,4 @@
1
- import { EventBusMessage } from '../fromRimori/EventBus';
1
+ import { EventBusHandler, EventBusMessage } from '../fromRimori/EventBus';
2
2
  export type AccomplishmentMessage = EventBusMessage<MicroAccomplishmentPayload>;
3
3
  export declare const skillCategories: readonly ["reading", "listening", "speaking", "writing", "learning", "community"];
4
4
  interface BaseAccomplishmentPayload {
@@ -23,7 +23,8 @@ export interface MacroAccomplishmentPayload extends BaseAccomplishmentPayload {
23
23
  export type AccomplishmentPayload = MicroAccomplishmentPayload | MacroAccomplishmentPayload;
24
24
  export declare class AccomplishmentController {
25
25
  private pluginId;
26
- constructor(pluginId: string);
26
+ private eventBus;
27
+ constructor(pluginId: string, eventBus?: EventBusHandler);
27
28
  emitAccomplishment(payload: Omit<AccomplishmentPayload, 'type'>): void;
28
29
  private validateAccomplishment;
29
30
  private sanitizeAccomplishment;
@@ -1,8 +1,9 @@
1
1
  import { EventBus } from '../fromRimori/EventBus';
2
2
  export const skillCategories = ['reading', 'listening', 'speaking', 'writing', 'learning', 'community'];
3
3
  export class AccomplishmentController {
4
- constructor(pluginId) {
4
+ constructor(pluginId, eventBus) {
5
5
  this.pluginId = pluginId;
6
+ this.eventBus = eventBus !== null && eventBus !== void 0 ? eventBus : EventBus;
6
7
  }
7
8
  emitAccomplishment(payload) {
8
9
  const accomplishmentPayload = Object.assign(Object.assign({}, payload), { type: 'durationMinutes' in payload ? 'macro' : 'micro' });
@@ -11,7 +12,7 @@ export class AccomplishmentController {
11
12
  }
12
13
  const sanitizedPayload = this.sanitizeAccomplishment(accomplishmentPayload);
13
14
  const topic = 'global.accomplishment.trigger' + (accomplishmentPayload.type === 'macro' ? 'Macro' : 'Micro');
14
- EventBus.emit(this.pluginId, topic, sanitizedPayload);
15
+ this.eventBus.emit(this.pluginId, topic, sanitizedPayload);
15
16
  }
16
17
  validateAccomplishment(payload) {
17
18
  if (!skillCategories.includes(payload.skillCategory)) {
@@ -85,7 +86,7 @@ export class AccomplishmentController {
85
86
  else if (topicLength !== 3) {
86
87
  throw new Error('Invalid accomplishment topic pattern. The pattern must be plugin.skillCategory.accomplishmentKeyword or an * as wildcard for any plugin, skill category or accomplishment keyword');
87
88
  }
88
- EventBus.on(['global.accomplishment.triggerMicro', 'global.accomplishment.triggerMacro'], (event) => {
89
+ this.eventBus.on(['global.accomplishment.triggerMicro', 'global.accomplishment.triggerMacro'], (event) => {
89
90
  const { plugin, skillCategory, accomplishmentKeyword } = this.getDecoupledTopic(accomplishmentTopic);
90
91
  if (plugin !== '*' && event.sender !== plugin)
91
92
  return;
@@ -48,7 +48,7 @@ export class Translator {
48
48
  },
49
49
  },
50
50
  debug: false,
51
- // showSupportNotice: false, // TODO enable with next version of i18next
51
+ showSupportNotice: false,
52
52
  parseMissingKeyHandler: (key, defaultValue) => {
53
53
  if (!key.trim())
54
54
  return '';
@@ -79,12 +79,13 @@ export class Translator {
79
79
  });
80
80
  }
81
81
  getTranslationUrl(language) {
82
+ const baseUrl = this.translationUrl || window.location.origin;
82
83
  // For localhost development, use local- prefix for non-English languages
83
- if (window.location.hostname === 'localhost') {
84
+ if (window.location.hostname === 'localhost' || new URL(baseUrl).hostname === 'localhost') {
84
85
  const filename = language !== 'en' ? `local-${language}` : language;
85
- return `${window.location.origin}/locales/${filename}.json`;
86
+ return `${baseUrl}/locales/${filename}.json`;
86
87
  }
87
- return `${this.translationUrl}/locales/${language}.json`;
88
+ return `${baseUrl}/locales/${language}.json`;
88
89
  }
89
90
  usePlugin(plugin) {
90
91
  if (!this.i18n) {
@@ -134,6 +135,7 @@ export class Translator {
134
135
  if (!this.i18n) {
135
136
  throw new Error('Translator is not initialized');
136
137
  }
138
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
137
139
  return this.i18n.t(key, options);
138
140
  }
139
141
  /**
@@ -29,6 +29,11 @@ export declare class EventBusHandler {
29
29
  private generatedIds;
30
30
  private cleanupInterval;
31
31
  private constructor();
32
+ /**
33
+ * Creates a new non-singleton EventBusHandler instance.
34
+ * Used in federation mode where each plugin needs its own isolated EventBus.
35
+ */
36
+ static create(name?: string): EventBusHandler;
32
37
  static getInstance(name?: string): EventBusHandler;
33
38
  /**
34
39
  * Starts the interval to cleanup the generated ids.
@@ -109,6 +114,10 @@ export declare class EventBusHandler {
109
114
  */
110
115
  private validateTopic;
111
116
  private logIfDebug;
117
+ /**
118
+ * Destroys this EventBus instance, cleaning up all listeners and intervals.
119
+ */
120
+ destroy(): void;
112
121
  private logAndThrowError;
113
122
  }
114
123
  export declare const EventBus: EventBusHandler;
@@ -15,9 +15,18 @@ export class EventBusHandler {
15
15
  this.evName = '';
16
16
  this.generatedIds = new Map(); // Map<id, timestamp>
17
17
  this.cleanupInterval = null;
18
- //private constructor
19
18
  this.startIdCleanup();
20
19
  }
20
+ /**
21
+ * Creates a new non-singleton EventBusHandler instance.
22
+ * Used in federation mode where each plugin needs its own isolated EventBus.
23
+ */
24
+ static create(name) {
25
+ const instance = new EventBusHandler();
26
+ if (name)
27
+ instance.evName = name;
28
+ return instance;
29
+ }
21
30
  static getInstance(name) {
22
31
  if (!EventBusHandler.instance) {
23
32
  EventBusHandler.instance = new EventBusHandler();
@@ -307,6 +316,18 @@ export class EventBusHandler {
307
316
  console.debug(`[${this.evName}] ` + args[0], ...args.slice(1));
308
317
  }
309
318
  }
319
+ /**
320
+ * Destroys this EventBus instance, cleaning up all listeners and intervals.
321
+ */
322
+ destroy() {
323
+ this.listeners.clear();
324
+ this.responseResolvers.clear();
325
+ if (this.cleanupInterval) {
326
+ clearInterval(this.cleanupInterval);
327
+ this.cleanupInterval = null;
328
+ }
329
+ this.generatedIds.clear();
330
+ }
310
331
  logAndThrowError(throwError, ...args) {
311
332
  const message = `[${this.evName}] ` + args[0];
312
333
  console.error(message, ...args.slice(1));
@@ -86,6 +86,8 @@ export interface RimoriPluginConfig<T extends object = object> {
86
86
  sidebar: (SidebarPage & T)[];
87
87
  /** Optional path to the plugin's settings/configuration page */
88
88
  settings?: string;
89
+ /** When true, rimori-main loads this plugin via Module Federation instead of an iframe. */
90
+ federated?: boolean;
89
91
  /** Optional array of event topics the plugin pages can listen to for cross-plugin communication */
90
92
  topics?: string[];
91
93
  };
@@ -40,6 +40,10 @@ export interface RimoriInfo {
40
40
  * - 'plugins' for beta and stable release channels
41
41
  */
42
42
  dbSchema: 'plugins' | 'plugins_alpha';
43
+ /**
44
+ * Whether text-to-speech is enabled globally (set in rimori-main navbar).
45
+ */
46
+ ttsEnabled: boolean;
43
47
  }
44
48
  export declare class RimoriCommunicationHandler {
45
49
  private port;
@@ -63,8 +67,9 @@ export declare class RimoriCommunicationHandler {
63
67
  /**
64
68
  * Handles updates to RimoriInfo from rimori-main.
65
69
  * Updates the cached info and Supabase client, then notifies all registered callbacks.
70
+ * Public so that federated mode can call it when the update event arrives via the plugin's isolated EventBus.
66
71
  */
67
- private handleRimoriInfoUpdate;
72
+ handleRimoriInfoUpdate(newInfo: RimoriInfo): void;
68
73
  /**
69
74
  * Registers a callback to be called when RimoriInfo is updated.
70
75
  * @param callback - Function to call with the new RimoriInfo
@@ -77,7 +77,7 @@ export class RimoriCommunicationHandler {
77
77
  // Listen for updates from rimori-main (data changes, token refresh, etc.)
78
78
  // Topic format: {pluginId}.supabase.triggerUpdate
79
79
  EventBus.on(`${this.pluginId}.supabase.triggerUpdate`, (ev) => {
80
- // console.log('[RimoriCommunicationHandler] Received update from rimori-main', ev.data);
80
+ // console.log('[RimoriCommunicationHandler] Received triggerUpdate via MessageChannel for', this.pluginId);
81
81
  this.handleRimoriInfoUpdate(ev.data);
82
82
  });
83
83
  // Mark MessageChannel as ready and process pending requests
@@ -205,12 +205,14 @@ export class RimoriCommunicationHandler {
205
205
  /**
206
206
  * Handles updates to RimoriInfo from rimori-main.
207
207
  * Updates the cached info and Supabase client, then notifies all registered callbacks.
208
+ * Public so that federated mode can call it when the update event arrives via the plugin's isolated EventBus.
208
209
  */
209
210
  handleRimoriInfoUpdate(newInfo) {
210
211
  if (JSON.stringify(this.rimoriInfo) === JSON.stringify(newInfo)) {
211
- // console.log('[RimoriCommunicationHandler] RimoriInfo update is the same as the cached info, skipping update');
212
+ // console.log('[RimoriCommunicationHandler] RimoriInfo update identical to cached info, skipping', this.pluginId);
212
213
  return;
213
214
  }
215
+ // console.log('[RimoriCommunicationHandler] Applying RimoriInfo update for', this.pluginId, '| ttsEnabled:', newInfo.ttsEnabled);
214
216
  // Update cached rimoriInfo
215
217
  this.rimoriInfo = newInfo;
216
218
  // Update Supabase client with new token
@@ -1,10 +1,12 @@
1
1
  import { SharedContentController } from './module/SharedContentController';
2
+ import { RimoriInfo } from './CommunicationHandler';
2
3
  import { PluginModule } from './module/PluginModule';
3
4
  import { DbModule } from './module/DbModule';
4
5
  import { EventModule } from './module/EventModule';
5
6
  import { AIModule } from './module/AIModule';
6
7
  import { ExerciseModule } from './module/ExerciseModule';
7
8
  import { StorageModule } from './module/StorageModule';
9
+ import { EventBusHandler } from '../fromRimori/EventBus';
8
10
  export declare class RimoriClient {
9
11
  private static instance;
10
12
  sharedContent: SharedContentController;
@@ -16,7 +18,15 @@ export declare class RimoriClient {
16
18
  /** Upload and manage images stored in Supabase via the backend. */
17
19
  storage: StorageModule;
18
20
  private rimoriInfo;
21
+ /** The EventBus instance used by this client. In federation mode this is a per-plugin instance. */
22
+ eventBus: EventBusHandler;
19
23
  private constructor();
24
+ /**
25
+ * Creates a RimoriClient with pre-existing RimoriInfo (federation mode).
26
+ * Uses a fresh per-plugin EventBus instance instead of the global singleton.
27
+ * Creates the Supabase PostgrestClient internally from the info.
28
+ */
29
+ static createWithInfo(info: RimoriInfo): RimoriClient;
20
30
  static getInstance(pluginId?: string): Promise<RimoriClient>;
21
31
  navigation: {
22
32
  toDashboard: () => void;
@@ -16,9 +16,10 @@ import { EventModule } from './module/EventModule';
16
16
  import { AIModule } from './module/AIModule';
17
17
  import { ExerciseModule } from './module/ExerciseModule';
18
18
  import { StorageModule } from './module/StorageModule';
19
- import { EventBus } from '../fromRimori/EventBus';
19
+ import { PostgrestClient } from '@supabase/postgrest-js';
20
+ import { EventBus, EventBusHandler } from '../fromRimori/EventBus';
20
21
  export class RimoriClient {
21
- constructor(controller, supabase, info) {
22
+ constructor(controller, supabase, info, eventBus) {
22
23
  this.navigation = {
23
24
  toDashboard: () => {
24
25
  this.event.emit('global.navigation.triggerToDashboard');
@@ -30,12 +31,13 @@ export class RimoriClient {
30
31
  }),
31
32
  };
32
33
  this.rimoriInfo = info;
34
+ this.eventBus = eventBus !== null && eventBus !== void 0 ? eventBus : EventBus;
33
35
  this.sharedContent = new SharedContentController(supabase, this);
34
36
  this.ai = new AIModule(info.backendUrl, () => this.rimoriInfo.token, info.pluginId);
35
37
  this.ai.setOnRateLimited((exercisesRemaining) => {
36
- EventBus.emit(info.pluginId, 'global.quota.triggerExceeded', { exercises_remaining: exercisesRemaining });
38
+ this.eventBus.emit(info.pluginId, 'global.quota.triggerExceeded', { exercises_remaining: exercisesRemaining });
37
39
  });
38
- this.event = new EventModule(info.pluginId, info.backendUrl, () => this.rimoriInfo.token, this.ai);
40
+ this.event = new EventModule(info.pluginId, info.backendUrl, () => this.rimoriInfo.token, this.ai, this.eventBus);
39
41
  this.db = new DbModule(supabase, controller, info);
40
42
  this.plugin = new PluginModule(supabase, controller, info, this.ai);
41
43
  this.exercise = new ExerciseModule(supabase, controller, info, this.event);
@@ -48,6 +50,32 @@ export class RimoriClient {
48
50
  Logger.getInstance(this);
49
51
  }
50
52
  }
53
+ /**
54
+ * Creates a RimoriClient with pre-existing RimoriInfo (federation mode).
55
+ * Uses a fresh per-plugin EventBus instance instead of the global singleton.
56
+ * Creates the Supabase PostgrestClient internally from the info.
57
+ */
58
+ static createWithInfo(info) {
59
+ const eventBus = EventBusHandler.create('Plugin EventBus ' + info.pluginId);
60
+ const controller = new RimoriCommunicationHandler(info.pluginId, true);
61
+ const supabase = new PostgrestClient(`${info.url}/rest/v1`, {
62
+ schema: info.dbSchema,
63
+ headers: {
64
+ apikey: info.key,
65
+ Authorization: `Bearer ${info.token}`,
66
+ },
67
+ });
68
+ const client = new RimoriClient(controller, supabase, info, eventBus);
69
+ // In federated mode, CommunicationHandler skips MessageChannel setup so it never
70
+ // subscribes to triggerUpdate events. PluginEventBridge forwards host-bus events to
71
+ // this plugin's isolated eventBus, so we listen here and hand off to the controller.
72
+ eventBus.on(`${info.pluginId}.supabase.triggerUpdate`, (ev) => {
73
+ var _a;
74
+ console.log('[RimoriClient] Federated triggerUpdate received for', info.pluginId, '| ttsEnabled:', (_a = ev.data) === null || _a === void 0 ? void 0 : _a.ttsEnabled);
75
+ controller.handleRimoriInfoUpdate(ev.data);
76
+ });
77
+ return client;
78
+ }
51
79
  static getInstance(pluginId) {
52
80
  return __awaiter(this, void 0, void 0, function* () {
53
81
  if (!RimoriClient.instance) {
@@ -13,6 +13,8 @@ export declare class ChunkedAudioPlayer {
13
13
  private startedPlaying;
14
14
  private onEndOfSpeech;
15
15
  private readonly backgroundNoiseLevel;
16
+ private currentSource;
17
+ private stopped;
16
18
  constructor();
17
19
  private init;
18
20
  setOnLoudnessChange(callback: (value: number) => void): void;
@@ -20,6 +20,8 @@ export class ChunkedAudioPlayer {
20
20
  this.startedPlaying = false;
21
21
  this.onEndOfSpeech = () => { };
22
22
  this.backgroundNoiseLevel = 30; // Background noise level that should be treated as baseline (0)
23
+ this.currentSource = null;
24
+ this.stopped = false;
23
25
  this.init();
24
26
  }
25
27
  init() {
@@ -37,12 +39,10 @@ export class ChunkedAudioPlayer {
37
39
  }
38
40
  addChunk(chunk, position) {
39
41
  return __awaiter(this, void 0, void 0, function* () {
42
+ if (this.stopped)
43
+ return;
40
44
  console.log('Adding chunk', position, chunk);
41
45
  this.chunkQueue[position] = chunk;
42
- // console.log("received chunk", {
43
- // chunkQueue: this.chunkQueue.length,
44
- // isPlaying: this.isPlaying,
45
- // })
46
46
  if (position === 0 && !this.startedPlaying) {
47
47
  this.startedPlaying = true;
48
48
  this.playChunks();
@@ -50,25 +50,27 @@ export class ChunkedAudioPlayer {
50
50
  });
51
51
  }
52
52
  playChunks() {
53
- // console.log({ isPlaying: this.isPlaying });
54
- if (this.isPlaying)
53
+ if (this.isPlaying || this.stopped)
55
54
  return;
56
55
  if (!this.chunkQueue[this.currentIndex]) {
57
56
  // wait until the correct chunk arrives
58
57
  setTimeout(() => this.playChunks(), 10);
58
+ return;
59
59
  }
60
60
  this.isPlaying = true;
61
61
  this.playChunk(this.chunkQueue[this.currentIndex]).then(() => {
62
62
  this.isPlaying = false;
63
+ if (this.stopped)
64
+ return;
63
65
  this.currentIndex++;
64
66
  if (this.chunkQueue[this.currentIndex]) {
65
67
  this.shouldMonitorLoudness = true;
66
68
  this.playChunks();
67
69
  }
68
70
  else {
69
- // console.log('Playback finished', { currentIndex: this.currentIndex, chunkQueue: this.chunkQueue });
70
71
  setTimeout(() => {
71
- // console.log('Check again if really playback finished', { currentIndex: this.currentIndex, chunkQueue: this.chunkQueue });
72
+ if (this.stopped)
73
+ return;
72
74
  if (this.chunkQueue.length > this.currentIndex) {
73
75
  this.playChunks();
74
76
  }
@@ -81,13 +83,23 @@ export class ChunkedAudioPlayer {
81
83
  });
82
84
  }
83
85
  stopPlayback() {
84
- // console.log('Stopping playback');
85
- // Implement logic to stop the current playback
86
+ this.stopped = true;
87
+ // Stop the currently playing audio source node
88
+ if (this.currentSource) {
89
+ try {
90
+ this.currentSource.stop();
91
+ }
92
+ catch (_a) {
93
+ // already stopped
94
+ }
95
+ this.currentSource = null;
96
+ }
86
97
  this.isPlaying = false;
87
98
  this.chunkQueue = [];
88
99
  this.startedPlaying = false;
89
100
  this.shouldMonitorLoudness = false;
90
101
  cancelAnimationFrame(this.handle);
102
+ this.loudnessCallback(0);
91
103
  }
92
104
  cleanup() {
93
105
  // Stop playback first
@@ -100,14 +112,17 @@ export class ChunkedAudioPlayer {
100
112
  }
101
113
  }
102
114
  playChunk(chunk) {
103
- // console.log({queue: this.chunkQueue})
104
- if (!chunk) {
115
+ if (!chunk || this.stopped) {
105
116
  return Promise.resolve();
106
117
  }
107
- // console.log('Playing chunk', chunk);
108
118
  return new Promise((resolve) => {
109
119
  const source = this.audioContext.createBufferSource();
120
+ this.currentSource = source;
110
121
  this.audioContext.decodeAudioData(chunk.slice(0)).then((audioBuffer) => {
122
+ if (this.stopped) {
123
+ resolve();
124
+ return;
125
+ }
111
126
  source.buffer = audioBuffer;
112
127
  // Create a GainNode for volume control
113
128
  const gainNode = this.audioContext.createGain();
@@ -117,10 +132,11 @@ export class ChunkedAudioPlayer {
117
132
  gainNode.connect(this.analyser);
118
133
  this.analyser.connect(this.audioContext.destination);
119
134
  source.start(0);
120
- // console.log('Playing chunk', this.currentIndex);
121
135
  gainNode.gain.value = this.volume;
122
136
  source.onended = () => {
123
- // console.log('Chunk ended');
137
+ if (this.currentSource === source) {
138
+ this.currentSource = null;
139
+ }
124
140
  resolve();
125
141
  };
126
142
  // Start monitoring loudness only once
@@ -190,11 +206,10 @@ export class ChunkedAudioPlayer {
190
206
  this.handle = requestAnimationFrame(() => this.monitorLoudness());
191
207
  }
192
208
  reset() {
193
- // console.log('Resetting player');
194
209
  this.stopPlayback();
210
+ this.stopped = false;
195
211
  this.currentIndex = 0;
196
212
  this.shouldMonitorLoudness = true;
197
- //reset to the beginning when the class gets initialized
198
213
  this.isMonitoring = false;
199
214
  this.isPlaying = false;
200
215
  this.init();
@@ -35,24 +35,6 @@ export interface Message {
35
35
  toolCalls?: ToolInvocation[];
36
36
  }
37
37
  export type OnLLMResponse = (id: string, response: string, finished: boolean, toolInvocations?: ToolInvocation[]) => void;
38
- export interface ObjectRequest {
39
- /**
40
- * The tools that the AI can use.
41
- */
42
- tool: AIObjectTool;
43
- /**
44
- * High level instructions for the AI to follow. Behaviour, tone, restrictions, etc.
45
- * Example: "Act like a recipe writer."
46
- */
47
- /** @deprecated Use server-side prompt definitions (prompt + variables) instead of building requests client-side. */
48
- behaviour?: string;
49
- /**
50
- * The specific instruction for the AI to follow.
51
- * Example: "Generate a recipe using chicken, rice and vegetables."
52
- */
53
- /** @deprecated Use server-side prompt definitions (prompt + variables) instead of building requests client-side. */
54
- instructions: string;
55
- }
56
38
  /**
57
39
  * Controller for AI-related operations.
58
40
  * Provides access to text generation, voice synthesis, and object generation.
@@ -88,24 +70,41 @@ export declare class AIModule {
88
70
  setOnRateLimited(cb: (exercisesRemaining: number) => void): void;
89
71
  /**
90
72
  * Generate text from messages using AI.
91
- * @param messages The messages to generate text from.
92
- * @param tools Optional tools to use for generation.
93
- * @param cache Whether to cache the result (default: false).
94
- * @param model The model to use for generation.
73
+ * @param params.messages The messages to generate text from.
74
+ * @param params.tools Optional tools to use for generation.
75
+ * @param params.cache Whether to cache the result (default: false).
76
+ * @param params.model The model to use for generation.
77
+ * @param params.prompt Server-side prompt name (e.g. 'writing.analysis').
78
+ * @param params.variables Variables for the server-side prompt template.
95
79
  * @returns The generated text.
96
80
  */
97
- getText(messages: Message[], tools?: Tool[], cache?: boolean, model?: string): Promise<string>;
81
+ getText(params: {
82
+ messages: Message[];
83
+ tools?: Tool[];
84
+ cache?: boolean;
85
+ model?: string;
86
+ prompt?: string;
87
+ variables?: Record<string, any>;
88
+ }): Promise<string>;
98
89
  /**
99
90
  * Stream text generation from messages using AI.
100
- * @param messages The messages to generate text from.
101
- * @param onMessage Callback for each message chunk.
102
- * @param tools Optional tools to use for generation.
103
- * @param cache Whether to cache the result (default: false).
104
- * @param model The model to use for generation.
91
+ * @param params.messages The messages to generate text from.
92
+ * @param params.onMessage Callback for each message chunk.
93
+ * @param params.tools Optional tools to use for generation.
94
+ * @param params.cache Whether to cache the result (default: false).
95
+ * @param params.model The model to use for generation.
96
+ * @param params.prompt Server-side prompt name (e.g. 'writing.analysis').
97
+ * @param params.variables Variables for the server-side prompt template.
105
98
  */
106
- getSteamedText(messages: Message[], onMessage: OnLLMResponse, tools?: Tool[], cache?: boolean, model?: string,
107
- /** @deprecated Use uuid variable with resolver 'knowledgeEntry' in prompt definitions instead. */
108
- knowledgeId?: string, prompt?: string, variables?: Record<string, any>): Promise<string>;
99
+ getStreamedText(params: {
100
+ messages: Message[];
101
+ onMessage: OnLLMResponse;
102
+ tools?: Tool[];
103
+ cache?: boolean;
104
+ model?: string;
105
+ prompt?: string;
106
+ variables?: Record<string, any>;
107
+ }): Promise<string>;
109
108
  /**
110
109
  * Generate voice audio from text using AI.
111
110
  * @param text The text to convert to voice.
@@ -123,8 +122,6 @@ export declare class AIModule {
123
122
  * @returns The transcribed text.
124
123
  */
125
124
  getTextFromVoice(file: Blob, language?: Language): Promise<string>;
126
- /** @deprecated Used by legacy client-side prompt path. Will be removed once all plugins migrate to server-side prompt definitions. */
127
- private getChatMessage;
128
125
  /**
129
126
  * Generate a structured object from a request using AI.
130
127
  * @param request.cache Whether to cache the result (default: false).
@@ -135,17 +132,9 @@ export declare class AIModule {
135
132
  * @returns The generated object.
136
133
  */
137
134
  getObject<T = any>(params: {
138
- /** @deprecated Use prompt + variables instead of client-side system prompts. */
139
- systemPrompt?: string;
140
- /** @deprecated Use prompt + variables instead. Schema is loaded server-side from the prompt definition. */
141
- responseSchema?: AIObjectTool;
142
- /** @deprecated Use prompt + variables instead of client-side user prompts. */
143
- userPrompt?: string;
144
135
  cache?: boolean;
145
136
  tools?: Tool[];
146
137
  model?: string;
147
- /** @deprecated Use uuid variable with resolver 'knowledgeEntry' in prompt definitions instead. */
148
- knowledgeId?: string;
149
138
  prompt?: string;
150
139
  variables?: Record<string, any>;
151
140
  }): Promise<T>;
@@ -159,18 +148,10 @@ export declare class AIModule {
159
148
  * @param request.variables Variables for the server-side prompt template.
160
149
  */
161
150
  getStreamedObject<T = any>(params: {
162
- /** @deprecated Use prompt + variables instead of client-side system prompts. */
163
- systemPrompt?: string;
164
- /** @deprecated Use prompt + variables instead. Schema is loaded server-side from the prompt definition. */
165
- responseSchema?: AIObjectTool;
166
- /** @deprecated Use prompt + variables instead of client-side user prompts. */
167
- userPrompt?: string;
168
151
  onResult: OnStreamedObjectResult<T>;
169
152
  cache?: boolean;
170
153
  tools?: Tool[];
171
154
  model?: string;
172
- /** @deprecated Use uuid variable with resolver 'knowledgeEntry' in prompt definitions instead. */
173
- knowledgeId?: string;
174
155
  prompt?: string;
175
156
  variables?: Record<string, any>;
176
157
  }): Promise<T>;
@@ -75,42 +75,47 @@ export class AIModule {
75
75
  }
76
76
  /**
77
77
  * Generate text from messages using AI.
78
- * @param messages The messages to generate text from.
79
- * @param tools Optional tools to use for generation.
80
- * @param cache Whether to cache the result (default: false).
81
- * @param model The model to use for generation.
78
+ * @param params.messages The messages to generate text from.
79
+ * @param params.tools Optional tools to use for generation.
80
+ * @param params.cache Whether to cache the result (default: false).
81
+ * @param params.model The model to use for generation.
82
+ * @param params.prompt Server-side prompt name (e.g. 'writing.analysis').
83
+ * @param params.variables Variables for the server-side prompt template.
82
84
  * @returns The generated text.
83
85
  */
84
- getText(messages_1, tools_1) {
85
- return __awaiter(this, arguments, void 0, function* (messages, tools, cache = false, model) {
86
+ getText(params) {
87
+ return __awaiter(this, void 0, void 0, function* () {
88
+ const { messages, tools, cache = false, model, prompt, variables } = params;
86
89
  const { result } = yield this.streamObject({
87
90
  cache,
88
91
  tools,
89
92
  model,
90
93
  messages,
94
+ prompt,
95
+ variables,
91
96
  });
92
97
  return result;
93
98
  });
94
99
  }
95
100
  /**
96
101
  * Stream text generation from messages using AI.
97
- * @param messages The messages to generate text from.
98
- * @param onMessage Callback for each message chunk.
99
- * @param tools Optional tools to use for generation.
100
- * @param cache Whether to cache the result (default: false).
101
- * @param model The model to use for generation.
102
+ * @param params.messages The messages to generate text from.
103
+ * @param params.onMessage Callback for each message chunk.
104
+ * @param params.tools Optional tools to use for generation.
105
+ * @param params.cache Whether to cache the result (default: false).
106
+ * @param params.model The model to use for generation.
107
+ * @param params.prompt Server-side prompt name (e.g. 'writing.analysis').
108
+ * @param params.variables Variables for the server-side prompt template.
102
109
  */
103
- getSteamedText(messages_1, onMessage_1, tools_1) {
104
- return __awaiter(this, arguments, void 0, function* (messages, onMessage, tools, cache = false, model,
105
- /** @deprecated Use uuid variable with resolver 'knowledgeEntry' in prompt definitions instead. */
106
- knowledgeId, prompt, variables) {
110
+ getStreamedText(params) {
111
+ return __awaiter(this, void 0, void 0, function* () {
112
+ const { messages, onMessage, tools, cache = false, model, prompt, variables, } = params;
107
113
  const messageId = Math.random().toString(36).substring(3);
108
114
  const { result } = yield this.streamObject({
109
115
  cache,
110
116
  tools,
111
117
  model,
112
118
  messages,
113
- knowledgeId,
114
119
  prompt,
115
120
  variables,
116
121
  onResult: ({ result }) => onMessage(messageId, result, false),
@@ -179,14 +184,6 @@ export class AIModule {
179
184
  });
180
185
  });
181
186
  }
182
- /** @deprecated Used by legacy client-side prompt path. Will be removed once all plugins migrate to server-side prompt definitions. */
183
- getChatMessage(systemPrompt, userPrompt) {
184
- const messages = [{ role: 'system', content: systemPrompt }];
185
- if (userPrompt) {
186
- messages.push({ role: 'user', content: userPrompt });
187
- }
188
- return messages;
189
- }
190
187
  /**
191
188
  * Generate a structured object from a request using AI.
192
189
  * @param request.cache Whether to cache the result (default: false).
@@ -198,14 +195,12 @@ export class AIModule {
198
195
  */
199
196
  getObject(params) {
200
197
  return __awaiter(this, void 0, void 0, function* () {
201
- const { systemPrompt, responseSchema, userPrompt, cache = false, tools = [], model = undefined, knowledgeId, prompt, variables, } = params;
198
+ const { cache = false, tools = [], model = undefined, prompt, variables } = params;
202
199
  return yield this.streamObject({
203
- responseSchema,
204
- messages: systemPrompt ? this.getChatMessage(systemPrompt, userPrompt) : [],
200
+ messages: [],
205
201
  cache,
206
202
  tools,
207
203
  model,
208
- knowledgeId,
209
204
  prompt,
210
205
  variables,
211
206
  });
@@ -222,15 +217,13 @@ export class AIModule {
222
217
  */
223
218
  getStreamedObject(params) {
224
219
  return __awaiter(this, void 0, void 0, function* () {
225
- const { systemPrompt, responseSchema, userPrompt, onResult, cache = false, tools = [], model = undefined, knowledgeId, prompt, variables, } = params;
220
+ const { onResult, cache = false, tools = [], model = undefined, prompt, variables } = params;
226
221
  return yield this.streamObject({
227
- responseSchema,
228
- messages: systemPrompt ? this.getChatMessage(systemPrompt, userPrompt) : [],
222
+ messages: [],
229
223
  onResult,
230
224
  cache,
231
225
  tools,
232
226
  model,
233
- knowledgeId,
234
227
  prompt,
235
228
  variables,
236
229
  });
@@ -239,7 +232,7 @@ export class AIModule {
239
232
  streamObject(params) {
240
233
  return __awaiter(this, void 0, void 0, function* () {
241
234
  var _a, _b, _c, _d, _e;
242
- const { messages, responseSchema, onResult = () => null, cache = false, tools = [], model = undefined, knowledgeId, prompt, variables, } = params;
235
+ const { messages, onResult = () => null, cache = false, tools = [], model = undefined, prompt, variables, } = params;
243
236
  const chatMessages = messages.map((message, index) => (Object.assign(Object.assign({}, message), { id: `${index + 1}` })));
244
237
  const payload = {
245
238
  cache,
@@ -247,15 +240,11 @@ export class AIModule {
247
240
  stream: true,
248
241
  messages: chatMessages,
249
242
  model,
250
- knowledge_id: knowledgeId,
251
243
  session_token_id: (_a = this.sessionTokenId) !== null && _a !== void 0 ? _a : undefined,
252
244
  };
253
245
  if (prompt) {
254
246
  payload.prompt = { name: this.resolvePromptName(prompt), variables: variables !== null && variables !== void 0 ? variables : {} };
255
247
  }
256
- if (responseSchema) {
257
- payload.responseSchema = responseSchema;
258
- }
259
248
  const response = yield fetch(`${this.backendUrl}/ai/llm`, {
260
249
  body: JSON.stringify(payload),
261
250
  method: 'POST',
@@ -1,6 +1,6 @@
1
1
  import { MainPanelAction, SidebarAction } from '../../fromRimori/PluginTypes';
2
2
  import { AccomplishmentPayload } from '../../controller/AccomplishmentController';
3
- import { EventBusMessage, EventHandler, EventPayload, EventListener } from '../../fromRimori/EventBus';
3
+ import { EventBusHandler, EventBusMessage, EventHandler, EventPayload, EventListener } from '../../fromRimori/EventBus';
4
4
  import { AIModule } from './AIModule';
5
5
  /**
6
6
  * Event module for plugin event bus operations.
@@ -12,7 +12,8 @@ export declare class EventModule {
12
12
  private aiModule;
13
13
  private backendUrl;
14
14
  private getToken;
15
- constructor(pluginId: string, backendUrl: string, getToken: () => string, aiModule: AIModule);
15
+ private eventBus;
16
+ constructor(pluginId: string, backendUrl: string, getToken: () => string, aiModule: AIModule, eventBus?: EventBusHandler);
16
17
  getGlobalEventTopic(preliminaryTopic: string): string;
17
18
  /**
18
19
  * Emit an event to Rimori or a plugin.
@@ -14,12 +14,13 @@ import { EventBus } from '../../fromRimori/EventBus';
14
14
  * Provides methods for emitting, listening to, and responding to events.
15
15
  */
16
16
  export class EventModule {
17
- constructor(pluginId, backendUrl, getToken, aiModule) {
17
+ constructor(pluginId, backendUrl, getToken, aiModule, eventBus) {
18
18
  this.pluginId = pluginId;
19
19
  this.backendUrl = backendUrl;
20
20
  this.getToken = getToken;
21
21
  this.aiModule = aiModule;
22
- this.accomplishmentController = new AccomplishmentController(pluginId);
22
+ this.eventBus = eventBus !== null && eventBus !== void 0 ? eventBus : EventBus;
23
+ this.accomplishmentController = new AccomplishmentController(pluginId, this.eventBus);
23
24
  }
24
25
  getGlobalEventTopic(preliminaryTopic) {
25
26
  var _a;
@@ -54,7 +55,7 @@ export class EventModule {
54
55
  */
55
56
  emit(topic, data, eventId) {
56
57
  const globalTopic = this.getGlobalEventTopic(topic);
57
- EventBus.emit(this.pluginId, globalTopic, data, eventId);
58
+ this.eventBus.emit(this.pluginId, globalTopic, data, eventId);
58
59
  }
59
60
  /**
60
61
  * Request an event.
@@ -68,7 +69,7 @@ export class EventModule {
68
69
  const globalTopic = this.getGlobalEventTopic(topic);
69
70
  yield this.aiModule.session.ensure();
70
71
  const sessionToken = (_a = this.aiModule.session.get()) !== null && _a !== void 0 ? _a : undefined;
71
- return EventBus.request(this.pluginId, globalTopic, data, sessionToken);
72
+ return this.eventBus.request(this.pluginId, globalTopic, data, sessionToken);
72
73
  });
73
74
  }
74
75
  /**
@@ -79,7 +80,7 @@ export class EventModule {
79
80
  */
80
81
  on(topic, callback) {
81
82
  const topics = Array.isArray(topic) ? topic : [topic];
82
- return EventBus.on(topics.map((t) => this.getGlobalEventTopic(t)), (event) => {
83
+ return this.eventBus.on(topics.map((t) => this.getGlobalEventTopic(t)), (event) => {
83
84
  if (event.ai_session_token && !this.aiModule.session.get()) {
84
85
  this.aiModule.session.set(event.ai_session_token);
85
86
  }
@@ -92,7 +93,7 @@ export class EventModule {
92
93
  * @param callback The callback to call when the event is emitted.
93
94
  */
94
95
  once(topic, callback) {
95
- EventBus.once(this.getGlobalEventTopic(topic), callback);
96
+ this.eventBus.once(this.getGlobalEventTopic(topic), callback);
96
97
  }
97
98
  /**
98
99
  * Respond to an event.
@@ -124,7 +125,7 @@ export class EventModule {
124
125
  }
125
126
  });
126
127
  }
127
- EventBus.respond(this.pluginId, topics.map((t) => this.getGlobalEventTopic(t)), wrappedData);
128
+ this.eventBus.respond(this.pluginId, topics.map((t) => this.getGlobalEventTopic(t)), wrappedData);
128
129
  }
129
130
  /**
130
131
  * Emit an accomplishment.
@@ -219,15 +220,21 @@ export class EventModule {
219
220
  */
220
221
  onSidePanelAction(callback, actionsToListen = []) {
221
222
  const listeningActions = Array.isArray(actionsToListen) ? actionsToListen : [actionsToListen];
222
- // this needs to be a emit and on because the main panel action is triggered by the user and not by the plugin
223
- this.emit('action.requestSidebar');
224
- return this.on('action.requestSidebar', ({ data }) => {
225
- // console.log("eventHandler .onSidePanelAction", data);
226
- // console.log('Received action for sidebar ' + data.action);
227
- // console.log('Listening to actions', listeningActions);
223
+ // Register the listener BEFORE emitting the request, so the synchronous response
224
+ // from the bridge/responder is captured (emit → bridge outbound → host respond → bridge inbound is synchronous).
225
+ console.log('[EventModule] onSidePanelAction: setting up listener for', this.pluginId, 'listening to:', listeningActions);
226
+ const listener = this.on('action.requestSidebar', ({ data }) => {
227
+ console.log('[EventModule] onSidePanelAction: received event', { data, listeningActions });
228
228
  if (listeningActions.length === 0 || listeningActions.includes(data.action)) {
229
+ console.log('[EventModule] onSidePanelAction: action matched, calling callback');
229
230
  callback(data);
230
231
  }
232
+ else {
233
+ console.log('[EventModule] onSidePanelAction: action NOT matched. Got:', data.action, 'expected:', listeningActions);
234
+ }
231
235
  });
236
+ console.log('[EventModule] onSidePanelAction: emitting action.requestSidebar for', this.pluginId);
237
+ this.emit('action.requestSidebar');
238
+ return listener;
232
239
  }
233
240
  }
@@ -23,6 +23,11 @@ export declare class PluginModule {
23
23
  releaseChannel: string;
24
24
  applicationMode: ApplicationMode;
25
25
  theme: Theme;
26
+ /**
27
+ * Whether text-to-speech is globally enabled (set in rimori-main navbar).
28
+ * Updated automatically when the user toggles TTS.
29
+ */
30
+ ttsEnabled: boolean;
26
31
  constructor(supabase: SupabaseClient, communicationHandler: RimoriCommunicationHandler, info: RimoriInfo, ai: AIModule);
27
32
  /**
28
33
  * Fetches settings based on guild configuration.
@@ -14,6 +14,12 @@ import { Translator } from '../../controller/TranslationController';
14
14
  */
15
15
  export class PluginModule {
16
16
  constructor(supabase, communicationHandler, info, ai) {
17
+ var _a;
18
+ /**
19
+ * Whether text-to-speech is globally enabled (set in rimori-main navbar).
20
+ * Updated automatically when the user toggles TTS.
21
+ */
22
+ this.ttsEnabled = true;
17
23
  this.rimoriInfo = info;
18
24
  this.supabase = supabase;
19
25
  this.pluginId = info.pluginId;
@@ -21,7 +27,12 @@ export class PluginModule {
21
27
  this.communicationHandler = communicationHandler;
22
28
  const currentPlugin = info.installedPlugins.find((plugin) => plugin.id === info.pluginId);
23
29
  this.translator = new Translator(info.interfaceLanguage, (currentPlugin === null || currentPlugin === void 0 ? void 0 : currentPlugin.endpoint) || '', ai);
24
- this.communicationHandler.onUpdate((updatedInfo) => (this.rimoriInfo = updatedInfo));
30
+ this.ttsEnabled = (_a = info.ttsEnabled) !== null && _a !== void 0 ? _a : true;
31
+ this.communicationHandler.onUpdate((updatedInfo) => {
32
+ var _a;
33
+ this.rimoriInfo = updatedInfo;
34
+ this.ttsEnabled = (_a = updatedInfo.ttsEnabled) !== null && _a !== void 0 ? _a : true;
35
+ });
25
36
  this.applicationMode = this.communicationHandler.getQueryParam('applicationMode');
26
37
  this.theme = this.communicationHandler.getQueryParam('rm_theme') || 'light';
27
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimori/client",
3
- "version": "2.5.28",
3
+ "version": "2.5.29-next.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {
@@ -36,9 +36,9 @@
36
36
  "format": "prettier --write ."
37
37
  },
38
38
  "dependencies": {
39
- "@supabase/postgrest-js": "^2.98.0",
39
+ "@supabase/postgrest-js": "^2.100.1",
40
40
  "dotenv": "^16.5.0",
41
- "i18next": "^25.8.15"
41
+ "i18next": "^25.10.10"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@eslint/js": "^9.37.0",