@rimori/client 2.5.8 → 2.5.9
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 +2 -2
- package/dist/controller/ObjectController.d.ts +2 -2
- package/dist/controller/ObjectController.js +55 -26
- package/dist/controller/TranslationController.d.ts +12 -3
- package/dist/controller/TranslationController.js +50 -3
- package/dist/plugin/CommunicationHandler.js +2 -2
- package/dist/plugin/RimoriClient.js +1 -1
- package/dist/plugin/module/AIModule.d.ts +6 -0
- package/dist/plugin/module/AIModule.js +11 -1
- package/dist/plugin/module/PluginModule.d.ts +2 -1
- package/dist/plugin/module/PluginModule.js +2 -2
- package/dist/utils/difficultyConverter.d.ts +8 -0
- package/dist/utils/difficultyConverter.js +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -119,7 +119,7 @@ Access metadata and settings through `client.plugin`:
|
|
|
119
119
|
- `plugin.getSettings(defaults)` / `plugin.setSettings(settings)` – persist configuration.
|
|
120
120
|
- `plugin.getPluginInfo()` – read active/installed plugin information.
|
|
121
121
|
- `plugin.getUserInfo()` – obtain user profile details (language, name, guild, etc.).
|
|
122
|
-
- `plugin.getTranslator()` – lazily initialize the translator for manual i18n.
|
|
122
|
+
- `plugin.getTranslator()` – lazily initialize the translator for manual i18n. Translation keys must follow the dot notation (`section.subsection.key`); non-key strings are translated via AI and cached.
|
|
123
123
|
|
|
124
124
|
### Database Access
|
|
125
125
|
|
|
@@ -183,7 +183,7 @@ The controller handles topic generation, metadata, and completion tracking autom
|
|
|
183
183
|
Import additional helpers as needed:
|
|
184
184
|
|
|
185
185
|
- `AudioController` – high-level audio playback/recording utilities for non-React environments.
|
|
186
|
-
- `Translator` – encapsulated i18next integration for manual translation flows.
|
|
186
|
+
- `Translator` – encapsulated i18next integration for manual translation flows, with AI fallback for non-key strings.
|
|
187
187
|
- `difficultyConverter` – convert between textual and numeric difficulty levels.
|
|
188
188
|
- Type definitions for AI messages, shared content, triggers, accomplishments, and more.
|
|
189
189
|
|
|
@@ -37,6 +37,6 @@ export interface ObjectRequest {
|
|
|
37
37
|
instructions: string;
|
|
38
38
|
}
|
|
39
39
|
export declare function generateObject<T = any>(backendUrl: string, request: ObjectRequest, token: string): Promise<T>;
|
|
40
|
-
export type
|
|
41
|
-
export declare function streamObject(backendUrl: string, request: ObjectRequest,
|
|
40
|
+
export type OnStreamedObjectResult<T = any> = (result: T, isLoading: boolean) => void;
|
|
41
|
+
export declare function streamObject<T = any>(backendUrl: string, request: ObjectRequest, onResult: OnStreamedObjectResult<T>, token: string): Promise<void>;
|
|
42
42
|
export {};
|
|
@@ -21,56 +21,85 @@ export function generateObject(backendUrl, request, token) {
|
|
|
21
21
|
}).then((response) => response.json());
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
const tryParseJson = (value) => {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(value);
|
|
27
|
+
}
|
|
28
|
+
catch (_a) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const mergeStreamObject = (base, patch) => {
|
|
33
|
+
if (Array.isArray(patch)) {
|
|
34
|
+
return patch.map((item, index) => mergeStreamObject(base === null || base === void 0 ? void 0 : base[index], item));
|
|
35
|
+
}
|
|
36
|
+
if (patch && typeof patch === 'object') {
|
|
37
|
+
const result = base && typeof base === 'object' && !Array.isArray(base) ? Object.assign({}, base) : {};
|
|
38
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
39
|
+
result[key] = mergeStreamObject(result[key], value);
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
return patch;
|
|
44
|
+
};
|
|
45
|
+
const applyStreamChunk = (current, chunk) => {
|
|
46
|
+
if (!chunk || typeof chunk !== 'object') {
|
|
47
|
+
return { next: current, updated: false };
|
|
48
|
+
}
|
|
49
|
+
if (chunk.object && typeof chunk.object === 'object') {
|
|
50
|
+
return { next: chunk.object, updated: true };
|
|
51
|
+
}
|
|
52
|
+
if (chunk.delta && typeof chunk.delta === 'object') {
|
|
53
|
+
return { next: mergeStreamObject(current, chunk.delta), updated: true };
|
|
54
|
+
}
|
|
55
|
+
if (chunk.value && typeof chunk.value === 'object') {
|
|
56
|
+
return { next: mergeStreamObject(current, chunk.value), updated: true };
|
|
57
|
+
}
|
|
58
|
+
return { next: current, updated: false };
|
|
59
|
+
};
|
|
60
|
+
export function streamObject(backendUrl, request, onResult, token) {
|
|
25
61
|
return __awaiter(this, void 0, void 0, function* () {
|
|
26
|
-
const messageId = Math.random().toString(36).substring(3);
|
|
27
62
|
const response = yield fetch(`${backendUrl}/ai/llm-object`, {
|
|
28
63
|
method: 'POST',
|
|
29
64
|
body: JSON.stringify({
|
|
30
65
|
stream: true,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
66
|
+
tool: request.tool,
|
|
67
|
+
behaviour: request.behaviour,
|
|
68
|
+
instructions: request.instructions,
|
|
34
69
|
}),
|
|
35
70
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
36
71
|
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
console.error('Failed to stream object:', response.status, response.statusText);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
37
76
|
if (!response.body) {
|
|
38
77
|
console.error('No response body.');
|
|
39
78
|
return;
|
|
40
79
|
}
|
|
41
80
|
const reader = response.body.getReader();
|
|
42
81
|
const decoder = new TextDecoder('utf-8');
|
|
43
|
-
let content = '';
|
|
44
82
|
let done = false;
|
|
45
|
-
|
|
83
|
+
let currentObject = {};
|
|
46
84
|
while (!done) {
|
|
47
|
-
const { value } = yield reader.read();
|
|
85
|
+
const { value, done: readerDone } = yield reader.read();
|
|
48
86
|
if (value) {
|
|
49
87
|
const chunk = decoder.decode(value, { stream: true });
|
|
50
|
-
const lines = chunk.split('\n').filter((line) => line.trim()
|
|
88
|
+
const lines = chunk.split('\n').filter((line) => line.trim());
|
|
51
89
|
for (const line of lines) {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
// console.log("data: ", { line, data, command });
|
|
55
|
-
if (command === '0') {
|
|
56
|
-
content += data;
|
|
57
|
-
// console.log("AI response:", content);
|
|
58
|
-
//content \n\n should be real line break when message is displayed
|
|
59
|
-
onResponse(messageId, content.replace(/\\n/g, '\n').replace(/\\+"/g, '"'), false);
|
|
60
|
-
}
|
|
61
|
-
else if (command === 'd') {
|
|
62
|
-
// console.log("AI usage:", JSON.parse(line.substring(2)));
|
|
90
|
+
const dataStr = line.substring(5).trim();
|
|
91
|
+
if (dataStr === '[DONE]') {
|
|
63
92
|
done = true;
|
|
64
93
|
break;
|
|
65
94
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// console.log("tools", tools);
|
|
69
|
-
toolInvocations.push(JSON.parse(line.substring(2)));
|
|
70
|
-
}
|
|
95
|
+
currentObject = JSON.parse(dataStr);
|
|
96
|
+
onResult(currentObject, true);
|
|
71
97
|
}
|
|
72
98
|
}
|
|
99
|
+
if (readerDone) {
|
|
100
|
+
done = true;
|
|
101
|
+
}
|
|
73
102
|
}
|
|
74
|
-
|
|
103
|
+
onResult(currentObject, false);
|
|
75
104
|
});
|
|
76
105
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { ThirdPartyModule, TOptions } from 'i18next';
|
|
2
|
+
import { ObjectRequest } from './ObjectController';
|
|
3
|
+
import { AIModule } from '../plugin/module/AIModule';
|
|
4
|
+
export type AIObjectGenerator = <T>(request: ObjectRequest) => Promise<T>;
|
|
2
5
|
/**
|
|
3
6
|
* Translator class for handling internationalization
|
|
4
7
|
*/
|
|
@@ -8,7 +11,10 @@ export declare class Translator {
|
|
|
8
11
|
private initializationPromise;
|
|
9
12
|
private i18n;
|
|
10
13
|
private translationUrl;
|
|
11
|
-
|
|
14
|
+
private ai;
|
|
15
|
+
private aiTranslationCache;
|
|
16
|
+
private aiTranslationInFlight;
|
|
17
|
+
constructor(initialLanguage: string, translationUrl: string, ai: AIModule);
|
|
12
18
|
/**
|
|
13
19
|
* Initialize translator with user's language
|
|
14
20
|
* @param userLanguage - Language code from user info
|
|
@@ -23,8 +29,8 @@ export declare class Translator {
|
|
|
23
29
|
*/
|
|
24
30
|
private fetchTranslations;
|
|
25
31
|
/**
|
|
26
|
-
* Get translation for a key
|
|
27
|
-
* @param key - Translation key
|
|
32
|
+
* Get translation for a key or freeform text. If the key is not a valid translation key, the freeform text is translated using AI and cached.
|
|
33
|
+
* @param key - Translation key or freeform text
|
|
28
34
|
* @param options - Translation options
|
|
29
35
|
* @returns Translated string
|
|
30
36
|
*/
|
|
@@ -37,4 +43,7 @@ export declare class Translator {
|
|
|
37
43
|
* Check if translator is initialized
|
|
38
44
|
*/
|
|
39
45
|
isReady(): boolean;
|
|
46
|
+
private isTranslationKey;
|
|
47
|
+
private translateFreeformText;
|
|
48
|
+
private fetchAiTranslation;
|
|
40
49
|
}
|
|
@@ -12,11 +12,14 @@ import { createInstance } from 'i18next';
|
|
|
12
12
|
* Translator class for handling internationalization
|
|
13
13
|
*/
|
|
14
14
|
export class Translator {
|
|
15
|
-
constructor(initialLanguage, translationUrl) {
|
|
15
|
+
constructor(initialLanguage, translationUrl, ai) {
|
|
16
|
+
this.aiTranslationCache = new Map();
|
|
17
|
+
this.aiTranslationInFlight = new Set();
|
|
16
18
|
this.currentLanguage = initialLanguage;
|
|
17
19
|
this.initializationState = 'not-inited';
|
|
18
20
|
this.initializationPromise = null;
|
|
19
21
|
this.translationUrl = translationUrl;
|
|
22
|
+
this.ai = ai;
|
|
20
23
|
}
|
|
21
24
|
/**
|
|
22
25
|
* Initialize translator with user's language
|
|
@@ -102,12 +105,15 @@ export class Translator {
|
|
|
102
105
|
});
|
|
103
106
|
}
|
|
104
107
|
/**
|
|
105
|
-
* Get translation for a key
|
|
106
|
-
* @param key - Translation key
|
|
108
|
+
* Get translation for a key or freeform text. If the key is not a valid translation key, the freeform text is translated using AI and cached.
|
|
109
|
+
* @param key - Translation key or freeform text
|
|
107
110
|
* @param options - Translation options
|
|
108
111
|
* @returns Translated string
|
|
109
112
|
*/
|
|
110
113
|
t(key, options) {
|
|
114
|
+
if (!this.isTranslationKey(key)) {
|
|
115
|
+
return this.translateFreeformText(key);
|
|
116
|
+
}
|
|
111
117
|
if (!this.i18n) {
|
|
112
118
|
throw new Error('Translator is not initialized');
|
|
113
119
|
}
|
|
@@ -125,4 +131,45 @@ export class Translator {
|
|
|
125
131
|
isReady() {
|
|
126
132
|
return this.initializationState === 'finished';
|
|
127
133
|
}
|
|
134
|
+
isTranslationKey(key) {
|
|
135
|
+
return /^[^\s.]+(\.[^\s.]+)+$/.test(key);
|
|
136
|
+
}
|
|
137
|
+
translateFreeformText(text) {
|
|
138
|
+
const cached = this.aiTranslationCache.get(text);
|
|
139
|
+
if (cached)
|
|
140
|
+
return cached;
|
|
141
|
+
if (!this.ai || this.aiTranslationInFlight.has(text)) {
|
|
142
|
+
return text;
|
|
143
|
+
}
|
|
144
|
+
this.aiTranslationInFlight.add(text);
|
|
145
|
+
void this.fetchAiTranslation(text).finally(() => {
|
|
146
|
+
this.aiTranslationInFlight.delete(text);
|
|
147
|
+
});
|
|
148
|
+
return text;
|
|
149
|
+
}
|
|
150
|
+
fetchAiTranslation(text) {
|
|
151
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
152
|
+
try {
|
|
153
|
+
if (!this.ai)
|
|
154
|
+
return;
|
|
155
|
+
const response = yield this.ai.getObject({
|
|
156
|
+
behaviour: 'You are a translation engine. Return only the translated text.',
|
|
157
|
+
instructions: `Translate the following text into ${this.currentLanguage}: ${text}`,
|
|
158
|
+
tool: {
|
|
159
|
+
translation: {
|
|
160
|
+
type: 'string',
|
|
161
|
+
description: `The translation of the input text into ${this.currentLanguage}.`,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
const translation = response === null || response === void 0 ? void 0 : response.translation;
|
|
166
|
+
if (translation) {
|
|
167
|
+
this.aiTranslationCache.set(text, translation);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
console.warn('Failed to translate freeform text:', { text, error });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
128
175
|
}
|
|
@@ -80,7 +80,7 @@ export class RimoriCommunicationHandler {
|
|
|
80
80
|
// Listen for updates from rimori-main (data changes, token refresh, etc.)
|
|
81
81
|
// Topic format: {pluginId}.supabase.triggerUpdate
|
|
82
82
|
EventBus.on(`${this.pluginId}.supabase.triggerUpdate`, (ev) => {
|
|
83
|
-
console.log('[RimoriCommunicationHandler] Received update from rimori-main', ev.data);
|
|
83
|
+
// console.log('[RimoriCommunicationHandler] Received update from rimori-main', ev.data);
|
|
84
84
|
this.handleRimoriInfoUpdate(ev.data);
|
|
85
85
|
});
|
|
86
86
|
// Mark MessageChannel as ready and process pending requests
|
|
@@ -211,7 +211,7 @@ export class RimoriCommunicationHandler {
|
|
|
211
211
|
*/
|
|
212
212
|
handleRimoriInfoUpdate(newInfo) {
|
|
213
213
|
if (JSON.stringify(this.rimoriInfo) === JSON.stringify(newInfo)) {
|
|
214
|
-
console.log('[RimoriCommunicationHandler] RimoriInfo update is the same as the cached info, skipping update');
|
|
214
|
+
// console.log('[RimoriCommunicationHandler] RimoriInfo update is the same as the cached info, skipping update');
|
|
215
215
|
return;
|
|
216
216
|
}
|
|
217
217
|
// Update cached rimoriInfo
|
|
@@ -32,7 +32,7 @@ export class RimoriClient {
|
|
|
32
32
|
this.ai = new AIModule(controller, info);
|
|
33
33
|
this.event = new EventModule(info.pluginId);
|
|
34
34
|
this.db = new DbModule(supabase, controller, info);
|
|
35
|
-
this.plugin = new PluginModule(supabase, controller, info);
|
|
35
|
+
this.plugin = new PluginModule(supabase, controller, info, this.ai);
|
|
36
36
|
this.exercise = new ExerciseModule(supabase, controller, info, this.event);
|
|
37
37
|
controller.onUpdate((updatedInfo) => {
|
|
38
38
|
this.rimoriInfo = updatedInfo;
|
|
@@ -46,4 +46,10 @@ export declare class AIModule {
|
|
|
46
46
|
* @returns The generated object.
|
|
47
47
|
*/
|
|
48
48
|
getObject<T = any>(request: ObjectRequest): Promise<T>;
|
|
49
|
+
/**
|
|
50
|
+
* Generate a streamed structured object from a request using AI.
|
|
51
|
+
* @param request The object generation request.
|
|
52
|
+
* @param onResult Callback for each result chunk.
|
|
53
|
+
*/
|
|
54
|
+
getStreamedObject<T = any>(request: ObjectRequest, onResult: (result: T, isLoading: boolean) => void): Promise<void>;
|
|
49
55
|
}
|
|
@@ -8,7 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import { generateText, streamChatGPT } from '../../controller/AIController';
|
|
11
|
-
import { generateObject } from '../../controller/ObjectController';
|
|
11
|
+
import { generateObject, streamObject } from '../../controller/ObjectController';
|
|
12
12
|
import { getSTTResponse, getTTSResponse } from '../../controller/VoiceController';
|
|
13
13
|
/**
|
|
14
14
|
* Controller for AI-related operations.
|
|
@@ -78,4 +78,14 @@ export class AIModule {
|
|
|
78
78
|
return generateObject(this.backendUrl, request, this.token);
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Generate a streamed structured object from a request using AI.
|
|
83
|
+
* @param request The object generation request.
|
|
84
|
+
* @param onResult Callback for each result chunk.
|
|
85
|
+
*/
|
|
86
|
+
getStreamedObject(request, onResult) {
|
|
87
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
88
|
+
return streamObject(this.backendUrl, request, onResult, this.token);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
81
91
|
}
|
|
@@ -3,6 +3,7 @@ import { RimoriCommunicationHandler, RimoriInfo } from '../CommunicationHandler'
|
|
|
3
3
|
import { Translator } from '../../controller/TranslationController';
|
|
4
4
|
import { ActivePlugin, Plugin } from '../../fromRimori/PluginTypes';
|
|
5
5
|
import { SupabaseClient } from '../CommunicationHandler';
|
|
6
|
+
import { AIModule } from './AIModule';
|
|
6
7
|
export type Theme = 'light' | 'dark' | 'system';
|
|
7
8
|
export type ApplicationMode = 'main' | 'sidebar' | 'settings';
|
|
8
9
|
/**
|
|
@@ -22,7 +23,7 @@ export declare class PluginModule {
|
|
|
22
23
|
releaseChannel: string;
|
|
23
24
|
applicationMode: ApplicationMode;
|
|
24
25
|
theme: Theme;
|
|
25
|
-
constructor(supabase: SupabaseClient, communicationHandler: RimoriCommunicationHandler, info: RimoriInfo);
|
|
26
|
+
constructor(supabase: SupabaseClient, communicationHandler: RimoriCommunicationHandler, info: RimoriInfo, ai: AIModule);
|
|
26
27
|
/**
|
|
27
28
|
* Set the settings for the plugin.
|
|
28
29
|
* @param settings The settings to set.
|
|
@@ -14,14 +14,14 @@ import { Translator } from '../../controller/TranslationController';
|
|
|
14
14
|
* Provides access to plugin settings, user info, and plugin information.
|
|
15
15
|
*/
|
|
16
16
|
export class PluginModule {
|
|
17
|
-
constructor(supabase, communicationHandler, info) {
|
|
17
|
+
constructor(supabase, communicationHandler, info, ai) {
|
|
18
18
|
this.rimoriInfo = info;
|
|
19
19
|
this.communicationHandler = communicationHandler;
|
|
20
20
|
this.pluginId = info.pluginId;
|
|
21
21
|
this.releaseChannel = info.releaseChannel;
|
|
22
22
|
this.settingsController = new SettingsController(supabase, info.pluginId, info.guild);
|
|
23
23
|
const currentPlugin = info.installedPlugins.find((plugin) => plugin.id === info.pluginId);
|
|
24
|
-
this.translator = new Translator(info.interfaceLanguage, (currentPlugin === null || currentPlugin === void 0 ? void 0 : currentPlugin.endpoint) || '');
|
|
24
|
+
this.translator = new Translator(info.interfaceLanguage, (currentPlugin === null || currentPlugin === void 0 ? void 0 : currentPlugin.endpoint) || '', ai);
|
|
25
25
|
this.communicationHandler.onUpdate((updatedInfo) => (this.rimoriInfo = updatedInfo));
|
|
26
26
|
this.applicationMode = this.communicationHandler.getQueryParam('applicationMode');
|
|
27
27
|
this.theme = this.communicationHandler.getQueryParam('rm_theme') || 'light';
|
|
@@ -2,3 +2,11 @@ export type LanguageLevel = 'Pre-A1' | 'A1' | 'A2' | 'B1' | 'B2' | 'C1' | 'C2' |
|
|
|
2
2
|
export declare function getDifficultyLevel(difficulty: LanguageLevel): number;
|
|
3
3
|
export declare function getDifficultyLabel(difficulty: number): LanguageLevel;
|
|
4
4
|
export declare function getNeighborDifficultyLevel(difficulty: LanguageLevel, difficultyAdjustment: number): LanguageLevel;
|
|
5
|
+
/**
|
|
6
|
+
* Compares two LanguageLevel values to determine their relative order.
|
|
7
|
+
* Returns:
|
|
8
|
+
* - negative number if level1 < level2
|
|
9
|
+
* - 0 if level1 === level2
|
|
10
|
+
* - positive number if level1 > level2
|
|
11
|
+
*/
|
|
12
|
+
export declare function compareLanguageLevels(level1: LanguageLevel, level2: LanguageLevel): number;
|
|
@@ -8,3 +8,13 @@ export function getDifficultyLabel(difficulty) {
|
|
|
8
8
|
export function getNeighborDifficultyLevel(difficulty, difficultyAdjustment) {
|
|
9
9
|
return getDifficultyLabel(getDifficultyLevel(difficulty) + difficultyAdjustment - 1);
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Compares two LanguageLevel values to determine their relative order.
|
|
13
|
+
* Returns:
|
|
14
|
+
* - negative number if level1 < level2
|
|
15
|
+
* - 0 if level1 === level2
|
|
16
|
+
* - positive number if level1 > level2
|
|
17
|
+
*/
|
|
18
|
+
export function compareLanguageLevels(level1, level2) {
|
|
19
|
+
return getDifficultyLevel(level1) - getDifficultyLevel(level2);
|
|
20
|
+
}
|