@rimori/client 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+ export interface Plugin {
3
+ id: string;
4
+ title: string;
5
+ icon_url: string;
6
+ website: string;
7
+ context_menu_actions: string;
8
+ plugin_pages: string;
9
+ sidebar_pages: string;
10
+ settings_page: string;
11
+ version: string;
12
+ external_hosted_url: string;
13
+ }
14
+ export declare function getPlugins(supabase: SupabaseClient): Promise<Plugin[]>;
@@ -0,0 +1,30 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ export function getPlugins(supabase) {
11
+ return __awaiter(this, void 0, void 0, function* () {
12
+ let { data, error } = yield supabase.from('plugins').select('*');
13
+ if (error) {
14
+ console.error(error);
15
+ return [];
16
+ }
17
+ return (data || []).map((plugin) => ({
18
+ id: plugin.id,
19
+ title: plugin.title,
20
+ icon_url: plugin.icon_url,
21
+ website: plugin.website,
22
+ context_menu_actions: plugin.context_menu_actions,
23
+ plugin_pages: plugin.plugin_pages,
24
+ sidebar_pages: plugin.sidebar_pages,
25
+ settings_page: plugin.settings_page,
26
+ version: plugin.version,
27
+ external_hosted_url: plugin.external_hosted_url,
28
+ }));
29
+ });
30
+ }
@@ -3,6 +3,7 @@ import { LanguageLevel } from "../utils/difficultyConverter";
3
3
  export interface UserSettings {
4
4
  motherTongue: string;
5
5
  languageLevel: LanguageLevel;
6
+ contextMenuOnSelect: boolean;
6
7
  }
7
8
  export interface SystemSettings {
8
9
  }
@@ -0,0 +1,14 @@
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+ export interface Plugin {
3
+ id: string;
4
+ title: string;
5
+ icon_url: string;
6
+ website: string;
7
+ context_menu_actions: string;
8
+ plugin_pages: string;
9
+ sidebar_pages: string;
10
+ settings_page: string;
11
+ version: string;
12
+ external_hosted_url: string;
13
+ }
14
+ export declare function getPlugins(supabase: SupabaseClient): Promise<Plugin[]>;
@@ -0,0 +1,30 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ export function getPlugins(supabase) {
11
+ return __awaiter(this, void 0, void 0, function* () {
12
+ let { data, error } = yield supabase.from('plugins').select('*');
13
+ if (error) {
14
+ console.error(error);
15
+ return [];
16
+ }
17
+ return (data || []).map((plugin) => ({
18
+ id: plugin.id,
19
+ title: plugin.title,
20
+ icon_url: plugin.icon_url,
21
+ website: plugin.website,
22
+ context_menu_actions: plugin.context_menu_actions,
23
+ plugin_pages: plugin.plugin_pages,
24
+ sidebar_pages: plugin.sidebar_pages,
25
+ settings_page: plugin.settings_page,
26
+ version: plugin.version,
27
+ external_hosted_url: plugin.external_hosted_url,
28
+ }));
29
+ });
30
+ }
@@ -11,7 +11,7 @@ export function getSTTResponse(supabase, audio) {
11
11
  return __awaiter(this, void 0, void 0, function* () {
12
12
  const formData = new FormData();
13
13
  formData.append('file', audio);
14
- return yield supabase.functions.invoke('speech', { method: 'POST', body: formData }).then((r) => r.text);
14
+ return yield supabase.functions.invoke('speech', { method: 'POST', body: formData }).then(({ data }) => data.text);
15
15
  });
16
16
  }
