@rimori/client 1.0.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.
- package/dist/CRUDModal.d.ts +16 -0
- package/dist/CRUDModal.js +31 -0
- package/dist/MarkdownEditor.d.ts +8 -0
- package/dist/MarkdownEditor.js +46 -0
- package/dist/audio/Playbutton.d.ts +14 -0
- package/dist/audio/Playbutton.js +73 -0
- package/dist/components/CRUDModal.d.ts +17 -0
- package/dist/components/CRUDModal.js +25 -0
- package/dist/components/MarkdownEditor.d.ts +8 -0
- package/dist/components/MarkdownEditor.js +46 -0
- package/dist/components/Spinner.d.ts +8 -0
- package/dist/components/Spinner.js +5 -0
- package/dist/components/audio/Playbutton.d.ts +15 -0
- package/dist/components/audio/Playbutton.js +78 -0
- package/dist/components/hooks/UseChatHook.d.ts +15 -0
- package/dist/components/hooks/UseChatHook.js +21 -0
- package/dist/controller/AIController.d.ts +22 -0
- package/dist/controller/AIController.js +68 -0
- package/dist/controller/ObjectController.d.ts +34 -0
- package/dist/controller/ObjectController.js +77 -0
- package/dist/controller/SettingsController.d.ts +24 -0
- package/dist/controller/SettingsController.js +72 -0
- package/dist/controller/SharedContentController.d.ts +22 -0
- package/dist/controller/SharedContentController.js +56 -0
- package/dist/controller/VoiceController.d.ts +10 -0
- package/dist/controller/VoiceController.js +28 -0
- package/dist/hooks/UseChatHook.d.ts +9 -0
- package/dist/hooks/UseChatHook.js +21 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/plugin/AIController copy.d.ts +22 -0
- package/dist/plugin/AIController copy.js +68 -0
- package/dist/plugin/AIController.d.ts +22 -0
- package/dist/plugin/AIController.js +68 -0
- package/dist/plugin/ObjectController.d.ts +34 -0
- package/dist/plugin/ObjectController.js +77 -0
- package/dist/plugin/PluginController.d.ts +29 -0
- package/dist/plugin/PluginController.js +138 -0
- package/dist/plugin/RimoriClient.d.ts +91 -0
- package/dist/plugin/RimoriClient.js +163 -0
- package/dist/plugin/SettingController.d.ts +13 -0
- package/dist/plugin/SettingController.js +55 -0
- package/dist/plugin/ThemeSetter.d.ts +1 -0
- package/dist/plugin/ThemeSetter.js +13 -0
- package/dist/plugin/VoiceController.d.ts +2 -0
- package/dist/plugin/VoiceController.js +27 -0
- package/dist/providers/EventEmitter.d.ts +11 -0
- package/dist/providers/EventEmitter.js +41 -0
- package/dist/providers/EventEmitterContext.d.ts +6 -0
- package/dist/providers/EventEmitterContext.js +19 -0
- package/dist/providers/PluginProvider.d.ts +8 -0
- package/dist/providers/PluginProvider.js +52 -0
- package/dist/style.css +110 -0
- package/dist/style.css.map +1 -0
- package/dist/utils/DifficultyConverter.d.ts +3 -0
- package/dist/utils/DifficultyConverter.js +7 -0
- package/dist/utils/PluginUtils.d.ts +2 -0
- package/dist/utils/PluginUtils.js +23 -0
- package/dist/utils/constants.d.ts +4 -0
- package/dist/utils/constants.js +12 -0
- package/dist/utils/difficultyConverter.d.ts +3 -0
- package/dist/utils/difficultyConverter.js +7 -0
- package/dist/utils/plugin/Client.d.ts +72 -0
- package/dist/utils/plugin/Client.js +118 -0
- package/dist/utils/plugin/PluginController.d.ts +36 -0
- package/dist/utils/plugin/PluginController.js +119 -0
- package/dist/utils/plugin/PluginUtils.d.ts +2 -0
- package/dist/utils/plugin/PluginUtils.js +23 -0
- package/dist/utils/plugin/RimoriClient.d.ts +72 -0
- package/dist/utils/plugin/RimoriClient.js +118 -0
- package/dist/utils/plugin/ThemeSetter.d.ts +1 -0
- package/dist/utils/plugin/ThemeSetter.js +13 -0
- package/dist/utils/plugin/WhereClauseBuilder.d.ts +24 -0
- package/dist/utils/plugin/WhereClauseBuilder.js +79 -0
- package/dist/utils/plugin/providers/EventEmitter.d.ts +11 -0
- package/dist/utils/plugin/providers/EventEmitter.js +41 -0
- package/dist/utils/plugin/providers/EventEmitterContext.d.ts +6 -0
- package/dist/utils/plugin/providers/EventEmitterContext.js +19 -0
- package/dist/utils/plugin/providers/PluginProvider.d.ts +8 -0
- package/dist/utils/plugin/providers/PluginProvider.js +49 -0
- package/package.json +30 -0
- package/src/components/CRUDModal.tsx +61 -0
- package/src/components/MarkdownEditor.tsx +111 -0
- package/src/components/Spinner.tsx +24 -0
- package/src/components/audio/Playbutton.tsx +119 -0
- package/src/controller/AIController.ts +87 -0
- package/src/controller/ObjectController.ts +109 -0
- package/src/controller/SettingsController.ts +87 -0
- package/src/controller/SharedContentController.ts +71 -0
- package/src/controller/VoiceController.ts +26 -0
- package/src/hooks/UseChatHook.ts +25 -0
- package/src/index.ts +14 -0
- package/src/plugin/PluginController.ts +158 -0
- package/src/plugin/RimoriClient.ts +207 -0
- package/src/plugin/ThemeSetter.ts +17 -0
- package/src/providers/EventEmitter.ts +48 -0
- package/src/providers/EventEmitterContext.tsx +27 -0
- package/src/providers/PluginProvider.tsx +68 -0
- package/src/style.scss +136 -0
- package/src/utils/PluginUtils.ts +26 -0
- package/src/utils/constants.ts +18 -0
- package/src/utils/difficultyConverter.ts +11 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Child } from "ibridge-flex";
|
|
2
|
+
import { createClient, SupabaseClient } from '@supabase/supabase-js'
|
|
3
|
+
import { RimoriClient } from "./RimoriClient";
|
|
4
|
+
|
|
5
|
+
interface SupabaseInfo {
|
|
6
|
+
url: string,
|
|
7
|
+
key: string,
|
|
8
|
+
token: string,
|
|
9
|
+
expiration: Date,
|
|
10
|
+
tablePrefix: string,
|
|
11
|
+
pluginId: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class PluginController {
|
|
15
|
+
private static instance: PluginController;
|
|
16
|
+
private static client: RimoriClient;
|
|
17
|
+
private plugin: Child<null, null>;
|
|
18
|
+
private onceListeners: Map<string, any[]> = new Map();
|
|
19
|
+
private listeners: Map<string, any[]> = new Map();
|
|
20
|
+
private communicationSecret: string | null = null;
|
|
21
|
+
private initialized = false;
|
|
22
|
+
private supabase: SupabaseClient | null = null;
|
|
23
|
+
private supabaseInfo: SupabaseInfo | null = null;
|
|
24
|
+
|
|
25
|
+
private constructor() {
|
|
26
|
+
// localStorage.debug = "*";
|
|
27
|
+
this.plugin = new Child({
|
|
28
|
+
triggerChild: ({ topic, data, _id }: any) => {
|
|
29
|
+
// console.log("trigger child with topic:" + topic + " and data: ", data);
|
|
30
|
+
this.onceListeners.get(topic)?.forEach((callback: any) => callback(_id, data));
|
|
31
|
+
this.onceListeners.set(topic, []);
|
|
32
|
+
this.listeners.get(topic)?.forEach((callback: any) => callback(_id, data));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
this.emit = this.emit.bind(this);
|
|
37
|
+
this.onOnce = this.onOnce.bind(this);
|
|
38
|
+
this.getClient = this.getClient.bind(this);
|
|
39
|
+
this.subscribe = this.subscribe.bind(this);
|
|
40
|
+
this.internalEmit = this.internalEmit.bind(this);
|
|
41
|
+
this.request = this.request.bind(this);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public static async getInstance(): Promise<RimoriClient> {
|
|
45
|
+
if (!PluginController.instance) {
|
|
46
|
+
PluginController.instance = new PluginController();
|
|
47
|
+
await PluginController.instance.init();
|
|
48
|
+
PluginController.client = await RimoriClient.getInstance(
|
|
49
|
+
PluginController.instance
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return PluginController.client;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async init() {
|
|
56
|
+
if (this.initialized) return;
|
|
57
|
+
|
|
58
|
+
// Wait for the plugin to be ready
|
|
59
|
+
await this.plugin.handshake().then(() => this.initialized = true).catch((error: any) => {
|
|
60
|
+
console.error("Failed to initialize the plugin communication:", error);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private getSecret() {
|
|
65
|
+
if (!this.communicationSecret) {
|
|
66
|
+
const secret = new URLSearchParams(window.location.search).get("secret");
|
|
67
|
+
if (!secret) {
|
|
68
|
+
throw new Error("Communication secret not found in URL as query parameter");
|
|
69
|
+
}
|
|
70
|
+
this.communicationSecret = secret;
|
|
71
|
+
}
|
|
72
|
+
return this.communicationSecret;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public async getClient(): Promise<{ supabase: SupabaseClient, tablePrefix: string, pluginId: string }> {
|
|
76
|
+
if (
|
|
77
|
+
this.supabase &&
|
|
78
|
+
this.supabaseInfo &&
|
|
79
|
+
this.supabaseInfo.expiration > new Date()
|
|
80
|
+
) {
|
|
81
|
+
return { supabase: this.supabase, tablePrefix: this.supabaseInfo.tablePrefix, pluginId: this.supabaseInfo.pluginId };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.supabaseInfo = await this.request<SupabaseInfo>("getSupabaseAccess");
|
|
85
|
+
|
|
86
|
+
this.supabase = createClient(this.supabaseInfo.url, this.supabaseInfo.key, {
|
|
87
|
+
accessToken: () => Promise.resolve(this.getToken())
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return { supabase: this.supabase, tablePrefix: this.supabaseInfo.tablePrefix, pluginId: this.supabaseInfo.pluginId };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public async getToken() {
|
|
94
|
+
if (this.supabaseInfo && this.supabaseInfo.expiration && this.supabaseInfo.expiration > new Date()) {
|
|
95
|
+
return this.supabaseInfo.token;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const response = await this.request<{ token: string, expiration: Date }>("getSupabaseAccess");
|
|
99
|
+
|
|
100
|
+
if (!this.supabaseInfo) {
|
|
101
|
+
throw new Error("Supabase info not found");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.supabaseInfo.token = response.token;
|
|
105
|
+
this.supabaseInfo.expiration = response.expiration;
|
|
106
|
+
|
|
107
|
+
return this.supabaseInfo.token;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public getSupabaseUrl() {
|
|
111
|
+
if (!this.supabaseInfo) {
|
|
112
|
+
throw new Error("Supabase info not found");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return this.supabaseInfo.url;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public emit(eventName: string, data?: any) {
|
|
119
|
+
this.internalEmit(eventName, 0, data);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// the communication needs to have an id to be able to distinguish between different responses
|
|
123
|
+
private internalEmit(eventName: string, id: number, data?: any) {
|
|
124
|
+
this.init().then(() => this.plugin.emitToParent(eventName, { data, _id: id, secret: this.getSecret() }));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public subscribe(eventName: string, callback: (_id: number, data: any) => void) {
|
|
128
|
+
if (!this.listeners.has(eventName)) {
|
|
129
|
+
this.listeners.set(eventName, []);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.listeners.get(eventName)?.push(callback);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public onOnce(eventName: string, callback: (data: any) => void) {
|
|
136
|
+
if (!this.onceListeners.has(eventName)) {
|
|
137
|
+
this.onceListeners.set(eventName, []);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.onceListeners.get(eventName)?.push(callback);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async request<T>(topic: string, data: any = {}): Promise<T> {
|
|
144
|
+
return await new Promise((resolve) => {
|
|
145
|
+
let triggered = false;
|
|
146
|
+
const id = Math.random();
|
|
147
|
+
|
|
148
|
+
this.internalEmit(topic, id, data);
|
|
149
|
+
|
|
150
|
+
this.subscribe(topic, (_id: number, data: any) => {
|
|
151
|
+
if (triggered || (_id !== id && _id !== 0)) return;
|
|
152
|
+
triggered = true;
|
|
153
|
+
|
|
154
|
+
resolve(data)
|
|
155
|
+
})
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { PluginController } from "./PluginController";
|
|
2
|
+
import { SupabaseClient } from "@supabase/supabase-js";
|
|
3
|
+
import { SettingsController } from "../controller/SettingsController";
|
|
4
|
+
import { GenericSchema } from "@supabase/supabase-js/dist/module/lib/types";
|
|
5
|
+
import { getSTTResponse, getTTSResponse } from "../controller/VoiceController";
|
|
6
|
+
import { PostgrestQueryBuilder, PostgrestFilterBuilder } from "@supabase/postgrest-js";
|
|
7
|
+
import { SharedContentController, BasicAssignment } from "../controller/SharedContentController";
|
|
8
|
+
import { streamChatGPT, Message, Tool, OnLLMResponse, generateText } from "../controller/AIController";
|
|
9
|
+
import { generateObject as generateObjectFunction, ObjectRequest } from "../controller/ObjectController";
|
|
10
|
+
|
|
11
|
+
interface RimoriClientOptions {
|
|
12
|
+
pluginController: PluginController;
|
|
13
|
+
supabase: SupabaseClient;
|
|
14
|
+
tablePrefix: string;
|
|
15
|
+
pluginId: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class RimoriClient {
|
|
19
|
+
private static instance: RimoriClient;
|
|
20
|
+
private superbase: SupabaseClient;
|
|
21
|
+
private plugin: PluginController;
|
|
22
|
+
public functions: SupabaseClient["functions"];
|
|
23
|
+
public storage: SupabaseClient["storage"];
|
|
24
|
+
public pluginId: string;
|
|
25
|
+
public tablePrefix: string;
|
|
26
|
+
private settingsController: SettingsController;
|
|
27
|
+
private sharedContentController: SharedContentController;
|
|
28
|
+
|
|
29
|
+
private constructor(options: RimoriClientOptions) {
|
|
30
|
+
this.superbase = options.supabase;
|
|
31
|
+
this.pluginId = options.pluginId;
|
|
32
|
+
this.plugin = options.pluginController;
|
|
33
|
+
this.tablePrefix = options.tablePrefix;
|
|
34
|
+
this.storage = this.superbase.storage;
|
|
35
|
+
this.functions = this.superbase.functions;
|
|
36
|
+
this.settingsController = new SettingsController(options.supabase, options.pluginId);
|
|
37
|
+
this.sharedContentController = new SharedContentController(this);
|
|
38
|
+
this.rpc = this.rpc.bind(this);
|
|
39
|
+
this.from = this.from.bind(this);
|
|
40
|
+
this.emit = this.emit.bind(this);
|
|
41
|
+
this.request = this.request.bind(this);
|
|
42
|
+
this.subscribe = this.subscribe.bind(this);
|
|
43
|
+
this.getSettings = this.getSettings.bind(this);
|
|
44
|
+
this.setSettings = this.setSettings.bind(this);
|
|
45
|
+
this.getAIResponse = this.getAIResponse.bind(this);
|
|
46
|
+
this.generateObject = this.generateObject.bind(this);
|
|
47
|
+
this.getVoiceResponse = this.getVoiceResponse.bind(this);
|
|
48
|
+
this.getAIResponseStream = this.getAIResponseStream.bind(this);
|
|
49
|
+
this.getVoiceToTextResponse = this.getVoiceToTextResponse.bind(this);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public static async getInstance(pluginController: PluginController): Promise<RimoriClient> {
|
|
53
|
+
if (!RimoriClient.instance) {
|
|
54
|
+
const { supabase, tablePrefix, pluginId } = await pluginController.getClient();
|
|
55
|
+
RimoriClient.instance = new RimoriClient({ pluginController, supabase, tablePrefix, pluginId });
|
|
56
|
+
}
|
|
57
|
+
return RimoriClient.instance;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public from<
|
|
61
|
+
TableName extends string & keyof GenericSchema['Tables'],
|
|
62
|
+
Table extends GenericSchema['Tables'][TableName]
|
|
63
|
+
>(relation: TableName): PostgrestQueryBuilder<GenericSchema, Table, TableName>
|
|
64
|
+
public from<
|
|
65
|
+
ViewName extends string & keyof GenericSchema['Views'],
|
|
66
|
+
View extends GenericSchema['Views'][ViewName]
|
|
67
|
+
>(relation: ViewName): PostgrestQueryBuilder<GenericSchema, View, ViewName>
|
|
68
|
+
public from(relation: string): PostgrestQueryBuilder<GenericSchema, any, any> {
|
|
69
|
+
return this.superbase.from(this.getTableName(relation));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Perform a function call.
|
|
74
|
+
*
|
|
75
|
+
* @param functionName - The function name to call
|
|
76
|
+
* @param args - The arguments to pass to the function call
|
|
77
|
+
* @param options - Named parameters
|
|
78
|
+
* @param options.head - When set to `true`, `data` will not be returned.
|
|
79
|
+
* Useful if you only need the count.
|
|
80
|
+
* @param options.get - When set to `true`, the function will be called with
|
|
81
|
+
* read-only access mode.
|
|
82
|
+
* @param options.count - Count algorithm to use to count rows returned by the
|
|
83
|
+
* function. Only applicable for [set-returning
|
|
84
|
+
* functions](https://www.postgresql.org/docs/current/functions-srf.html).
|
|
85
|
+
*
|
|
86
|
+
* `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the
|
|
87
|
+
* hood.
|
|
88
|
+
*
|
|
89
|
+
* `"planned"`: Approximated but fast count algorithm. Uses the Postgres
|
|
90
|
+
* statistics under the hood.
|
|
91
|
+
*
|
|
92
|
+
* `"estimated"`: Uses exact count for low numbers and planned count for high
|
|
93
|
+
* numbers.
|
|
94
|
+
*/
|
|
95
|
+
rpc<Fn extends GenericSchema['Functions'][string], FnName extends string & keyof GenericSchema['Functions']>(
|
|
96
|
+
functionName: FnName,
|
|
97
|
+
args: Fn['Args'] = {},
|
|
98
|
+
options: {
|
|
99
|
+
head?: boolean
|
|
100
|
+
get?: boolean
|
|
101
|
+
count?: 'exact' | 'planned' | 'estimated'
|
|
102
|
+
} = {}
|
|
103
|
+
): PostgrestFilterBuilder<
|
|
104
|
+
GenericSchema,
|
|
105
|
+
Fn['Returns'] extends any[]
|
|
106
|
+
? Fn['Returns'][number] extends Record<string, unknown>
|
|
107
|
+
? Fn['Returns'][number]
|
|
108
|
+
: never
|
|
109
|
+
: never,
|
|
110
|
+
Fn['Returns'],
|
|
111
|
+
string,
|
|
112
|
+
null
|
|
113
|
+
> {
|
|
114
|
+
return this.superbase.rpc(this.getTableName(functionName), args, options)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private getTableName(type: string) {
|
|
118
|
+
return this.tablePrefix + "_" + type;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public subscribe(eventName: string, callback: (_id: number, data: any) => void) {
|
|
122
|
+
this.plugin.subscribe(eventName, callback);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public request<T>(eventName: string, data?: any): Promise<T> {
|
|
126
|
+
return this.plugin.request(eventName, data);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public emit(eventName: string, data: any) {
|
|
130
|
+
this.plugin.emit(eventName, data);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get the settings for the plugin. T can be any type of settings, UserSettings or SystemSettings.
|
|
135
|
+
* @param defaultSettings The default settings to use if no settings are found.
|
|
136
|
+
* @param genericSettings The type of settings to get.
|
|
137
|
+
* @returns The settings for the plugin.
|
|
138
|
+
*/
|
|
139
|
+
public async getSettings<T extends object>(defaultSettings: T, genericSettings?: "user" | "system"): Promise<T> {
|
|
140
|
+
return this.settingsController.getSettings<T>(defaultSettings, genericSettings);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public async setSettings(settings: any, genericSettings?: "user" | "system") {
|
|
144
|
+
await this.settingsController.setSettings(settings, genericSettings);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public async getAIResponse(messages: Message[], tools?: Tool[]): Promise<string> {
|
|
148
|
+
const token = await this.plugin.getToken();
|
|
149
|
+
return generateText(messages, tools || [], token).then(response => response.messages[0].content[0].text);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public async getAIResponseStream(messages: Message[], onMessage: OnLLMResponse, tools?: Tool[]) {
|
|
153
|
+
const token = await this.plugin.getToken();
|
|
154
|
+
streamChatGPT(messages, tools || [], onMessage, token);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public async getVoiceResponse(text: string, voice = "alloy", speed = 1, language?: string): Promise<Blob> {
|
|
158
|
+
return getTTSResponse(
|
|
159
|
+
this.plugin.getSupabaseUrl(),
|
|
160
|
+
{ input: text, voice, speed, language },
|
|
161
|
+
await this.plugin.getToken()
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public getVoiceToTextResponse(file: Blob): Promise<string> {
|
|
166
|
+
return getSTTResponse(this.superbase, file);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
public async generateObject(request: ObjectRequest): Promise<any> {
|
|
170
|
+
const token = await this.plugin.getToken();
|
|
171
|
+
return generateObjectFunction(request, token);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Fetch new shared content.
|
|
176
|
+
* @param type The type of shared content to fetch. E.g. assignments, exercises, etc.
|
|
177
|
+
* @param generatorInstructions The instructions for the generator.
|
|
178
|
+
* @param filter The filter for the shared content.
|
|
179
|
+
* @returns The new shared content.
|
|
180
|
+
*/
|
|
181
|
+
public async fetchNewSharedContent<T, R = T & BasicAssignment>(
|
|
182
|
+
type: string,
|
|
183
|
+
generatorInstructions: (reservedTopics: string[]) => Promise<ObjectRequest> | ObjectRequest,
|
|
184
|
+
filter?: { column: string, value: string | number | boolean },
|
|
185
|
+
): Promise<R[]> {
|
|
186
|
+
return this.sharedContentController.fetchNewSharedContent(type, generatorInstructions, filter);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get a shared content item by id.
|
|
191
|
+
* @param type The type of shared content to get. E.g. assignments, exercises, etc.
|
|
192
|
+
* @param id The id of the shared content item.
|
|
193
|
+
* @returns The shared content item.
|
|
194
|
+
*/
|
|
195
|
+
public async getSharedContent<T extends BasicAssignment>(type: string, id: string): Promise<T> {
|
|
196
|
+
return this.sharedContentController.getSharedContent(type, id);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Complete a shared content item.
|
|
201
|
+
* @param type The type of shared content to complete. E.g. assignments, exercises, etc.
|
|
202
|
+
* @param assignmentId The id of the shared content item to complete.
|
|
203
|
+
*/
|
|
204
|
+
public async completeSharedContent(type: string, assignmentId: string) {
|
|
205
|
+
return this.sharedContentController.completeSharedContent(type, assignmentId);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function setTheme() {
|
|
2
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
3
|
+
|
|
4
|
+
let theme = urlParams.get('theme');
|
|
5
|
+
const isSidebar = urlParams.get('applicationMode') === "sidebar";
|
|
6
|
+
|
|
7
|
+
if (!theme || theme === 'system') {
|
|
8
|
+
theme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
document.documentElement.classList.add("dark:text-gray-200", "bg-white");
|
|
12
|
+
|
|
13
|
+
if (theme === 'dark') {
|
|
14
|
+
document.documentElement.classList.add('dark', isSidebar ? "bg-gray-920" : "bg-gray-950");
|
|
15
|
+
document.documentElement.style.background = isSidebar ? "rgb(6, 12, 30)" : "rgb(3, 7, 18)";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
type Listener<T = any> = (event: T) => void;
|
|
2
|
+
|
|
3
|
+
export class EventEmitter {
|
|
4
|
+
private events: Map<string, Listener[]> = new Map();
|
|
5
|
+
|
|
6
|
+
constructor() {
|
|
7
|
+
this.on = this.on.bind(this);
|
|
8
|
+
this.once = this.once.bind(this);
|
|
9
|
+
this.emit = this.emit.bind(this);
|
|
10
|
+
this.removeListener = this.removeListener.bind(this);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Subscribe to an event
|
|
14
|
+
on<T = any>(eventName: string, listener: Listener<T>): void {
|
|
15
|
+
if (!this.events.has(eventName)) {
|
|
16
|
+
this.events.set(eventName, []);
|
|
17
|
+
}
|
|
18
|
+
this.events.get(eventName)!.push(listener);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Subscribe to an event for a single invocation
|
|
22
|
+
once<T = any>(eventName: string, listener: Listener<T>): void {
|
|
23
|
+
const onceWrapper: Listener<T> = (event) => {
|
|
24
|
+
this.removeListener(eventName, onceWrapper);
|
|
25
|
+
listener(event);
|
|
26
|
+
};
|
|
27
|
+
this.on(eventName, onceWrapper);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Remove a specific listener
|
|
31
|
+
removeListener<T = any>(eventName: string, listener: Listener<T>): void {
|
|
32
|
+
const listeners = this.events.get(eventName);
|
|
33
|
+
if (!listeners) return;
|
|
34
|
+
|
|
35
|
+
this.events.set(eventName, listeners.filter((l) => l !== listener));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Emit an event
|
|
39
|
+
emit<T = any>(eventName: string, data?: T): void {
|
|
40
|
+
const listeners = this.events.get(eventName);
|
|
41
|
+
console.log("emit", eventName, data, listeners);
|
|
42
|
+
if (!listeners) return;
|
|
43
|
+
|
|
44
|
+
listeners.forEach((listener) => listener(data));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const emitter = new EventEmitter();
|
|
48
|
+
export const EmitterSingleton = emitter;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext, useRef } from "react";
|
|
4
|
+
import { EmitterSingleton, EventEmitter } from "./EventEmitter";
|
|
5
|
+
|
|
6
|
+
// Create the Context
|
|
7
|
+
const EventEmitterContext = createContext<EventEmitter | null>(null);
|
|
8
|
+
|
|
9
|
+
// Provider Component
|
|
10
|
+
export const EventEmitterProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
11
|
+
const eventEmitterRef = useRef(EmitterSingleton);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<EventEmitterContext.Provider value={eventEmitterRef.current}>
|
|
15
|
+
{children}
|
|
16
|
+
</EventEmitterContext.Provider>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Hook to use the EventEmitter
|
|
21
|
+
export const useEventEmitter = (): EventEmitter => {
|
|
22
|
+
const context = useContext(EventEmitterContext);
|
|
23
|
+
if (!context) {
|
|
24
|
+
throw new Error("useEventEmitter must be used within an EventEmitterProvider");
|
|
25
|
+
}
|
|
26
|
+
return context;
|
|
27
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React, { createContext, useContext, ReactNode, useEffect, useState } from 'react';
|
|
2
|
+
import { PluginController } from '../plugin/PluginController';
|
|
3
|
+
import { RimoriClient } from '../plugin/RimoriClient';
|
|
4
|
+
interface PluginProviderProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const PluginContext = createContext<RimoriClient | null>(null);
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export const PluginProvider: React.FC<PluginProviderProps> = ({ children }) => {
|
|
12
|
+
const [plugin, setPlugin] = useState<RimoriClient | null>(null);
|
|
13
|
+
//route change
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
let lastHash = window.location.hash;
|
|
16
|
+
|
|
17
|
+
setInterval(() => {
|
|
18
|
+
if (lastHash !== window.location.hash) {
|
|
19
|
+
lastHash = window.location.hash;
|
|
20
|
+
console.log('url changed:', lastHash);
|
|
21
|
+
plugin?.emit('urlChange', window.location.hash);
|
|
22
|
+
}
|
|
23
|
+
}, 100);
|
|
24
|
+
PluginController.getInstance().then(setPlugin);
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
//context menu
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
let isOpen = false;
|
|
30
|
+
const handleContextMenu = (e: MouseEvent) => {
|
|
31
|
+
const selection = window.getSelection()?.toString().trim();
|
|
32
|
+
if (selection) {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
// console.log('context menu', selection);
|
|
35
|
+
plugin?.emit('contextMenu', { text: selection, x: e.clientX, y: e.clientY, open: true });
|
|
36
|
+
isOpen = true;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Hide the menu on click outside
|
|
41
|
+
const handleClick = () => isOpen && plugin?.emit('contextMenu', { text: '', x: 0, y: 0, open: false });
|
|
42
|
+
|
|
43
|
+
document.addEventListener("click", handleClick);
|
|
44
|
+
document.addEventListener('contextmenu', handleContextMenu);
|
|
45
|
+
return () => {
|
|
46
|
+
document.removeEventListener("click", handleClick);
|
|
47
|
+
document.removeEventListener('contextmenu', handleContextMenu);
|
|
48
|
+
};
|
|
49
|
+
}, [plugin]);
|
|
50
|
+
|
|
51
|
+
if(!plugin){
|
|
52
|
+
return ""
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<PluginContext.Provider value={plugin}>
|
|
57
|
+
{children}
|
|
58
|
+
</PluginContext.Provider>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const usePlugin = () => {
|
|
63
|
+
const context = useContext(PluginContext);
|
|
64
|
+
if (context === null) {
|
|
65
|
+
throw new Error('usePlugin must be used within an PluginProvider');
|
|
66
|
+
}
|
|
67
|
+
return context;
|
|
68
|
+
};
|
package/src/style.scss
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
dialog::backdrop {
|
|
2
|
+
backdrop-filter: blur(2px);
|
|
3
|
+
// background: rgb(255, 255, 255, 0.5);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.dark * dialog::backdrop {
|
|
7
|
+
background: transparent;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.tiptap {
|
|
11
|
+
padding-top: 5px;
|
|
12
|
+
padding-left: 7px;
|
|
13
|
+
/* min-height: 300px; */
|
|
14
|
+
|
|
15
|
+
&:focus-visible {
|
|
16
|
+
outline: none;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
h1,
|
|
20
|
+
h2,
|
|
21
|
+
h3,
|
|
22
|
+
h4,
|
|
23
|
+
h5,
|
|
24
|
+
h6 {
|
|
25
|
+
@apply font-bold;
|
|
26
|
+
margin-bottom: 1rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
h1 {
|
|
30
|
+
@apply text-4xl;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
h2 {
|
|
34
|
+
@apply text-3xl;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
h3 {
|
|
38
|
+
@apply text-2xl;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
h4 {
|
|
42
|
+
@apply text-xl;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
h5 {
|
|
46
|
+
@apply text-lg;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
h6 {
|
|
50
|
+
@apply text-base;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
p {
|
|
54
|
+
@apply mb-4;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
a {
|
|
58
|
+
@apply text-blue-600 hover:text-blue-800;
|
|
59
|
+
text-decoration: none;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
a:hover {
|
|
63
|
+
@apply underline;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
ul {
|
|
67
|
+
@apply list-disc pl-8;
|
|
68
|
+
|
|
69
|
+
li>p {
|
|
70
|
+
@apply mb-1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
ol {
|
|
75
|
+
@apply list-decimal pl-7;
|
|
76
|
+
|
|
77
|
+
li>p {
|
|
78
|
+
@apply mb-1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
blockquote {
|
|
83
|
+
@apply border-l-4 pl-4 italic text-gray-600 my-4;
|
|
84
|
+
border-color: #ccc;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
code {
|
|
88
|
+
font-family: monospace;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pre {
|
|
92
|
+
@apply bg-gray-800 text-gray-500 p-4 rounded-lg overflow-x-auto;
|
|
93
|
+
font-family: monospace;
|
|
94
|
+
white-space: pre-wrap;
|
|
95
|
+
word-wrap: break-word;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
img {
|
|
99
|
+
@apply max-w-full h-auto rounded-lg my-4;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
table {
|
|
103
|
+
@apply table-auto w-full border-collapse mb-4;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
th,
|
|
107
|
+
td {
|
|
108
|
+
@apply border px-4 py-2 text-left;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
th {
|
|
112
|
+
@apply bg-gray-500 font-semibold;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
tr:nth-child(even) {
|
|
116
|
+
@apply bg-gray-400;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@media (max-width: 768px) {
|
|
120
|
+
h1 {
|
|
121
|
+
@apply text-3xl;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
h2 {
|
|
125
|
+
@apply text-2xl;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
p {
|
|
129
|
+
@apply text-base;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
img {
|
|
133
|
+
@apply max-w-full;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function isFullscreen() {
|
|
2
|
+
return !!document.fullscreenElement;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function triggerFullscreen(onStateChange: (isFullscreen: boolean) => void, selector?: string) {
|
|
6
|
+
document.addEventListener("fullscreenchange", () => {
|
|
7
|
+
onStateChange(isFullscreen());
|
|
8
|
+
});
|
|
9
|
+
try {
|
|
10
|
+
const ref = document.querySelector(selector || "#root")!
|
|
11
|
+
if (!isFullscreen()) {
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
ref.requestFullscreen() || ref.webkitRequestFullscreen()
|
|
14
|
+
} else {
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
document.exitFullscreen() || document.webkitExitFullscreen()
|
|
17
|
+
}
|
|
18
|
+
} catch (error: any) {
|
|
19
|
+
console.error("Failed to enter fullscreen", error.message);
|
|
20
|
+
}
|
|
21
|
+
onStateChange(isFullscreen());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|