@rimori/react-client 0.1.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/.prettierignore +35 -0
- package/LICENSE +201 -0
- package/README copy.md +1216 -0
- package/README.md +1 -0
- package/dist/components/MarkdownEditor.d.ts +8 -0
- package/dist/components/MarkdownEditor.js +48 -0
- package/dist/components/Spinner.d.ts +8 -0
- package/dist/components/Spinner.js +4 -0
- package/dist/components/ai/Assistant.d.ts +9 -0
- package/dist/components/ai/Assistant.js +58 -0
- package/dist/components/ai/Avatar.d.ts +14 -0
- package/dist/components/ai/Avatar.js +59 -0
- package/dist/components/ai/EmbeddedAssistent/AudioInputField.d.ts +7 -0
- package/dist/components/ai/EmbeddedAssistent/AudioInputField.js +37 -0
- package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.d.ts +8 -0
- package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.js +79 -0
- package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.d.ts +19 -0
- package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.js +91 -0
- package/dist/components/ai/EmbeddedAssistent/TTS/Player.d.ts +27 -0
- package/dist/components/ai/EmbeddedAssistent/TTS/Player.js +185 -0
- package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.d.ts +11 -0
- package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.js +95 -0
- package/dist/components/ai/utils.d.ts +6 -0
- package/dist/components/ai/utils.js +13 -0
- package/dist/components/audio/Playbutton.d.ts +15 -0
- package/dist/components/audio/Playbutton.js +80 -0
- package/dist/components/components/ContextMenu.d.ts +10 -0
- package/dist/components/components/ContextMenu.js +135 -0
- package/dist/hooks/I18nHooks.d.ts +11 -0
- package/dist/hooks/I18nHooks.js +25 -0
- package/dist/hooks/UseChatHook.d.ts +10 -0
- package/dist/hooks/UseChatHook.js +29 -0
- package/dist/providers/PluginProvider.d.ts +11 -0
- package/dist/providers/PluginProvider.js +142 -0
- package/dist/react-client/plugin/ThemeSetter.d.ts +2 -0
- package/dist/react-client/plugin/ThemeSetter.js +19 -0
- package/dist/react-client/src/components/ContextMenu.d.ts +10 -0
- package/dist/react-client/src/components/ContextMenu.js +135 -0
- package/dist/react-client/src/components/MarkdownEditor.d.ts +8 -0
- package/dist/react-client/src/components/MarkdownEditor.js +48 -0
- package/dist/react-client/src/components/Spinner.d.ts +8 -0
- package/dist/react-client/src/components/Spinner.js +4 -0
- package/dist/react-client/src/components/ai/Assistant.d.ts +9 -0
- package/dist/react-client/src/components/ai/Assistant.js +58 -0
- package/dist/react-client/src/components/ai/Avatar.d.ts +14 -0
- package/dist/react-client/src/components/ai/Avatar.js +59 -0
- package/dist/react-client/src/components/ai/EmbeddedAssistent/AudioInputField.d.ts +7 -0
- package/dist/react-client/src/components/ai/EmbeddedAssistent/AudioInputField.js +37 -0
- package/dist/react-client/src/components/ai/EmbeddedAssistent/CircleAudioAvatar.d.ts +8 -0
- package/dist/react-client/src/components/ai/EmbeddedAssistent/CircleAudioAvatar.js +79 -0
- package/dist/react-client/src/components/ai/EmbeddedAssistent/TTS/MessageSender.d.ts +19 -0
- package/dist/react-client/src/components/ai/EmbeddedAssistent/TTS/MessageSender.js +91 -0
- package/dist/react-client/src/components/ai/EmbeddedAssistent/TTS/Player.d.ts +27 -0
- package/dist/react-client/src/components/ai/EmbeddedAssistent/TTS/Player.js +185 -0
- package/dist/react-client/src/components/ai/EmbeddedAssistent/VoiceRecoder.d.ts +11 -0
- package/dist/react-client/src/components/ai/EmbeddedAssistent/VoiceRecoder.js +95 -0
- package/dist/react-client/src/components/ai/utils.d.ts +6 -0
- package/dist/react-client/src/components/ai/utils.js +13 -0
- package/dist/react-client/src/components/audio/Playbutton.d.ts +15 -0
- package/dist/react-client/src/components/audio/Playbutton.js +82 -0
- package/dist/react-client/src/components/components/ContextMenu.d.ts +10 -0
- package/dist/react-client/src/components/components/ContextMenu.js +135 -0
- package/dist/react-client/src/hooks/I18nHooks.d.ts +11 -0
- package/dist/react-client/src/hooks/I18nHooks.js +25 -0
- package/dist/react-client/src/hooks/UseChatHook.d.ts +10 -0
- package/dist/react-client/src/hooks/UseChatHook.js +29 -0
- package/dist/react-client/src/plugin/ThemeSetter.d.ts +2 -0
- package/dist/react-client/src/plugin/ThemeSetter.js +19 -0
- package/dist/react-client/src/providers/PluginProvider.d.ts +12 -0
- package/dist/react-client/src/providers/PluginProvider.js +142 -0
- package/dist/react-client/src/utils/FullscreenUtils.d.ts +2 -0
- package/dist/react-client/src/utils/FullscreenUtils.js +23 -0
- package/dist/react-client/src/utils/PluginUtils.d.ts +2 -0
- package/dist/react-client/src/utils/PluginUtils.js +23 -0
- package/dist/rimori-client/src/cli/types/DatabaseTypes.d.ts +103 -0
- package/dist/rimori-client/src/cli/types/DatabaseTypes.js +2 -0
- package/dist/rimori-client/src/controller/AIController.d.ts +15 -0
- package/dist/rimori-client/src/controller/AIController.js +255 -0
- package/dist/rimori-client/src/controller/AccomplishmentController.d.ts +38 -0
- package/dist/rimori-client/src/controller/AccomplishmentController.js +112 -0
- package/dist/rimori-client/src/controller/AudioController.d.ts +37 -0
- package/dist/rimori-client/src/controller/AudioController.js +68 -0
- package/dist/rimori-client/src/controller/ExerciseController.d.ts +54 -0
- package/dist/rimori-client/src/controller/ExerciseController.js +74 -0
- package/dist/rimori-client/src/controller/ObjectController.d.ts +42 -0
- package/dist/rimori-client/src/controller/ObjectController.js +76 -0
- package/dist/rimori-client/src/controller/SettingsController.d.ts +79 -0
- package/dist/rimori-client/src/controller/SettingsController.js +118 -0
- package/dist/rimori-client/src/controller/SharedContentController.d.ts +106 -0
- package/dist/rimori-client/src/controller/SharedContentController.js +285 -0
- package/dist/rimori-client/src/controller/TranslationController.d.ts +38 -0
- package/dist/rimori-client/src/controller/TranslationController.js +106 -0
- package/dist/rimori-client/src/controller/VoiceController.d.ts +9 -0
- package/dist/rimori-client/src/controller/VoiceController.js +37 -0
- package/dist/rimori-client/src/fromRimori/EventBus.d.ts +101 -0
- package/dist/rimori-client/src/fromRimori/EventBus.js +263 -0
- package/dist/rimori-client/src/fromRimori/PluginTypes.d.ts +174 -0
- package/dist/rimori-client/src/fromRimori/PluginTypes.js +1 -0
- package/dist/rimori-client/src/index.d.ts +11 -0
- package/dist/rimori-client/src/index.js +10 -0
- package/dist/rimori-client/src/plugin/CommunicationHandler.d.ts +48 -0
- package/dist/rimori-client/src/plugin/CommunicationHandler.js +234 -0
- package/dist/rimori-client/src/plugin/Logger.d.ts +73 -0
- package/dist/rimori-client/src/plugin/Logger.js +308 -0
- package/dist/rimori-client/src/plugin/RimoriClient.d.ts +258 -0
- package/dist/rimori-client/src/plugin/RimoriClient.js +375 -0
- package/dist/rimori-client/src/plugin/StandaloneClient.d.ts +17 -0
- package/dist/rimori-client/src/plugin/StandaloneClient.js +115 -0
- package/dist/rimori-client/src/utils/difficultyConverter.d.ts +4 -0
- package/dist/rimori-client/src/utils/difficultyConverter.js +10 -0
- package/dist/rimori-client/src/utils/endpoint.d.ts +2 -0
- package/dist/rimori-client/src/utils/endpoint.js +2 -0
- package/dist/style.css +110 -0
- package/dist/style.css.map +1 -0
- package/dist/utils/PluginUtils.d.ts +2 -0
- package/dist/utils/PluginUtils.js +23 -0
- package/eslint.config.js +53 -0
- package/index.ts +6 -0
- package/package.json +47 -0
- package/prettier.config.js +8 -0
- package/src/components/ContextMenu.tsx +177 -0
- package/src/components/MarkdownEditor.tsx +144 -0
- package/src/components/Spinner.tsx +29 -0
- package/src/components/ai/Assistant.tsx +96 -0
- package/src/components/ai/Avatar.tsx +99 -0
- package/src/components/ai/EmbeddedAssistent/AudioInputField.tsx +73 -0
- package/src/components/ai/EmbeddedAssistent/CircleAudioAvatar.tsx +107 -0
- package/src/components/ai/EmbeddedAssistent/TTS/MessageSender.ts +96 -0
- package/src/components/ai/EmbeddedAssistent/TTS/Player.ts +197 -0
- package/src/components/ai/EmbeddedAssistent/VoiceRecoder.tsx +129 -0
- package/src/components/ai/utils.ts +21 -0
- package/src/components/audio/Playbutton.tsx +126 -0
- package/src/hooks/I18nHooks.ts +33 -0
- package/src/hooks/UseChatHook.ts +38 -0
- package/src/plugin/ThemeSetter.ts +23 -0
- package/src/providers/PluginProvider.tsx +197 -0
- package/src/style.scss +136 -0
- package/src/utils/FullscreenUtils.ts +22 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
import { createClient } from '@supabase/supabase-js';
|
|
11
|
+
import { EventBus } from '../fromRimori/EventBus';
|
|
12
|
+
import { DEFAULT_ANON_KEY, DEFAULT_ENDPOINT } from '../utils/endpoint';
|
|
13
|
+
export class StandaloneClient {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.supabase = createClient(config.url, config.key);
|
|
16
|
+
this.config = config;
|
|
17
|
+
}
|
|
18
|
+
static getInstance() {
|
|
19
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
20
|
+
if (!StandaloneClient.instance) {
|
|
21
|
+
const config = yield fetch('https://app.rimori.se/config.json')
|
|
22
|
+
.then((res) => res.json())
|
|
23
|
+
.catch((err) => {
|
|
24
|
+
console.warn('Error fetching config.json, using default values', err);
|
|
25
|
+
});
|
|
26
|
+
StandaloneClient.instance = new StandaloneClient({
|
|
27
|
+
url: (config === null || config === void 0 ? void 0 : config.SUPABASE_URL) || DEFAULT_ENDPOINT,
|
|
28
|
+
key: (config === null || config === void 0 ? void 0 : config.SUPABASE_ANON_KEY) || DEFAULT_ANON_KEY,
|
|
29
|
+
backendUrl: (config === null || config === void 0 ? void 0 : config.BACKEND_URL) || 'https://api.rimori.se',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return StandaloneClient.instance;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
getClient() {
|
|
36
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
37
|
+
return this.supabase;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
needsLogin() {
|
|
41
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
42
|
+
const { error } = yield this.supabase.auth.getUser();
|
|
43
|
+
return error !== null;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
login(email, password) {
|
|
47
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
48
|
+
const { error } = yield this.supabase.auth.signInWithPassword({ email, password });
|
|
49
|
+
if (error) {
|
|
50
|
+
console.error('Login failed:', error);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
console.log('Successfully logged in');
|
|
54
|
+
return true;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
static initListeners(pluginId) {
|
|
58
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
59
|
+
console.warn('The plugin seams to not be running inside the Rimori platform. Switching to development standalone mode.');
|
|
60
|
+
// console.log("event that needs to be handled", event);
|
|
61
|
+
const { supabase, config } = yield StandaloneClient.getInstance();
|
|
62
|
+
// EventBus.on("*", async (event) => {
|
|
63
|
+
EventBus.respond('standalone', 'global.supabase.requestAccess', () => __awaiter(this, void 0, void 0, function* () {
|
|
64
|
+
var _a;
|
|
65
|
+
const session = yield supabase.auth.getSession();
|
|
66
|
+
console.log('session', session);
|
|
67
|
+
// Call the NestJS backend endpoint instead of the Supabase edge function
|
|
68
|
+
// get current guild id if any
|
|
69
|
+
let guildId = null;
|
|
70
|
+
try {
|
|
71
|
+
const { data: { user }, } = yield supabase.auth.getUser();
|
|
72
|
+
if (user) {
|
|
73
|
+
const { data: profile } = yield supabase
|
|
74
|
+
.from('profiles')
|
|
75
|
+
.select('current_guild_id')
|
|
76
|
+
.eq('user_id', user.id)
|
|
77
|
+
.maybeSingle();
|
|
78
|
+
guildId = (profile === null || profile === void 0 ? void 0 : profile.current_guild_id) || null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (_) {
|
|
82
|
+
guildId = null;
|
|
83
|
+
}
|
|
84
|
+
const response = yield fetch(`${config.backendUrl}/plugin/token`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: {
|
|
87
|
+
'Content-Type': 'application/json',
|
|
88
|
+
Authorization: `Bearer ${(_a = session.data.session) === null || _a === void 0 ? void 0 : _a.access_token}`,
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify({
|
|
91
|
+
pluginId: pluginId,
|
|
92
|
+
guildId: guildId,
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
const errorText = yield response.text();
|
|
97
|
+
throw new Error(`Failed to get plugin token. ${response.status}: ${errorText}`);
|
|
98
|
+
}
|
|
99
|
+
const data = yield response.json();
|
|
100
|
+
return {
|
|
101
|
+
token: data.token,
|
|
102
|
+
pluginId: pluginId,
|
|
103
|
+
url: config.url,
|
|
104
|
+
key: config.key,
|
|
105
|
+
backendUrl: config.backendUrl,
|
|
106
|
+
tablePrefix: pluginId,
|
|
107
|
+
expiration: new Date(Date.now() + 1000 * 60 * 60 * 1.5), // 1.5 hours
|
|
108
|
+
};
|
|
109
|
+
}));
|
|
110
|
+
EventBus.on('*', (event) => __awaiter(this, void 0, void 0, function* () {
|
|
111
|
+
console.log('[standalone] would send event to parent', event);
|
|
112
|
+
}), ['standalone']);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type LanguageLevel = 'Pre-A1' | 'A1' | 'A2' | 'B1' | 'B2' | 'C1' | 'C2' | 'Post-C2';
|
|
2
|
+
export declare function getDifficultyLevel(difficulty: LanguageLevel): number;
|
|
3
|
+
export declare function getDifficultyLabel(difficulty: number): LanguageLevel;
|
|
4
|
+
export declare function getNeighborDifficultyLevel(difficulty: LanguageLevel, difficultyAdjustment: number): LanguageLevel;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const codes = ['Pre-A1', 'A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'Post-C2'];
|
|
2
|
+
export function getDifficultyLevel(difficulty) {
|
|
3
|
+
return codes.indexOf(difficulty) + 1;
|
|
4
|
+
}
|
|
5
|
+
export function getDifficultyLabel(difficulty) {
|
|
6
|
+
return codes[difficulty];
|
|
7
|
+
}
|
|
8
|
+
export function getNeighborDifficultyLevel(difficulty, difficultyAdjustment) {
|
|
9
|
+
return getDifficultyLabel(getDifficultyLevel(difficulty) + difficultyAdjustment - 1);
|
|
10
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const DEFAULT_ENDPOINT = "https://pheptqdoqsdnadgoihvr.supabase.co";
|
|
2
|
+
export declare const DEFAULT_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBoZXB0cWRvcXNkbmFkZ29paHZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzE2OTY2ODcsImV4cCI6MjA0NzI3MjY4N30.4GPFAXTF8685FaXISdAPNCIM-H3RGLo8GbyhQpu1mP0";
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export const DEFAULT_ENDPOINT = 'https://pheptqdoqsdnadgoihvr.supabase.co';
|
|
2
|
+
export const DEFAULT_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBoZXB0cWRvcXNkbmFkZ29paHZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzE2OTY2ODcsImV4cCI6MjA0NzI3MjY4N30.4GPFAXTF8685FaXISdAPNCIM-H3RGLo8GbyhQpu1mP0';
|
package/dist/style.css
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
dialog::backdrop {
|
|
2
|
+
backdrop-filter: blur(2px);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.dark * dialog::backdrop {
|
|
6
|
+
background: transparent;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.tiptap {
|
|
10
|
+
padding-top: 5px;
|
|
11
|
+
padding-left: 7px;
|
|
12
|
+
/* min-height: 300px; */
|
|
13
|
+
}
|
|
14
|
+
.tiptap:focus-visible {
|
|
15
|
+
outline: none;
|
|
16
|
+
}
|
|
17
|
+
.tiptap h1,
|
|
18
|
+
.tiptap h2,
|
|
19
|
+
.tiptap h3,
|
|
20
|
+
.tiptap h4,
|
|
21
|
+
.tiptap h5,
|
|
22
|
+
.tiptap h6 {
|
|
23
|
+
@apply font-bold;
|
|
24
|
+
margin-bottom: 1rem;
|
|
25
|
+
}
|
|
26
|
+
.tiptap h1 {
|
|
27
|
+
@apply text-4xl;
|
|
28
|
+
}
|
|
29
|
+
.tiptap h2 {
|
|
30
|
+
@apply text-3xl;
|
|
31
|
+
}
|
|
32
|
+
.tiptap h3 {
|
|
33
|
+
@apply text-2xl;
|
|
34
|
+
}
|
|
35
|
+
.tiptap h4 {
|
|
36
|
+
@apply text-xl;
|
|
37
|
+
}
|
|
38
|
+
.tiptap h5 {
|
|
39
|
+
@apply text-lg;
|
|
40
|
+
}
|
|
41
|
+
.tiptap h6 {
|
|
42
|
+
@apply text-base;
|
|
43
|
+
}
|
|
44
|
+
.tiptap p {
|
|
45
|
+
@apply mb-4;
|
|
46
|
+
}
|
|
47
|
+
.tiptap a {
|
|
48
|
+
@apply text-blue-600 hover:text-blue-800;
|
|
49
|
+
text-decoration: none;
|
|
50
|
+
}
|
|
51
|
+
.tiptap a:hover {
|
|
52
|
+
@apply underline;
|
|
53
|
+
}
|
|
54
|
+
.tiptap ul {
|
|
55
|
+
@apply list-disc pl-8;
|
|
56
|
+
}
|
|
57
|
+
.tiptap ul li > p {
|
|
58
|
+
@apply mb-1;
|
|
59
|
+
}
|
|
60
|
+
.tiptap ol {
|
|
61
|
+
@apply list-decimal pl-7;
|
|
62
|
+
}
|
|
63
|
+
.tiptap ol li > p {
|
|
64
|
+
@apply mb-1;
|
|
65
|
+
}
|
|
66
|
+
.tiptap blockquote {
|
|
67
|
+
@apply border-l-4 pl-4 italic text-gray-600 my-4;
|
|
68
|
+
border-color: #ccc;
|
|
69
|
+
}
|
|
70
|
+
.tiptap code {
|
|
71
|
+
font-family: monospace;
|
|
72
|
+
}
|
|
73
|
+
.tiptap pre {
|
|
74
|
+
@apply bg-gray-800 text-gray-500 p-4 rounded-lg overflow-x-auto;
|
|
75
|
+
font-family: monospace;
|
|
76
|
+
white-space: pre-wrap;
|
|
77
|
+
word-wrap: break-word;
|
|
78
|
+
}
|
|
79
|
+
.tiptap img {
|
|
80
|
+
@apply max-w-full h-auto rounded-lg my-4;
|
|
81
|
+
}
|
|
82
|
+
.tiptap table {
|
|
83
|
+
@apply table-auto w-full border-collapse mb-4;
|
|
84
|
+
}
|
|
85
|
+
.tiptap th,
|
|
86
|
+
.tiptap td {
|
|
87
|
+
@apply border px-4 py-2 text-left;
|
|
88
|
+
}
|
|
89
|
+
.tiptap th {
|
|
90
|
+
@apply bg-gray-500 font-semibold;
|
|
91
|
+
}
|
|
92
|
+
.tiptap tr:nth-child(even) {
|
|
93
|
+
@apply bg-gray-400;
|
|
94
|
+
}
|
|
95
|
+
@media (max-width: 768px) {
|
|
96
|
+
.tiptap h1 {
|
|
97
|
+
@apply text-3xl;
|
|
98
|
+
}
|
|
99
|
+
.tiptap h2 {
|
|
100
|
+
@apply text-2xl;
|
|
101
|
+
}
|
|
102
|
+
.tiptap p {
|
|
103
|
+
@apply text-base;
|
|
104
|
+
}
|
|
105
|
+
.tiptap img {
|
|
106
|
+
@apply max-w-full;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/*# sourceMappingURL=style.css.map */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sourceRoot":"","sources":["../src/style.scss"],"names":[],"mappings":"AAAA;EACE;;;AAIF;EACE;;;AAGF;EACE;EACA;AACA;;AAEA;EACE;;AAGF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;EACA;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;EACA;;AAGF;EACE;;AAGF;EACE;;AAEA;EACE;;AAIJ;EACE;;AAEA;EACE;;AAIJ;EACE;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;;AAGF;EACE;;AAGF;AAAA;EAEE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;IACE;;EAGF;IACE;;EAGF;IACE;;EAGF;IACE","file":"style.css"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function isFullscreen() {
|
|
2
|
+
return !!document.fullscreenElement;
|
|
3
|
+
}
|
|
4
|
+
export function triggerFullscreen(onStateChange, selector) {
|
|
5
|
+
document.addEventListener('fullscreenchange', () => {
|
|
6
|
+
onStateChange(isFullscreen());
|
|
7
|
+
});
|
|
8
|
+
try {
|
|
9
|
+
const ref = document.querySelector(selector || '#root');
|
|
10
|
+
if (!isFullscreen()) {
|
|
11
|
+
// @ts-ignore
|
|
12
|
+
ref.requestFullscreen() || ref.webkitRequestFullscreen();
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
document.exitFullscreen() || document.webkitExitFullscreen();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
console.error('Failed to enter fullscreen', error.message);
|
|
21
|
+
}
|
|
22
|
+
onStateChange(isFullscreen());
|
|
23
|
+
}
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import js from '@eslint/js';
|
|
2
|
+
import globals from 'globals';
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks';
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh';
|
|
5
|
+
import tseslint from 'typescript-eslint';
|
|
6
|
+
import prettier from 'eslint-plugin-prettier';
|
|
7
|
+
import prettierConfig from 'eslint-config-prettier';
|
|
8
|
+
|
|
9
|
+
export default [
|
|
10
|
+
{ ignores: ['dist', 'node_modules', 'build', '*.js'] },
|
|
11
|
+
js.configs.recommended,
|
|
12
|
+
...tseslint.configs.recommended,
|
|
13
|
+
prettierConfig,
|
|
14
|
+
{
|
|
15
|
+
files: ['**/*.{ts,tsx,js,jsx}'],
|
|
16
|
+
languageOptions: {
|
|
17
|
+
ecmaVersion: 2020,
|
|
18
|
+
globals: {
|
|
19
|
+
...globals.browser,
|
|
20
|
+
...globals.node,
|
|
21
|
+
...globals.jest,
|
|
22
|
+
},
|
|
23
|
+
sourceType: 'module',
|
|
24
|
+
parserOptions: {
|
|
25
|
+
projectService: true,
|
|
26
|
+
tsconfigRootDir: import.meta.dirname,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
plugins: {
|
|
30
|
+
'react-hooks': reactHooks,
|
|
31
|
+
'react-refresh': reactRefresh,
|
|
32
|
+
prettier: prettier,
|
|
33
|
+
},
|
|
34
|
+
rules: {
|
|
35
|
+
...reactHooks.configs.recommended.rules,
|
|
36
|
+
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
|
37
|
+
'@typescript-eslint/no-unused-vars': 'warn',
|
|
38
|
+
'@typescript-eslint/no-explicit-any': 'warn',
|
|
39
|
+
'@typescript-eslint/no-floating-promises': 'warn',
|
|
40
|
+
'@typescript-eslint/no-unsafe-argument': 'warn',
|
|
41
|
+
'@typescript-eslint/explicit-function-return-type': 'warn',
|
|
42
|
+
'@typescript-eslint/explicit-module-boundary-types': 'warn',
|
|
43
|
+
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
|
44
|
+
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
|
45
|
+
'@typescript-eslint/no-unsafe-call': 'warn',
|
|
46
|
+
'@typescript-eslint/no-unsafe-return': 'warn',
|
|
47
|
+
'@typescript-eslint/no-inferrable-types': 'warn',
|
|
48
|
+
'@typescript-eslint/no-non-null-assertion': 'warn',
|
|
49
|
+
'@typescript-eslint/ban-ts-comment': 'warn',
|
|
50
|
+
'prettier/prettier': 'error',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
];
|
package/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Re-export everything
|
|
2
|
+
export * from './src/hooks/UseChatHook';
|
|
3
|
+
export * from './src/providers/PluginProvider';
|
|
4
|
+
export * from './src/utils/FullscreenUtils';
|
|
5
|
+
export { FirstMessages } from './src/components/ai/utils';
|
|
6
|
+
export { useTranslation } from './src/hooks/I18nHooks';
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rimori/react-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc && sass src/style.scss:dist/style.css",
|
|
15
|
+
"dev": "tsc -w --preserveWatchOutput",
|
|
16
|
+
"css-dev": "sass --watch src/style.scss:dist/style.css",
|
|
17
|
+
"lint": "eslint . --fix",
|
|
18
|
+
"format": "prettier --write ."
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"react": "^18.0.0",
|
|
22
|
+
"react-dom": "^18.0.0",
|
|
23
|
+
"@rimori/client": "^2.0.0"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@tiptap/react": "2.10.3",
|
|
27
|
+
"@tiptap/starter-kit": "2.10.3",
|
|
28
|
+
"html2canvas": "1.4.1",
|
|
29
|
+
"react-icons": "5.4.0",
|
|
30
|
+
"tiptap-markdown": "0.8.10"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@eslint/js": "^9.37.0",
|
|
34
|
+
"@rimori/client": "^2.0.0",
|
|
35
|
+
"eslint-config-prettier": "^10.1.8",
|
|
36
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
37
|
+
"eslint-plugin-react-hooks": "^7.0.0",
|
|
38
|
+
"eslint-plugin-react-refresh": "^0.4.23",
|
|
39
|
+
"form-data": "^4.0.2",
|
|
40
|
+
"globals": "^16.4.0",
|
|
41
|
+
"node-fetch": "^3.3.2",
|
|
42
|
+
"prettier": "^3.6.2",
|
|
43
|
+
"sass": "^1.82.0",
|
|
44
|
+
"typescript": "^5.7.2",
|
|
45
|
+
"typescript-eslint": "^8.46.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { EventBus, RimoriClient, MenuEntry } from '@rimori/client';
|
|
3
|
+
|
|
4
|
+
export interface Position {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
text?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ContextMenu = ({ client }: { client: RimoriClient }) => {
|
|
11
|
+
const [isOpen, setIsOpen] = useState<boolean>(false);
|
|
12
|
+
const [actions, setActions] = useState<MenuEntry[]>([]);
|
|
13
|
+
const [position, setPosition] = useState<Position>({ x: 0, y: 0 });
|
|
14
|
+
const [openOnTextSelect, setOpenOnTextSelect] = useState(false);
|
|
15
|
+
const [menuWidth, setMenuWidth] = useState<number>(0);
|
|
16
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
17
|
+
const isMobile = window.innerWidth < 768;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Calculates position for mobile context menu based on selected text bounds.
|
|
21
|
+
* Centers the menu horizontally over the selected text and positions it 30px below the text's end.
|
|
22
|
+
* @param selectedText - The currently selected text
|
|
23
|
+
* @param menuWidth - The width of the menu to center properly
|
|
24
|
+
* @returns Position object with x and y coordinates
|
|
25
|
+
*/
|
|
26
|
+
const calculateMobilePosition = (selectedText: string, menuWidth: number = 0): Position => {
|
|
27
|
+
const selection = window.getSelection();
|
|
28
|
+
if (!selection || !selectedText) {
|
|
29
|
+
return { x: 0, y: 0, text: selectedText };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const range = selection.getRangeAt(0);
|
|
33
|
+
const rect = range.getBoundingClientRect();
|
|
34
|
+
|
|
35
|
+
// Center horizontally over the selected text, accounting for menu width
|
|
36
|
+
const centerX = rect.left + rect.width / 2 - menuWidth / 2;
|
|
37
|
+
|
|
38
|
+
// Position 12px below where the text ends vertically
|
|
39
|
+
const textEndY = rect.bottom + 12;
|
|
40
|
+
|
|
41
|
+
return { x: centerX, y: textEndY, text: selectedText };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const actions = client.plugin
|
|
46
|
+
.getPluginInfo()
|
|
47
|
+
.installedPlugins.flatMap((p) => p.context_menu_actions)
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
setActions(actions);
|
|
50
|
+
setOpenOnTextSelect(client.plugin.getUserInfo().context_menu_on_select);
|
|
51
|
+
|
|
52
|
+
EventBus.on<{ actions: MenuEntry[] }>('global.contextMenu.createActions', ({ data }) => {
|
|
53
|
+
setActions([...data.actions, ...actions]);
|
|
54
|
+
});
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
// Update menu width when menu is rendered
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (isOpen && menuRef.current) {
|
|
60
|
+
setMenuWidth(menuRef.current.offsetWidth);
|
|
61
|
+
}
|
|
62
|
+
}, [isOpen, actions]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
// Track mouse position globally
|
|
66
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
67
|
+
const selectedText = window.getSelection()?.toString().trim();
|
|
68
|
+
if (isOpen && selectedText === position.text) return;
|
|
69
|
+
|
|
70
|
+
if (isMobile && selectedText) {
|
|
71
|
+
setPosition(calculateMobilePosition(selectedText, menuWidth));
|
|
72
|
+
} else {
|
|
73
|
+
setPosition({ x: e.clientX, y: e.clientY, text: selectedText });
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const handleMouseUp = (e: MouseEvent) => {
|
|
78
|
+
const selectedText = window.getSelection()?.toString().trim();
|
|
79
|
+
// Check if click is inside the context menu
|
|
80
|
+
if (menuRef.current && menuRef.current.contains(e.target as Node)) {
|
|
81
|
+
// Don't close the menu if clicking inside
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Prevent context menu on textarea or text input selection
|
|
86
|
+
const target = e.target as HTMLElement;
|
|
87
|
+
const isTextInput =
|
|
88
|
+
target &&
|
|
89
|
+
(target.tagName === 'TEXTAREA' || (target.tagName === 'INPUT' && (target as HTMLInputElement).type === 'text'));
|
|
90
|
+
if (isTextInput) {
|
|
91
|
+
setIsOpen(false);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (e.button === 0 && isOpen) {
|
|
96
|
+
setIsOpen(false);
|
|
97
|
+
window.getSelection()?.removeAllRanges();
|
|
98
|
+
} else if (selectedText && (openOnTextSelect || e.button === 2)) {
|
|
99
|
+
if (e.button === 2) {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (isMobile) {
|
|
104
|
+
setPosition(calculateMobilePosition(selectedText, menuWidth));
|
|
105
|
+
} else {
|
|
106
|
+
setPosition({ x: e.clientX, y: e.clientY, text: selectedText });
|
|
107
|
+
}
|
|
108
|
+
setIsOpen(true);
|
|
109
|
+
} else {
|
|
110
|
+
setIsOpen(false);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Add selectionchange listener to close menu if selection is cleared and update position for mobile
|
|
115
|
+
const handleSelectionChange = () => {
|
|
116
|
+
const selectedText = window.getSelection()?.toString().trim();
|
|
117
|
+
if (!selectedText && isOpen) {
|
|
118
|
+
setIsOpen(false);
|
|
119
|
+
} else if (selectedText && isOpen && isMobile) {
|
|
120
|
+
// Update position in real-time as text selection changes on mobile
|
|
121
|
+
setPosition(calculateMobilePosition(selectedText, menuWidth));
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
126
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
127
|
+
document.addEventListener('contextmenu', handleMouseUp);
|
|
128
|
+
document.addEventListener('selectionchange', handleSelectionChange);
|
|
129
|
+
|
|
130
|
+
return () => {
|
|
131
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
132
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
133
|
+
document.removeEventListener('contextmenu', handleMouseUp);
|
|
134
|
+
document.removeEventListener('selectionchange', handleSelectionChange);
|
|
135
|
+
};
|
|
136
|
+
}, [openOnTextSelect, isOpen, position.text]);
|
|
137
|
+
|
|
138
|
+
if (!isOpen) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div
|
|
144
|
+
ref={menuRef}
|
|
145
|
+
className="fixed bg-gray-400 dark:bg-gray-700 shadow-lg border border-gray-400 rounded-md overflow-hidden dark:text-white z-50"
|
|
146
|
+
style={{ top: position.y, left: position.x }}
|
|
147
|
+
>
|
|
148
|
+
{actions.map((action, index) => (
|
|
149
|
+
<MenuEntryItem
|
|
150
|
+
key={index}
|
|
151
|
+
icon={action.icon}
|
|
152
|
+
text={action.text}
|
|
153
|
+
onClick={() => {
|
|
154
|
+
setIsOpen(false);
|
|
155
|
+
window.getSelection()?.removeAllRanges();
|
|
156
|
+
client.event.emitSidebarAction(action.plugin_id, action.action_key, position.text);
|
|
157
|
+
}}
|
|
158
|
+
/>
|
|
159
|
+
))}
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
function MenuEntryItem(props: { icon: React.ReactNode; text: string; onClick: () => void }) {
|
|
165
|
+
return (
|
|
166
|
+
<button
|
|
167
|
+
onClick={props.onClick}
|
|
168
|
+
className="px-4 py-2 text-left hover:bg-gray-500 dark:hover:bg-gray-600 w-full flex flex-row"
|
|
169
|
+
>
|
|
170
|
+
<span className="flex-grow">{props.icon}</span>
|
|
171
|
+
<span className="flex-grow">{props.text}</span>
|
|
172
|
+
{/* <span className="text-sm">Ctrl+Shift+xxxx</span> */}
|
|
173
|
+
</button>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export default ContextMenu;
|