17
17
  export function getTTSResponse(supabaseUrl, request, token) {
@@ -5,6 +5,7 @@ import { PostgrestQueryBuilder, PostgrestFilterBuilder } from "@supabase/postgre
5
5
  import { BasicAssignment } from "../controller/SharedContentController";
6
6
  import { Message, Tool, OnLLMResponse } from "../controller/AIController";
7
7
  import { ObjectRequest } from "../controller/ObjectController";
8
+ import { Plugin } from "../controller/SidePluginController";
8
9
  export declare class RimoriClient {
9
10
  private static instance;
10
11
  private superbase;
@@ -63,6 +64,11 @@ export declare class RimoriClient {
63
64
  getAIResponseStream(messages: Message[], onMessage: OnLLMResponse, tools?: Tool[]): Promise<void>;
64
65
  getVoiceResponse(text: string, voice?: string, speed?: number, language?: string): Promise<Blob>;
65
66
  getVoiceToTextResponse(file: Blob): Promise<string>;
67
+ /**
68
+ * Fetches all installed plugins.
69
+ * @returns A promise that resolves to an array of plugins
70
+ */
71
+ getPlugins(): Promise<Plugin[]>;
66
72
  generateObject(request: ObjectRequest): Promise<any>;
67
73
  /**
68
74
  * Fetch new shared content.
@@ -88,4 +94,5 @@ export declare class RimoriClient {
88
94
  * @param assignmentId The id of the shared content item to complete.
89
95
  */
90
96
  completeSharedContent(type: string, assignmentId: string): Promise<void>;
97
+ triggerSidebarAction(pluginId: string, actionKey: string, text?: string): void;
91
98
  }
@@ -12,6 +12,7 @@ import { getSTTResponse, getTTSResponse } from "../controller/VoiceController";
12
12
  import { SharedContentController } from "../controller/SharedContentController";
13
13
  import { streamChatGPT, generateText } from "../controller/AIController";
14
14
  import { generateObject as generateObjectFunction } from "../controller/ObjectController";
15
+ import { getPlugins } from "../controller/SidePluginController";
15
16
  export class RimoriClient {
16
17
  constructor(options) {
17
18
  this.superbase = options.supabase;
@@ -121,6 +122,15 @@ export class RimoriClient {
121
122
  getVoiceToTextResponse(file) {
122
123
  return getSTTResponse(this.superbase, file);
123
124
  }
125
+ /**
126
+ * Fetches all installed plugins.
127
+ * @returns A promise that resolves to an array of plugins
128
+ */
129
+ getPlugins() {
130
+ return __awaiter(this, void 0, void 0, function* () {
131
+ return getPlugins(this.superbase);
132
+ });
133
+ }
124
134
  generateObject(request) {
125
135
  return __awaiter(this, void 0, void 0, function* () {
126
136
  const token = yield this.plugin.getToken();
@@ -160,4 +170,7 @@ export class RimoriClient {
160
170
  return this.sharedContentController.completeSharedContent(type, assignmentId);
161
171
  });
162
172
  }
173
+ triggerSidebarAction(pluginId, actionKey, text) {
174
+ this.emit("triggerSidebarAction", { pluginId, actionKey, text });
175
+ }
163
176
  }
@@ -4,6 +4,7 @@ import { PluginController } from '../plugin/PluginController';
4
4
  const PluginContext = createContext(null);
5
5
  export const PluginProvider = ({ children }) => {
6
6
  const [plugin, setPlugin] = useState(null);
7
+ const [contextMenuOnSelect, setContextMenuOnTextSelection] = useState(false);
7
8
  //route change
8
9
  useEffect(() => {
9
10
  let lastHash = window.location.hash;
@@ -16,28 +17,81 @@ export const PluginProvider = ({ children }) => {
16
17
  }, 100);
17
18
  PluginController.getInstance().then(setPlugin);
18
19
  }, []);
20
+ //check if context menu opens on text selection
21
+ useEffect(() => {
22
+ if (!plugin)
23
+ return;
24
+ plugin.getSettings({
25
+ languageLevel: "A1",
26
+ motherTongue: "English",
27
+ contextMenuOnSelect: false,
28
+ }, "user").then((settings) => {
29
+ setContextMenuOnTextSelection(settings.contextMenuOnSelect);
30
+ }).catch(error => {
31
+ console.error('Error fetching settings:', error);
32
+ });
33
+ }, [plugin]);
34
+ //detect page height change
35
+ useEffect(() => {
36
+ const body = document.body;
37
+ const handleResize = () => plugin === null || plugin === void 0 ? void 0 : plugin.emit('heightAdjustment', body.clientHeight);
38
+ body.addEventListener('resize', handleResize);
39
+ handleResize();
40
+ return () => body.removeEventListener('resize', handleResize);
41
+ }, [plugin]);
19
42
  //context menu
20
43
  useEffect(() => {
21
- let isOpen = false;
44
+ let lastMouseX = 0;
45
+ let lastMouseY = 0;
46
+ let isSelecting = false;
47
+ // Track mouse position
48
+ const handleMouseMove = (e) => {
49
+ lastMouseX = e.clientX;
50
+ lastMouseY = e.clientY;
51
+ };
22
52
  const handleContextMenu = (e) => {
23
53
  var _a;
24
54
  const selection = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString().trim();
25
55
  if (selection) {
26
56
  e.preventDefault();
27
- // console.log('context menu', selection);
57
+ // console.log('context menu handled', selection);
28
58
  plugin === null || plugin === void 0 ? void 0 : plugin.emit('contextMenu', { text: selection, x: e.clientX, y: e.clientY, open: true });
29
- isOpen = true;
30
59
  }
31
60
  };
32
- // Hide the menu on click outside
33
- const handleClick = () => isOpen && (plugin === null || plugin === void 0 ? void 0 : plugin.emit('contextMenu', { text: '', x: 0, y: 0, open: false }));
34
- document.addEventListener("click", handleClick);
61
+ const handleSelectionChange = () => {
62
+ var _a;
63
+ // if (triggerOnTextSelection) {
64
+ const selection = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString().trim();
65
+ const open = !!selection && isSelecting;
66
+ // console.log('Selection change, contextMenuOnSelect:', contextMenuOnSelect);
67
+ plugin === null || plugin === void 0 ? void 0 : plugin.emit('contextMenu', { text: selection, x: lastMouseX, y: lastMouseY, open });
68
+ // }
69
+ };
70
+ const handleMouseUpDown = (e) => {
71
+ if (e.type === 'mousedown') {
72
+ isSelecting = false;
73
+ }
74
+ else if (e.type === 'mouseup') {
75
+ isSelecting = true;
76
+ // console.log('mouseup, contextMenuOnSelect:', contextMenuOnSelect);
77
+ if (contextMenuOnSelect) {
78
+ handleSelectionChange();
79
+ }
80
+ }
81
+ };
35
82
  document.addEventListener('contextmenu', handleContextMenu);
83
+ document.addEventListener('selectionchange', handleSelectionChange);
84
+ document.addEventListener("mousemove", handleMouseMove);
85
+ document.addEventListener('mousedown', handleMouseUpDown);
86
+ document.addEventListener('mouseup', handleMouseUpDown);
36
87
  return () => {
37
- document.removeEventListener("click", handleClick);
88
+ document.removeEventListener("mousemove", handleMouseMove);
38
89
  document.removeEventListener('contextmenu', handleContextMenu);
90
+ document.removeEventListener('selectionchange', handleSelectionChange);
91
+ document.removeEventListener('mousedown', handleMouseUpDown);
92
+ document.removeEventListener('mouseup', handleMouseUpDown);
39
93
  };
40
- }, [plugin]);
94
+ }, [plugin, contextMenuOnSelect]);
41
95
  if (!plugin) {
42
96
  return "";
43
97
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimori/client",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {
@@ -6,6 +6,7 @@ type SettingsType = "user" | "system" | "plugin";
6
6
  export interface UserSettings {
7
7
  motherTongue: string;
8
8
  languageLevel: LanguageLevel;
9
+ contextMenuOnSelect: boolean;
9
10
  }
10
11
 
11
12
  export interface SystemSettings {
@@ -0,0 +1,36 @@
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+
3
+ export interface Plugin {
4
+ id: string;
5
+ title: string;
6
+ icon_url: string;
7
+ website: string;
8
+ context_menu_actions: string;
9
+ plugin_pages: string;
10
+ sidebar_pages: string;
11
+ settings_page: string;
12
+ version: string;
13
+ external_hosted_url: string;
14
+ }
15
+
16
+ export async function getPlugins(supabase: SupabaseClient): Promise<Plugin[]> {
17
+ let { data, error } = await supabase.from('plugins').select('*');
18
+
19
+ if (error) {
20
+ console.error(error);
21
+ return [];
22
+ }
23
+
24
+ return (data || []).map((plugin: any) => ({
25
+ id: plugin.id,
26
+ title: plugin.title,
27
+ icon_url: plugin.icon_url,
28
+ website: plugin.website,
29
+ context_menu_actions: plugin.context_menu_actions,
30
+ plugin_pages: plugin.plugin_pages,
31
+ sidebar_pages: plugin.sidebar_pages,
32
+ settings_page: plugin.settings_page,
33
+ version: plugin.version,
34
+ external_hosted_url: plugin.external_hosted_url,
35
+ }));
36
+ }
@@ -4,7 +4,7 @@ export async function getSTTResponse(supabase: SupabaseClient, audio: Blob) {
4
4
  const formData = new FormData();
5
5
  formData.append('file', audio);
6
6
 
7
- return await supabase.functions.invoke('speech', { method: 'POST', body: formData }).then((r: any) => r.text);
7
+ return await supabase.functions.invoke('speech', { method: 'POST', body: formData }).then(({ data }) => data.text);
8
8
  }
9
9
 
10
10
  export async function getTTSResponse(supabaseUrl: string, request: TTSRequest, token: string) {
@@ -7,6 +7,7 @@ import { PostgrestQueryBuilder, PostgrestFilterBuilder } from "@supabase/postgre
7
7
  import { SharedContentController, BasicAssignment } from "../controller/SharedContentController";
8
8
  import { streamChatGPT, Message, Tool, OnLLMResponse, generateText } from "../controller/AIController";
9
9
  import { generateObject as generateObjectFunction, ObjectRequest } from "../controller/ObjectController";
10
+ import { getPlugins, Plugin } from "../controller/SidePluginController";
10
11
 
11
12
  interface RimoriClientOptions {
12
13
  pluginController: PluginController;
@@ -166,6 +167,14 @@ export class RimoriClient {
166
167
  return getSTTResponse(this.superbase, file);
167
168
  }
168
169
 
170
+ /**
171
+ * Fetches all installed plugins.
172
+ * @returns A promise that resolves to an array of plugins
173
+ */
174
+ public async getPlugins(): Promise<Plugin[]> {
175
+ return getPlugins(this.superbase);
176
+ }
177
+
169
178
  public async generateObject(request: ObjectRequest): Promise<any> {
170
179
  const token = await this.plugin.getToken();
171
180
  return generateObjectFunction(request, token);
@@ -204,4 +213,8 @@ export class RimoriClient {
204
213
  public async completeSharedContent(type: string, assignmentId: string) {
205
214
  return this.sharedContentController.completeSharedContent(type, assignmentId);
206
215
  }
216
+
217
+ public triggerSidebarAction(pluginId: string, actionKey: string, text?: string) {
218
+ this.emit("triggerSidebarAction", { pluginId, actionKey, text });
219
+ }
207
220
  }
@@ -1,6 +1,8 @@
1
1
  import React, { createContext, useContext, ReactNode, useEffect, useState } from 'react';
2
2
  import { PluginController } from '../plugin/PluginController';
3
3
  import { RimoriClient } from '../plugin/RimoriClient';
4
+ import { UserSettings } from '../controller/SettingsController';
5
+
4
6
  interface PluginProviderProps {
5
7
  children: ReactNode;
6
8
  }
@@ -10,6 +12,8 @@ const PluginContext = createContext<RimoriClient | null>(null);
10
12
 
11
13
  export const PluginProvider: React.FC<PluginProviderProps> = ({ children }) => {
12
14
  const [plugin, setPlugin] = useState<RimoriClient | null>(null);
15
+ const [contextMenuOnSelect, setContextMenuOnTextSelection] = useState(false);
16
+
13
17
  //route change
14
18
  useEffect(() => {
15
19
  let lastHash = window.location.hash;
@@ -24,31 +28,85 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children }) => {
24
28
  PluginController.getInstance().then(setPlugin);
25
29
  }, []);
26
30
 
31
+ //check if context menu opens on text selection
32
+ useEffect(() => {
33
+ if (!plugin) return;
34
+ plugin.getSettings<UserSettings>({
35
+ languageLevel: "A1",
36
+ motherTongue: "English",
37
+ contextMenuOnSelect: false,
38
+ }, "user").then((settings) => {
39
+ setContextMenuOnTextSelection(settings.contextMenuOnSelect);
40
+ }).catch(error => {
41
+ console.error('Error fetching settings:', error);
42
+ });
43
+ }, [plugin]);
44
+
45
+ //detect page height change
46
+ useEffect(() => {
47
+ const body = document.body;
48
+ const handleResize = () => plugin?.emit('heightAdjustment', body.clientHeight);
49
+ body.addEventListener('resize', handleResize);
50
+ handleResize();
51
+ return () => body.removeEventListener('resize', handleResize);
52
+ }, [plugin]);
53
+
27
54
  //context menu
28
55
  useEffect(() => {
29
- let isOpen = false;
56
+ let lastMouseX = 0;
57
+ let lastMouseY = 0;
58
+ let isSelecting = false;
59
+
60
+ // Track mouse position
61
+ const handleMouseMove = (e: MouseEvent) => {
62
+ lastMouseX = e.clientX;
63
+ lastMouseY = e.clientY;
64
+ };
65
+
30
66
  const handleContextMenu = (e: MouseEvent) => {
31
67
  const selection = window.getSelection()?.toString().trim();
32
68
  if (selection) {
33
69
  e.preventDefault();
34
- // console.log('context menu', selection);
70
+ // console.log('context menu handled', selection);
35
71
  plugin?.emit('contextMenu', { text: selection, x: e.clientX, y: e.clientY, open: true });
36
- isOpen = true;
37
72
  }
38
73
  };
39
74
 
40
- // Hide the menu on click outside
41
- const handleClick = () => isOpen && plugin?.emit('contextMenu', { text: '', x: 0, y: 0, open: false });
75
+ const handleSelectionChange = () => {
76
+ // if (triggerOnTextSelection) {
77
+ const selection = window.getSelection()?.toString().trim();
78
+ const open = !!selection && isSelecting;
79
+ // console.log('Selection change, contextMenuOnSelect:', contextMenuOnSelect);
80
+ plugin?.emit('contextMenu', { text: selection, x: lastMouseX, y: lastMouseY, open });
81
+ // }
82
+ };
83
+ const handleMouseUpDown = (e: MouseEvent) => {
84
+ if (e.type === 'mousedown') {
85
+ isSelecting = false;
86
+ } else if (e.type === 'mouseup') {
87
+ isSelecting = true;
88
+ // console.log('mouseup, contextMenuOnSelect:', contextMenuOnSelect);
89
+ if (contextMenuOnSelect) {
90
+ handleSelectionChange();
91
+ }
92
+ }
93
+ };
42
94
 
43
- document.addEventListener("click", handleClick);
44
95
  document.addEventListener('contextmenu', handleContextMenu);
96
+ document.addEventListener('selectionchange', handleSelectionChange);
97
+ document.addEventListener("mousemove", handleMouseMove);
98
+ document.addEventListener('mousedown', handleMouseUpDown);
99
+ document.addEventListener('mouseup', handleMouseUpDown);
45
100
  return () => {
46
- document.removeEventListener("click", handleClick);
101
+ document.removeEventListener("mousemove", handleMouseMove);
47
102
  document.removeEventListener('contextmenu', handleContextMenu);
103
+ document.removeEventListener('selectionchange', handleSelectionChange);
104
+ document.removeEventListener('mousedown', handleMouseUpDown);
105
+ document.removeEventListener('mouseup', handleMouseUpDown);
48
106
  };
49
- }, [plugin]);
107
+ }, [plugin, contextMenuOnSelect]);
50
108
 
51
- if(!plugin){
109
+ if (!plugin) {
52
110
  return ""
53
111
  }
54
112