@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.
Files changed (139) hide show
  1. package/.prettierignore +35 -0
  2. package/LICENSE +201 -0
  3. package/README copy.md +1216 -0
  4. package/README.md +1 -0
  5. package/dist/components/MarkdownEditor.d.ts +8 -0
  6. package/dist/components/MarkdownEditor.js +48 -0
  7. package/dist/components/Spinner.d.ts +8 -0
  8. package/dist/components/Spinner.js +4 -0
  9. package/dist/components/ai/Assistant.d.ts +9 -0
  10. package/dist/components/ai/Assistant.js +58 -0
  11. package/dist/components/ai/Avatar.d.ts +14 -0
  12. package/dist/components/ai/Avatar.js +59 -0
  13. package/dist/components/ai/EmbeddedAssistent/AudioInputField.d.ts +7 -0
  14. package/dist/components/ai/EmbeddedAssistent/AudioInputField.js +37 -0
  15. package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.d.ts +8 -0
  16. package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.js +79 -0
  17. package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.d.ts +19 -0
  18. package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.js +91 -0
  19. package/dist/components/ai/EmbeddedAssistent/TTS/Player.d.ts +27 -0
  20. package/dist/components/ai/EmbeddedAssistent/TTS/Player.js +185 -0
  21. package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.d.ts +11 -0
  22. package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.js +95 -0
  23. package/dist/components/ai/utils.d.ts +6 -0
  24. package/dist/components/ai/utils.js +13 -0
  25. package/dist/components/audio/Playbutton.d.ts +15 -0
  26. package/dist/components/audio/Playbutton.js +80 -0
  27. package/dist/components/components/ContextMenu.d.ts +10 -0
  28. package/dist/components/components/ContextMenu.js +135 -0
  29. package/dist/hooks/I18nHooks.d.ts +11 -0
  30. package/dist/hooks/I18nHooks.js +25 -0
  31. package/dist/hooks/UseChatHook.d.ts +10 -0
  32. package/dist/hooks/UseChatHook.js +29 -0
  33. package/dist/providers/PluginProvider.d.ts +11 -0
  34. package/dist/providers/PluginProvider.js +142 -0
  35. package/dist/react-client/plugin/ThemeSetter.d.ts +2 -0
  36. package/dist/react-client/plugin/ThemeSetter.js +19 -0
  37. package/dist/react-client/src/components/ContextMenu.d.ts +10 -0
  38. package/dist/react-client/src/components/ContextMenu.js +135 -0
  39. package/dist/react-client/src/components/MarkdownEditor.d.ts +8 -0
  40. package/dist/react-client/src/components/MarkdownEditor.js +48 -0
  41. package/dist/react-client/src/components/Spinner.d.ts +8 -0
  42. package/dist/react-client/src/components/Spinner.js +4 -0
  43. package/dist/react-client/src/components/ai/Assistant.d.ts +9 -0
  44. package/dist/react-client/src/components/ai/Assistant.js +58 -0
  45. package/dist/react-client/src/components/ai/Avatar.d.ts +14 -0
  46. package/dist/react-client/src/components/ai/Avatar.js +59 -0
  47. package/dist/react-client/src/components/ai/EmbeddedAssistent/AudioInputField.d.ts +7 -0
  48. package/dist/react-client/src/components/ai/EmbeddedAssistent/AudioInputField.js +37 -0
  49. package/dist/react-client/src/components/ai/EmbeddedAssistent/CircleAudioAvatar.d.ts +8 -0
  50. package/dist/react-client/src/components/ai/EmbeddedAssistent/CircleAudioAvatar.js +79 -0
  51. package/dist/react-client/src/components/ai/EmbeddedAssistent/TTS/MessageSender.d.ts +19 -0
  52. package/dist/react-client/src/components/ai/EmbeddedAssistent/TTS/MessageSender.js +91 -0
  53. package/dist/react-client/src/components/ai/EmbeddedAssistent/TTS/Player.d.ts +27 -0
  54. package/dist/react-client/src/components/ai/EmbeddedAssistent/TTS/Player.js +185 -0
  55. package/dist/react-client/src/components/ai/EmbeddedAssistent/VoiceRecoder.d.ts +11 -0
  56. package/dist/react-client/src/components/ai/EmbeddedAssistent/VoiceRecoder.js +95 -0
  57. package/dist/react-client/src/components/ai/utils.d.ts +6 -0
  58. package/dist/react-client/src/components/ai/utils.js +13 -0
  59. package/dist/react-client/src/components/audio/Playbutton.d.ts +15 -0
  60. package/dist/react-client/src/components/audio/Playbutton.js +82 -0
  61. package/dist/react-client/src/components/components/ContextMenu.d.ts +10 -0
  62. package/dist/react-client/src/components/components/ContextMenu.js +135 -0
  63. package/dist/react-client/src/hooks/I18nHooks.d.ts +11 -0
  64. package/dist/react-client/src/hooks/I18nHooks.js +25 -0
  65. package/dist/react-client/src/hooks/UseChatHook.d.ts +10 -0
  66. package/dist/react-client/src/hooks/UseChatHook.js +29 -0
  67. package/dist/react-client/src/plugin/ThemeSetter.d.ts +2 -0
  68. package/dist/react-client/src/plugin/ThemeSetter.js +19 -0
  69. package/dist/react-client/src/providers/PluginProvider.d.ts +12 -0
  70. package/dist/react-client/src/providers/PluginProvider.js +142 -0
  71. package/dist/react-client/src/utils/FullscreenUtils.d.ts +2 -0
  72. package/dist/react-client/src/utils/FullscreenUtils.js +23 -0
  73. package/dist/react-client/src/utils/PluginUtils.d.ts +2 -0
  74. package/dist/react-client/src/utils/PluginUtils.js +23 -0
  75. package/dist/rimori-client/src/cli/types/DatabaseTypes.d.ts +103 -0
  76. package/dist/rimori-client/src/cli/types/DatabaseTypes.js +2 -0
  77. package/dist/rimori-client/src/controller/AIController.d.ts +15 -0
  78. package/dist/rimori-client/src/controller/AIController.js +255 -0
  79. package/dist/rimori-client/src/controller/AccomplishmentController.d.ts +38 -0
  80. package/dist/rimori-client/src/controller/AccomplishmentController.js +112 -0
  81. package/dist/rimori-client/src/controller/AudioController.d.ts +37 -0
  82. package/dist/rimori-client/src/controller/AudioController.js +68 -0
  83. package/dist/rimori-client/src/controller/ExerciseController.d.ts +54 -0
  84. package/dist/rimori-client/src/controller/ExerciseController.js +74 -0
  85. package/dist/rimori-client/src/controller/ObjectController.d.ts +42 -0
  86. package/dist/rimori-client/src/controller/ObjectController.js +76 -0
  87. package/dist/rimori-client/src/controller/SettingsController.d.ts +79 -0
  88. package/dist/rimori-client/src/controller/SettingsController.js +118 -0
  89. package/dist/rimori-client/src/controller/SharedContentController.d.ts +106 -0
  90. package/dist/rimori-client/src/controller/SharedContentController.js +285 -0
  91. package/dist/rimori-client/src/controller/TranslationController.d.ts +38 -0
  92. package/dist/rimori-client/src/controller/TranslationController.js +106 -0
  93. package/dist/rimori-client/src/controller/VoiceController.d.ts +9 -0
  94. package/dist/rimori-client/src/controller/VoiceController.js +37 -0
  95. package/dist/rimori-client/src/fromRimori/EventBus.d.ts +101 -0
  96. package/dist/rimori-client/src/fromRimori/EventBus.js +263 -0
  97. package/dist/rimori-client/src/fromRimori/PluginTypes.d.ts +174 -0
  98. package/dist/rimori-client/src/fromRimori/PluginTypes.js +1 -0
  99. package/dist/rimori-client/src/index.d.ts +11 -0
  100. package/dist/rimori-client/src/index.js +10 -0
  101. package/dist/rimori-client/src/plugin/CommunicationHandler.d.ts +48 -0
  102. package/dist/rimori-client/src/plugin/CommunicationHandler.js +234 -0
  103. package/dist/rimori-client/src/plugin/Logger.d.ts +73 -0
  104. package/dist/rimori-client/src/plugin/Logger.js +308 -0
  105. package/dist/rimori-client/src/plugin/RimoriClient.d.ts +258 -0
  106. package/dist/rimori-client/src/plugin/RimoriClient.js +375 -0
  107. package/dist/rimori-client/src/plugin/StandaloneClient.d.ts +17 -0
  108. package/dist/rimori-client/src/plugin/StandaloneClient.js +115 -0
  109. package/dist/rimori-client/src/utils/difficultyConverter.d.ts +4 -0
  110. package/dist/rimori-client/src/utils/difficultyConverter.js +10 -0
  111. package/dist/rimori-client/src/utils/endpoint.d.ts +2 -0
  112. package/dist/rimori-client/src/utils/endpoint.js +2 -0
  113. package/dist/style.css +110 -0
  114. package/dist/style.css.map +1 -0
  115. package/dist/utils/PluginUtils.d.ts +2 -0
  116. package/dist/utils/PluginUtils.js +23 -0
  117. package/eslint.config.js +53 -0
  118. package/index.ts +6 -0
  119. package/package.json +47 -0
  120. package/prettier.config.js +8 -0
  121. package/src/components/ContextMenu.tsx +177 -0
  122. package/src/components/MarkdownEditor.tsx +144 -0
  123. package/src/components/Spinner.tsx +29 -0
  124. package/src/components/ai/Assistant.tsx +96 -0
  125. package/src/components/ai/Avatar.tsx +99 -0
  126. package/src/components/ai/EmbeddedAssistent/AudioInputField.tsx +73 -0
  127. package/src/components/ai/EmbeddedAssistent/CircleAudioAvatar.tsx +107 -0
  128. package/src/components/ai/EmbeddedAssistent/TTS/MessageSender.ts +96 -0
  129. package/src/components/ai/EmbeddedAssistent/TTS/Player.ts +197 -0
  130. package/src/components/ai/EmbeddedAssistent/VoiceRecoder.tsx +129 -0
  131. package/src/components/ai/utils.ts +21 -0
  132. package/src/components/audio/Playbutton.tsx +126 -0
  133. package/src/hooks/I18nHooks.ts +33 -0
  134. package/src/hooks/UseChatHook.ts +38 -0
  135. package/src/plugin/ThemeSetter.ts +23 -0
  136. package/src/providers/PluginProvider.tsx +197 -0
  137. package/src/style.scss +136 -0
  138. package/src/utils/FullscreenUtils.ts +22 -0
  139. 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,2 @@
1
+ export declare function isFullscreen(): boolean;
2
+ export declare function triggerFullscreen(onStateChange: (isFullscreen: boolean) => void, selector?: string): void;
@@ -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
+ }
@@ -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,8 @@
1
+ export default {
2
+ printWidth: 120,
3
+ singleQuote: true,
4
+ trailingComma: "all",
5
+ semi: true,
6
+ tabWidth: 2,
7
+ useTabs: false,
8
+ };
@@ -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;