@rimori/client 1.0.5 → 1.1.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 +955 -28
- package/dist/components/MarkdownEditor.js +6 -4
- package/dist/components/PluginController.d.ts +21 -0
- package/dist/components/PluginController.js +116 -0
- package/dist/components/ai/Assistant.js +1 -1
- package/dist/components/ai/Avatar.d.ts +6 -4
- package/dist/components/ai/Avatar.js +14 -6
- package/dist/components/ai/EmbeddedAssistent/AudioInputField.js +1 -1
- package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.d.ts +2 -1
- package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.js +36 -15
- package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.d.ts +1 -0
- package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.js +3 -0
- package/dist/components/ai/EmbeddedAssistent/TTS/Player.d.ts +2 -0
- package/dist/components/ai/EmbeddedAssistent/TTS/Player.js +5 -0
- package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.d.ts +3 -0
- package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.js +41 -5
- package/dist/components/ai/utils.d.ts +1 -1
- package/dist/components.d.ts +1 -0
- package/dist/components.js +1 -0
- package/dist/controller/AIController.js +2 -1
- package/dist/controller/SettingsController.d.ts +15 -15
- package/dist/controller/SettingsController.js +15 -16
- package/dist/controller/SharedContentController.d.ts +58 -11
- package/dist/controller/SharedContentController.js +161 -26
- package/dist/controller/SidePluginController.d.ts +1 -12
- package/dist/controller/SidePluginController.js +2 -1
- package/dist/core/components/ContextMenu.d.ts +10 -0
- package/dist/core/components/ContextMenu.js +93 -0
- package/dist/core.d.ts +1 -4
- package/dist/core.js +1 -4
- package/dist/hooks/UseChatHook.d.ts +1 -1
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -8
- package/dist/plugin/AccomplishmentHandler.d.ts +38 -0
- package/dist/plugin/AccomplishmentHandler.js +108 -0
- package/dist/plugin/ContextMenu.d.ts +17 -0
- package/dist/plugin/ContextMenu.js +45 -0
- package/dist/plugin/PluginController.js +6 -3
- package/dist/plugin/RimoriClient.d.ts +92 -65
- package/dist/plugin/RimoriClient.js +105 -75
- package/dist/plugin/ThemeSetter.js +2 -2
- package/dist/plugin/fromRimori/EventBus.d.ts +6 -3
- package/dist/plugin/fromRimori/EventBus.js +15 -9
- package/dist/plugin/fromRimori/PluginTypes.d.ts +48 -0
- package/dist/plugin/fromRimori/PluginTypes.js +1 -0
- package/dist/providers/PluginController.d.ts +21 -0
- package/dist/providers/PluginController.js +116 -0
- package/dist/providers/PluginProvider.js +26 -73
- package/dist/types/Actions.d.ts +4 -0
- package/dist/types/Actions.js +1 -0
- package/dist/utils/Language.d.ts +66 -0
- package/dist/utils/Language.js +67 -0
- package/dist/utils/difficultyConverter.d.ts +1 -0
- package/dist/utils/difficultyConverter.js +3 -0
- package/dist/worker/WorkerSetup.js +4 -4
- package/package.json +2 -3
- package/src/components/MarkdownEditor.tsx +78 -76
- package/src/components/ai/Assistant.tsx +1 -1
- package/src/components/ai/Avatar.tsx +66 -49
- package/src/components/ai/EmbeddedAssistent/AudioInputField.tsx +1 -1
- package/src/components/ai/EmbeddedAssistent/CircleAudioAvatar.tsx +82 -59
- package/src/components/ai/EmbeddedAssistent/TTS/MessageSender.ts +4 -0
- package/src/components/ai/EmbeddedAssistent/TTS/Player.ts +6 -0
- package/src/components/ai/EmbeddedAssistent/VoiceRecoder.tsx +51 -8
- package/src/components/ai/utils.ts +1 -1
- package/src/components.ts +2 -1
- package/src/controller/AIController.ts +2 -1
- package/src/controller/SettingsController.ts +83 -84
- package/src/controller/SharedContentController.ts +214 -53
- package/src/controller/SidePluginController.ts +3 -14
- package/src/core/components/ContextMenu.tsx +123 -0
- package/src/core.ts +1 -4
- package/src/hooks/UseChatHook.ts +17 -17
- package/src/index.ts +0 -8
- package/src/plugin/AccomplishmentHandler.ts +165 -0
- package/src/plugin/PluginController.ts +105 -103
- package/src/plugin/RimoriClient.ts +267 -250
- package/src/plugin/ThemeSetter.ts +2 -2
- package/src/plugin/fromRimori/EventBus.ts +23 -12
- package/src/plugin/fromRimori/PluginTypes.ts +64 -0
- package/src/providers/PluginProvider.tsx +63 -110
- package/src/types/Actions.ts +6 -0
- package/src/utils/Language.ts +70 -0
- package/src/utils/difficultyConverter.ts +4 -0
- package/src/worker/WorkerSetup.ts +4 -4
- package/dist/components/avatar/Assistant.d.ts +0 -9
- package/dist/components/avatar/Assistant.js +0 -59
- package/dist/components/avatar/Avatar.d.ts +0 -12
- package/dist/components/avatar/Avatar.js +0 -42
- package/dist/components/avatar/EmbeddedAssistent/AudioInputField.d.ts +0 -7
- package/dist/components/avatar/EmbeddedAssistent/AudioInputField.js +0 -38
- package/dist/components/avatar/EmbeddedAssistent/CircleAudioAvatar.d.ts +0 -7
- package/dist/components/avatar/EmbeddedAssistent/CircleAudioAvatar.js +0 -59
- package/dist/components/avatar/EmbeddedAssistent/TTS/MessageSender.d.ts +0 -19
- package/dist/components/avatar/EmbeddedAssistent/TTS/MessageSender.js +0 -84
- package/dist/components/avatar/EmbeddedAssistent/TTS/Player.d.ts +0 -25
- package/dist/components/avatar/EmbeddedAssistent/TTS/Player.js +0 -180
- package/dist/components/avatar/EmbeddedAssistent/VoiceRecoder.d.ts +0 -7
- package/dist/components/avatar/EmbeddedAssistent/VoiceRecoder.js +0 -45
- package/dist/components/avatar/utils.d.ts +0 -6
- package/dist/components/avatar/utils.js +0 -14
|
@@ -69,7 +69,8 @@ export async function streamChatGPT(supabaseUrl: string, messages: Message[], to
|
|
|
69
69
|
// console.log("AI response:", content);
|
|
70
70
|
|
|
71
71
|
//content \n\n should be real line break when message is displayed
|
|
72
|
-
|
|
72
|
+
//content \"\" should be real double quote when message is displayed
|
|
73
|
+
onResponse(messageId, content.replace(/\\n/g, '\n').replace(/\\+"|"\\+/g, '"'), false);
|
|
73
74
|
} else if (command === 'd') {
|
|
74
75
|
// console.log("AI usage:", JSON.parse(line.substring(2)));
|
|
75
76
|
done = true;
|
|
@@ -1,102 +1,101 @@
|
|
|
1
1
|
import { SupabaseClient } from "@supabase/supabase-js";
|
|
2
2
|
import { LanguageLevel } from "../utils/difficultyConverter";
|
|
3
|
+
import { Language } from "../utils/Language";
|
|
3
4
|
|
|
4
5
|
export interface UserInfo {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
6
|
+
skill_level_reading: LanguageLevel;
|
|
7
|
+
skill_level_writing: LanguageLevel;
|
|
8
|
+
skill_level_grammar: LanguageLevel;
|
|
9
|
+
skill_level_speaking: LanguageLevel;
|
|
10
|
+
skill_level_listening: LanguageLevel;
|
|
11
|
+
skill_level_understanding: LanguageLevel;
|
|
12
|
+
goal_longterm: string;
|
|
13
|
+
goal_weekly: string;
|
|
14
|
+
study_buddy: string;
|
|
15
|
+
story_genre: string;
|
|
16
|
+
study_duration: number;
|
|
17
|
+
mother_tongue: Language;
|
|
18
|
+
motivation_type: string;
|
|
19
|
+
onboarding_completed: boolean;
|
|
20
|
+
context_menu_on_select: boolean;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export class SettingsController {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
constructor(supabase: SupabaseClient, pluginId: string) {
|
|
28
|
-
this.supabase = supabase;
|
|
29
|
-
this.pluginId = pluginId;
|
|
30
|
-
}
|
|
24
|
+
private pluginId: string;
|
|
25
|
+
private supabase: SupabaseClient;
|
|
31
26
|
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
constructor(supabase: SupabaseClient, pluginId: string) {
|
|
28
|
+
this.supabase = supabase;
|
|
29
|
+
this.pluginId = pluginId;
|
|
30
|
+
}
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
32
|
+
private async fetchSettings(): Promise<any | null> {
|
|
33
|
+
const { data } = await this.supabase.from("plugin_settings").select("*").eq("plugin_id", this.pluginId)
|
|
38
34
|
|
|
39
|
-
|
|
35
|
+
if (!data || data.length === 0) {
|
|
36
|
+
return null;
|
|
40
37
|
}
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
return data[0].settings;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public async setSettings(settings: any): Promise<void> {
|
|
43
|
+
await this.supabase.from("plugin_settings").upsert({ plugin_id: this.pluginId, settings });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public async getUserInfo(): Promise<UserInfo> {
|
|
47
|
+
const { data } = await this.supabase.from("profiles").select("*");
|
|
48
|
+
|
|
49
|
+
if (!data || data.length === 0) {
|
|
50
|
+
return {
|
|
51
|
+
mother_tongue: "en",
|
|
52
|
+
skill_level_listening: "Pre-A1",
|
|
53
|
+
skill_level_reading: "Pre-A1",
|
|
54
|
+
skill_level_speaking: "Pre-A1",
|
|
55
|
+
skill_level_writing: "Pre-A1",
|
|
56
|
+
skill_level_understanding: "Pre-A1",
|
|
57
|
+
skill_level_grammar: "Pre-A1",
|
|
58
|
+
goal_longterm: "",
|
|
59
|
+
goal_weekly: "",
|
|
60
|
+
study_buddy: "clarence",
|
|
61
|
+
story_genre: "adventure",
|
|
62
|
+
study_duration: 30,
|
|
63
|
+
motivation_type: "self-motivated",
|
|
64
|
+
onboarding_completed: false,
|
|
65
|
+
context_menu_on_select: false,
|
|
66
|
+
}
|
|
44
67
|
}
|
|
45
68
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
motivation_type: "self-motivated",
|
|
61
|
-
study_buddy: "clarence",
|
|
62
|
-
preferred_genre: "adventure",
|
|
63
|
-
milestone: "",
|
|
64
|
-
settings: {
|
|
65
|
-
contextMenuOnSelect: false,
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return data[0];
|
|
69
|
+
return data[0].settings;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get the settings for the plugin. T can be any type of settings, UserSettings or SystemSettings.
|
|
74
|
+
* @param defaultSettings The default settings to use if no settings are found.
|
|
75
|
+
* @returns The settings for the plugin.
|
|
76
|
+
*/
|
|
77
|
+
public async getSettings<T extends object>(defaultSettings: T): Promise<T> {
|
|
78
|
+
const storedSettings = await this.fetchSettings() as T | null;
|
|
79
|
+
|
|
80
|
+
if (!storedSettings) {
|
|
81
|
+
await this.setSettings(defaultSettings);
|
|
82
|
+
return defaultSettings;
|
|
71
83
|
}
|
|
72
84
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// Handle settings migration
|
|
87
|
-
const storedKeys = Object.keys(storedSettings);
|
|
88
|
-
const defaultKeys = Object.keys(defaultSettings);
|
|
89
|
-
|
|
90
|
-
if (storedKeys.length !== defaultKeys.length) {
|
|
91
|
-
const validStoredSettings = Object.fromEntries(
|
|
92
|
-
Object.entries(storedSettings).filter(([key]) => defaultKeys.includes(key))
|
|
93
|
-
);
|
|
94
|
-
const mergedSettings = { ...defaultSettings, ...validStoredSettings } as T;
|
|
95
|
-
|
|
96
|
-
await this.setSettings(mergedSettings);
|
|
97
|
-
return mergedSettings;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return storedSettings;
|
|
85
|
+
// Handle settings migration
|
|
86
|
+
const storedKeys = Object.keys(storedSettings);
|
|
87
|
+
const defaultKeys = Object.keys(defaultSettings);
|
|
88
|
+
|
|
89
|
+
if (storedKeys.length !== defaultKeys.length) {
|
|
90
|
+
const validStoredSettings = Object.fromEntries(
|
|
91
|
+
Object.entries(storedSettings).filter(([key]) => defaultKeys.includes(key))
|
|
92
|
+
);
|
|
93
|
+
const mergedSettings = { ...defaultSettings, ...validStoredSettings } as T;
|
|
94
|
+
|
|
95
|
+
await this.setSettings(mergedSettings);
|
|
96
|
+
return mergedSettings;
|
|
101
97
|
}
|
|
98
|
+
|
|
99
|
+
return storedSettings;
|
|
100
|
+
}
|
|
102
101
|
}
|
|
@@ -1,71 +1,232 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ObjectRequest } from "./ObjectController";
|
|
1
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
3
2
|
import { RimoriClient } from "../plugin/RimoriClient";
|
|
3
|
+
import { ObjectRequest } from "./ObjectController";
|
|
4
|
+
|
|
5
|
+
export interface BasicAssignment<T> {
|
|
6
|
+
id: string;
|
|
7
|
+
createdAt: Date;
|
|
8
|
+
topic: string;
|
|
9
|
+
createdBy: string;
|
|
10
|
+
verified: boolean;
|
|
11
|
+
keywords: any;
|
|
12
|
+
data: T;
|
|
13
|
+
}
|
|
4
14
|
|
|
5
|
-
export interface
|
|
6
|
-
|
|
7
|
-
createdAt: Date;
|
|
8
|
-
topic: string;
|
|
9
|
-
createdBy: string;
|
|
10
|
-
verified: boolean;
|
|
11
|
-
keywords: any;
|
|
15
|
+
export interface SharedContentObjectRequest extends ObjectRequest {
|
|
16
|
+
fixedProperties?: Record<string, string | number | boolean>
|
|
12
17
|
}
|
|
13
18
|
|
|
19
|
+
export type SharedContentFilter = Record<string, string | number | boolean>
|
|
20
|
+
|
|
14
21
|
export class SharedContentController {
|
|
15
|
-
|
|
22
|
+
private supabase: SupabaseClient;
|
|
23
|
+
private rimoriClient: RimoriClient;
|
|
24
|
+
|
|
25
|
+
constructor(supabase: SupabaseClient, rimoriClient: RimoriClient) {
|
|
26
|
+
this.supabase = supabase;
|
|
27
|
+
this.rimoriClient = rimoriClient;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Fetch new shared content for a given content type.
|
|
32
|
+
* @param contentType - The type of content to fetch.
|
|
33
|
+
* @param generatorInstructions - The instructions for the generator. The object needs to have a tool property with a topic and keywords property to let a new unique topic be generated.
|
|
34
|
+
* @param filter - An optional filter to apply to the query.
|
|
35
|
+
* @param privateTopic - An optional flag to indicate if the topic should be private and only be visible to the user.
|
|
36
|
+
* @returns The new shared content.
|
|
37
|
+
*/
|
|
38
|
+
public async getNewSharedContent<T>(
|
|
39
|
+
contentType: string,
|
|
40
|
+
generatorInstructions: SharedContentObjectRequest,
|
|
41
|
+
//this filter is there if the content should be filtered additionally by a column and value
|
|
42
|
+
filter?: SharedContentFilter,
|
|
43
|
+
privateTopic?: boolean,
|
|
44
|
+
): Promise<BasicAssignment<T>> {
|
|
45
|
+
const query = this.supabase.from("shared_content")
|
|
46
|
+
.select("*, scc:shared_content_completed(id)")
|
|
47
|
+
.eq('content_type', contentType)
|
|
48
|
+
.is('scc.id', null)
|
|
49
|
+
.limit(10);
|
|
50
|
+
|
|
51
|
+
if (filter) {
|
|
52
|
+
query.contains('data', filter);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { data: newAssignments, error } = await query;
|
|
56
|
+
|
|
57
|
+
if (error) {
|
|
58
|
+
console.error('error fetching new assignments:', error);
|
|
59
|
+
throw new Error('error fetching new assignments');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log('newAssignments:', newAssignments);
|
|
63
|
+
|
|
64
|
+
if (newAssignments.length > 0) {
|
|
65
|
+
const index = Math.floor(Math.random() * newAssignments.length);
|
|
66
|
+
return newAssignments[index];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// generate new assignments
|
|
70
|
+
const fullInstructions = await this.getGeneratorInstructions(contentType, generatorInstructions, filter);
|
|
71
|
+
|
|
72
|
+
console.log('fullInstructions:', fullInstructions);
|
|
73
|
+
|
|
74
|
+
const instructions = await this.rimoriClient.llm.getObject(fullInstructions);
|
|
75
|
+
|
|
76
|
+
console.log('instructions:', instructions);
|
|
77
|
+
|
|
78
|
+
const { data: newAssignment, error: insertError } = await this.supabase.from("shared_content").insert({
|
|
79
|
+
private: privateTopic,
|
|
80
|
+
content_type: contentType,
|
|
81
|
+
topic: instructions.topic,
|
|
82
|
+
keywords: instructions.keywords.map(({ text }: { text: string }) => text),
|
|
83
|
+
data: { ...instructions, topic: undefined, keywords: undefined, ...generatorInstructions.fixedProperties },
|
|
84
|
+
}).select();
|
|
85
|
+
|
|
86
|
+
if (insertError) {
|
|
87
|
+
console.error('error inserting new assignment:', insertError);
|
|
88
|
+
throw new Error('error inserting new assignment');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return newAssignment[0];
|
|
92
|
+
}
|
|
16
93
|
|
|
17
|
-
|
|
18
|
-
|
|
94
|
+
private async getGeneratorInstructions(contentType: string, generatorInstructions: ObjectRequest, filter?: SharedContentFilter): Promise<ObjectRequest> {
|
|
95
|
+
const completedTopics = await this.getCompletedTopics(contentType, filter);
|
|
96
|
+
|
|
97
|
+
generatorInstructions.instructions += `
|
|
98
|
+
The following topics are already taken: ${completedTopics.join(', ')}`;
|
|
99
|
+
|
|
100
|
+
generatorInstructions.tool.topic = {
|
|
101
|
+
type: "string",
|
|
102
|
+
description: "What the topic is about. Short. ",
|
|
103
|
+
}
|
|
104
|
+
generatorInstructions.tool.keywords = {
|
|
105
|
+
type: [{ text: { type: "string" } }],
|
|
106
|
+
description: "Keywords around the topic of the assignment.",
|
|
107
|
+
}
|
|
108
|
+
return generatorInstructions;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private async getCompletedTopics(contentType: string, filter?: SharedContentFilter): Promise<string[]> {
|
|
112
|
+
const query = this.supabase.from("shared_content")
|
|
113
|
+
.select("topic, keywords, scc:shared_content_completed(id)")
|
|
114
|
+
.eq('content_type', contentType)
|
|
115
|
+
.not('scc.id', 'is', null)
|
|
116
|
+
|
|
117
|
+
if (filter) {
|
|
118
|
+
query.contains('data', filter);
|
|
19
119
|
}
|
|
20
120
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const queryParameter = { filter_column: filter?.column || null, filter_value: filter?.value || null, unread: true }
|
|
27
|
-
const { data: newAssignments } = await this.rimoriClient.db.rpc(type + "_entries", queryParameter)
|
|
28
|
-
console.log('newAssignments:', newAssignments);
|
|
29
|
-
|
|
30
|
-
if ((newAssignments as any[]).length > 0) {
|
|
31
|
-
return newAssignments as R[];
|
|
32
|
-
}
|
|
33
|
-
// generate new assignments
|
|
34
|
-
const { data: oldAssignments } = await this.rimoriClient.db.rpc(type + "_entries", { ...queryParameter, unread: false })
|
|
35
|
-
console.log('oldAssignments:', oldAssignments);
|
|
36
|
-
const reservedTopics = this.getReservedTopics(oldAssignments as BasicAssignment[]);
|
|
37
|
-
|
|
38
|
-
const request = await generatorInstructions(reservedTopics);
|
|
39
|
-
if (!request.tool.keywords || !request.tool.topic) {
|
|
40
|
-
throw new Error("topic or keywords not found in the request schema");
|
|
41
|
-
}
|
|
42
|
-
const instructions = await this.rimoriClient.llm.getObject(request);
|
|
43
|
-
console.log('instructions:', instructions);
|
|
44
|
-
|
|
45
|
-
const preparedData = {
|
|
46
|
-
id: uuidv4(),
|
|
47
|
-
...instructions,
|
|
48
|
-
keywords: this.purifyStringArray(instructions.keywords),
|
|
49
|
-
};
|
|
50
|
-
return await this.rimoriClient.db.from(type).insert(preparedData).then(() => [preparedData] as R[]);
|
|
121
|
+
const { data: oldAssignments, error } = await query;
|
|
122
|
+
|
|
123
|
+
if (error) {
|
|
124
|
+
console.error('error fetching old assignments:', error);
|
|
125
|
+
return [];
|
|
51
126
|
}
|
|
127
|
+
return oldAssignments.map(({ topic, keywords }) => `${topic}(${keywords.join(',')})`);
|
|
128
|
+
}
|
|
52
129
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
130
|
+
public async getSharedContent<T>(contentType: string, id: string): Promise<BasicAssignment<T>> {
|
|
131
|
+
const { data, error } = await this.supabase.from("shared_content").select().eq('content_type', contentType).eq('id', id).single();
|
|
132
|
+
if (error) {
|
|
133
|
+
console.error('error fetching shared content:', error);
|
|
134
|
+
throw new Error('error fetching shared content');
|
|
58
135
|
}
|
|
136
|
+
return data;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public async completeSharedContent(contentType: string, assignmentId: string) {
|
|
140
|
+
await this.supabase.from("shared_content_completed").insert({ content_type: contentType, id: assignmentId });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Fetch shared content from the database based on optional filters.
|
|
145
|
+
* @param contentType - The type of content to fetch.
|
|
146
|
+
* @param filter - Optional filter to apply to the query.
|
|
147
|
+
* @param limit - Optional limit for the number of results.
|
|
148
|
+
* @returns Array of shared content matching the criteria.
|
|
149
|
+
*/
|
|
150
|
+
public async getSharedContentList<T>(contentType: string, filter?: SharedContentFilter, limit?: number): Promise<BasicAssignment<T>[]> {
|
|
151
|
+
const query = this.supabase.from("shared_content").select("*").eq('content_type', contentType).limit(limit ?? 30);
|
|
59
152
|
|
|
60
|
-
|
|
61
|
-
|
|
153
|
+
if (filter) {
|
|
154
|
+
query.contains('data', filter);
|
|
62
155
|
}
|
|
63
156
|
|
|
64
|
-
|
|
65
|
-
|
|
157
|
+
const { data, error } = await query;
|
|
158
|
+
|
|
159
|
+
if (error) {
|
|
160
|
+
console.error('error fetching shared content:', error);
|
|
161
|
+
throw new Error('error fetching shared content');
|
|
66
162
|
}
|
|
67
163
|
|
|
68
|
-
|
|
69
|
-
|
|
164
|
+
return data;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Insert new shared content into the database.
|
|
169
|
+
* @param param
|
|
170
|
+
* @param param.contentType - The type of content to insert.
|
|
171
|
+
* @param param.topic - The topic of the content.
|
|
172
|
+
* @param param.keywords - Keywords associated with the content.
|
|
173
|
+
* @param param.data - The content data to store.
|
|
174
|
+
* @param param.privateTopic - Optional flag to indicate if the topic should be private.
|
|
175
|
+
* @returns The inserted shared content.
|
|
176
|
+
* @throws {Error} if insertion fails.
|
|
177
|
+
*/
|
|
178
|
+
public async createSharedContent<T>({ contentType, topic, keywords, data, privateTopic }: SharedContent<T>): Promise<BasicAssignment<T>> {
|
|
179
|
+
const { data: newContent, error } = await this.supabase.from("shared_content").insert({
|
|
180
|
+
private: privateTopic,
|
|
181
|
+
content_type: contentType,
|
|
182
|
+
topic,
|
|
183
|
+
keywords,
|
|
184
|
+
data,
|
|
185
|
+
}).select();
|
|
186
|
+
|
|
187
|
+
if (error) {
|
|
188
|
+
console.error('error inserting shared content:', error);
|
|
189
|
+
throw new Error('error inserting shared content');
|
|
70
190
|
}
|
|
191
|
+
|
|
192
|
+
return newContent[0];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Update existing shared content in the database.
|
|
197
|
+
* @param id - The ID of the content to update.
|
|
198
|
+
* @param updates - The updates to apply to the shared content.
|
|
199
|
+
* @returns The updated shared content.
|
|
200
|
+
* @throws {Error} if update fails.
|
|
201
|
+
*/
|
|
202
|
+
public async updateSharedContent<T>(id: string, updates: Partial<SharedContent<T>>): Promise<BasicAssignment<T>> {
|
|
203
|
+
const updateData: any = {};
|
|
204
|
+
|
|
205
|
+
if (updates.contentType) updateData.content_type = updates.contentType;
|
|
206
|
+
if (updates.topic) updateData.topic = updates.topic;
|
|
207
|
+
if (updates.keywords) updateData.keywords = updates.keywords;
|
|
208
|
+
if (updates.data) updateData.data = updates.data;
|
|
209
|
+
if (updates.privateTopic !== undefined) updateData.private = updates.privateTopic;
|
|
210
|
+
|
|
211
|
+
const { data: updatedContent, error } = await this.supabase.from("shared_content").update(updateData).eq('id', id).select();
|
|
212
|
+
|
|
213
|
+
if (error) {
|
|
214
|
+
console.error('error updating shared content:', error);
|
|
215
|
+
throw new Error('error updating shared content');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!updatedContent || updatedContent.length === 0) {
|
|
219
|
+
throw new Error('shared content not found');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return updatedContent[0];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface SharedContent<T> {
|
|
227
|
+
contentType: string;
|
|
228
|
+
topic: string;
|
|
229
|
+
keywords: string[];
|
|
230
|
+
data: T;
|
|
231
|
+
privateTopic?: boolean;
|
|
71
232
|
}
|
|
@@ -1,17 +1,5 @@
|
|
|
1
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
|
-
}
|
|
2
|
+
import { Plugin } from '../plugin/fromRimori/PluginTypes';
|
|
15
3
|
|
|
16
4
|
export async function getPlugins(supabase: SupabaseClient): Promise<Plugin[]> {
|
|
17
5
|
let { data, error } = await supabase.from('plugins').select('*');
|
|
@@ -24,8 +12,9 @@ export async function getPlugins(supabase: SupabaseClient): Promise<Plugin[]> {
|
|
|
24
12
|
return (data || []).map((plugin: any) => ({
|
|
25
13
|
id: plugin.id,
|
|
26
14
|
title: plugin.title,
|
|
15
|
+
description: plugin.description,
|
|
27
16
|
icon_url: plugin.icon_url,
|
|
28
|
-
|
|
17
|
+
endpoint: plugin.endpoint,
|
|
29
18
|
context_menu_actions: plugin.context_menu_actions,
|
|
30
19
|
plugin_pages: plugin.plugin_pages,
|
|
31
20
|
sidebar_pages: plugin.sidebar_pages,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { EventBus } from "../../plugin/fromRimori/EventBus";
|
|
3
|
+
import { RimoriClient } from "../../plugin/RimoriClient";
|
|
4
|
+
import { MenuEntry } from "../../plugin/fromRimori/PluginTypes";
|
|
5
|
+
|
|
6
|
+
export interface Position {
|
|
7
|
+
x: number,
|
|
8
|
+
y: number,
|
|
9
|
+
text?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ContextMenu = ({ client }: { client: RimoriClient }) => {
|
|
13
|
+
const [isOpen, setIsOpen] = useState<boolean>(false);
|
|
14
|
+
const [actions, setActions] = useState<MenuEntry[]>([]);
|
|
15
|
+
const [position, setPosition] = useState<Position>({ x: 0, y: 0 });
|
|
16
|
+
const [openOnTextSelect, setOpenOnTextSelect] = useState(false);
|
|
17
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
client.plugin.getInstalled().then(plugins => {
|
|
21
|
+
setActions(plugins.flatMap(p => p.context_menu_actions).filter(Boolean));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
client.plugin.getUserInfo().then((userInfo) => {
|
|
25
|
+
setOpenOnTextSelect(userInfo.context_menu_on_select);
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
EventBus.on<{ actions: MenuEntry[] }>("global.contextMenu.createActions", ({ data }) => {
|
|
29
|
+
setActions([...data.actions, ...actions]);
|
|
30
|
+
});
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
// Track mouse position globally
|
|
35
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
36
|
+
const selectedText = window.getSelection()?.toString().trim();
|
|
37
|
+
if (isOpen && selectedText === position.text) return;
|
|
38
|
+
setPosition({ x: e.clientX, y: e.clientY, text: selectedText });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleMouseUp = (e: MouseEvent) => {
|
|
42
|
+
const selectedText = window.getSelection()?.toString().trim();
|
|
43
|
+
// Check if click is inside the context menu
|
|
44
|
+
if (menuRef.current && menuRef.current.contains(e.target as Node)) {
|
|
45
|
+
// Don't close the menu if clicking inside
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Prevent context menu on textarea or text input selection
|
|
50
|
+
const target = e.target as HTMLElement;
|
|
51
|
+
const isTextInput = target && (
|
|
52
|
+
(target.tagName === 'TEXTAREA') ||
|
|
53
|
+
(target.tagName === 'INPUT' && (target as HTMLInputElement).type === 'text')
|
|
54
|
+
);
|
|
55
|
+
if (isTextInput) {
|
|
56
|
+
setIsOpen(false);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (e.button === 0 && isOpen) {
|
|
61
|
+
setIsOpen(false);
|
|
62
|
+
window.getSelection()?.removeAllRanges();
|
|
63
|
+
} else if (selectedText && (openOnTextSelect || e.button === 2)) {
|
|
64
|
+
if (e.button === 2) {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
}
|
|
67
|
+
setPosition({ x: e.clientX, y: e.clientY, text: selectedText });
|
|
68
|
+
setIsOpen(true);
|
|
69
|
+
} else {
|
|
70
|
+
setIsOpen(false);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Add selectionchange listener to close menu if selection is cleared
|
|
75
|
+
const handleSelectionChange = () => {
|
|
76
|
+
const selectedText = window.getSelection()?.toString().trim();
|
|
77
|
+
if (!selectedText && isOpen) {
|
|
78
|
+
setIsOpen(false);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
83
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
84
|
+
document.addEventListener("contextmenu", handleMouseUp);
|
|
85
|
+
document.addEventListener("selectionchange", handleSelectionChange);
|
|
86
|
+
|
|
87
|
+
return () => {
|
|
88
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
89
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
90
|
+
document.removeEventListener("contextmenu", handleMouseUp);
|
|
91
|
+
document.removeEventListener("selectionchange", handleSelectionChange);
|
|
92
|
+
};
|
|
93
|
+
}, [openOnTextSelect, isOpen, position.text]);
|
|
94
|
+
|
|
95
|
+
if (!isOpen) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div
|
|
101
|
+
ref={menuRef}
|
|
102
|
+
className="fixed bg-gray-400 dark:bg-gray-700 shadow-lg border border-gray-400 rounded-md overflow-hidden dark:text-white z-50"
|
|
103
|
+
style={{ top: position.y, left: position.x }}>
|
|
104
|
+
{actions.map((action, index) => (
|
|
105
|
+
<MenuEntryItem key={index} icon={action.icon} text={action.text} onClick={() => {
|
|
106
|
+
setIsOpen(false);
|
|
107
|
+
window.getSelection()?.removeAllRanges();
|
|
108
|
+
client.event.emitSidebarAction(action.pluginId, action.actionKey, position.text);
|
|
109
|
+
}} />
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function MenuEntryItem(props: { icon: React.ReactNode, text: string, onClick: () => void }) {
|
|
116
|
+
return <button onClick={props.onClick} className="px-4 py-2 text-left hover:bg-gray-500 dark:hover:bg-gray-600 w-full flex flex-row">
|
|
117
|
+
<span className="flex-grow">{props.icon}</span>
|
|
118
|
+
<span className="flex-grow">{props.text}</span>
|
|
119
|
+
{/* <span className="text-sm">Ctrl+Shift+xxxx</span> */}
|
|
120
|
+
</button>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export default ContextMenu;
|
package/src/core.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
// Core functionality exports
|
|
2
|
-
export * from "./controller/AIController";
|
|
3
|
-
export * from "./controller/SharedContentController";
|
|
4
|
-
export * from "./controller/SettingsController";
|
|
5
2
|
export * from "./plugin/RimoriClient";
|
|
6
3
|
export * from "./plugin/PluginController";
|
|
7
4
|
export * from "./utils/difficultyConverter";
|
|
8
5
|
export * from "./utils/PluginUtils";
|
|
9
6
|
export * from "./worker/WorkerSetup";
|
|
10
|
-
export * from "./
|
|
7
|
+
export * from "./utils/Language";
|