@rimori/client 1.2.0 → 1.3.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 +61 -18
- package/dist/cli/scripts/init/dev-registration.js +0 -1
- package/dist/cli/scripts/init/main.d.ts +1 -1
- package/dist/cli/scripts/init/main.js +1 -0
- package/dist/components/LoggerExample.d.ts +6 -0
- package/dist/components/LoggerExample.js +79 -0
- package/dist/components/ai/Assistant.js +2 -2
- package/dist/components/ai/Avatar.js +2 -2
- package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.js +41 -32
- package/dist/components/audio/Playbutton.js +2 -2
- package/dist/components/components/ContextMenu.js +48 -9
- package/dist/core/controller/AIController.js +202 -69
- package/dist/core/controller/AudioController.d.ts +0 -0
- package/dist/core/controller/AudioController.js +1 -0
- package/dist/core/controller/ObjectController.d.ts +2 -2
- package/dist/core/controller/ObjectController.js +8 -8
- package/dist/core/controller/SettingsController.d.ts +16 -0
- package/dist/core/controller/SharedContentController.d.ts +30 -2
- package/dist/core/controller/SharedContentController.js +74 -23
- package/dist/core/controller/VoiceController.d.ts +2 -3
- package/dist/core/controller/VoiceController.js +11 -4
- package/dist/core/core.d.ts +1 -0
- package/dist/fromRimori/EventBus.js +1 -1
- package/dist/fromRimori/PluginTypes.d.ts +7 -4
- package/dist/hooks/UseChatHook.js +6 -4
- package/dist/hooks/UseLogger.d.ts +30 -0
- package/dist/hooks/UseLogger.js +122 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plugin/AudioController.d.ts +37 -0
- package/dist/plugin/AudioController.js +68 -0
- package/dist/plugin/Logger.d.ts +68 -0
- package/dist/plugin/Logger.js +256 -0
- package/dist/plugin/LoggerExample.d.ts +16 -0
- package/dist/plugin/LoggerExample.js +140 -0
- package/dist/plugin/PluginController.d.ts +15 -3
- package/dist/plugin/PluginController.js +162 -39
- package/dist/plugin/RimoriClient.d.ts +55 -13
- package/dist/plugin/RimoriClient.js +60 -23
- package/dist/plugin/StandaloneClient.d.ts +1 -0
- package/dist/plugin/StandaloneClient.js +16 -5
- package/dist/plugin/ThemeSetter.d.ts +2 -2
- package/dist/plugin/ThemeSetter.js +8 -5
- package/dist/providers/PluginProvider.d.ts +1 -1
- package/dist/providers/PluginProvider.js +36 -10
- package/dist/utils/audioFormats.d.ts +26 -0
- package/dist/utils/audioFormats.js +67 -0
- package/dist/worker/WorkerSetup.d.ts +3 -2
- package/dist/worker/WorkerSetup.js +22 -67
- package/package.json +7 -6
- package/src/cli/scripts/init/dev-registration.ts +0 -1
- package/src/cli/scripts/init/main.ts +1 -0
- package/src/components/ai/Assistant.tsx +2 -2
- package/src/components/ai/Avatar.tsx +2 -2
- package/src/components/ai/EmbeddedAssistent/VoiceRecoder.tsx +39 -32
- package/src/components/audio/Playbutton.tsx +2 -2
- package/src/components/components/ContextMenu.tsx +53 -9
- package/src/core/controller/AIController.ts +236 -75
- package/src/core/controller/ObjectController.ts +8 -8
- package/src/core/controller/SettingsController.ts +16 -0
- package/src/core/controller/SharedContentController.ts +87 -25
- package/src/core/controller/VoiceController.ts +24 -19
- package/src/core/core.ts +1 -0
- package/src/fromRimori/EventBus.ts +1 -1
- package/src/fromRimori/PluginTypes.ts +6 -4
- package/src/hooks/UseChatHook.ts +6 -4
- package/src/index.ts +1 -0
- package/src/plugin/AudioController.ts +58 -0
- package/src/plugin/Logger.ts +324 -0
- package/src/plugin/PluginController.ts +171 -43
- package/src/plugin/RimoriClient.ts +95 -30
- package/src/plugin/StandaloneClient.ts +22 -6
- package/src/plugin/ThemeSetter.ts +8 -5
- package/src/providers/PluginProvider.tsx +40 -10
- package/src/worker/WorkerSetup.ts +14 -63
|
@@ -22,7 +22,11 @@ export class SharedContentController {
|
|
|
22
22
|
* @param contentType - The type of content to fetch.
|
|
23
23
|
* @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.
|
|
24
24
|
* @param filter - An optional filter to apply to the query.
|
|
25
|
-
* @param
|
|
25
|
+
* @param options - Optional options.
|
|
26
|
+
* @param options.privateTopic - If the topic should be private and only be visible to the user.
|
|
27
|
+
* @param options.skipDbSave - If true, do not persist a newly generated content to the DB (default false).
|
|
28
|
+
* @param options.alwaysGenerateNew - If true, always generate a new content even if there is already a content with the same filter.
|
|
29
|
+
* @param options.excludeIds - Optional list of shared_content ids to exclude from selection.
|
|
26
30
|
* @returns The new shared content.
|
|
27
31
|
*/
|
|
28
32
|
public async getNewSharedContent<T>(
|
|
@@ -30,56 +34,66 @@ export class SharedContentController {
|
|
|
30
34
|
generatorInstructions: SharedContentObjectRequest,
|
|
31
35
|
//this filter is there if the content should be filtered additionally by a column and value
|
|
32
36
|
filter?: SharedContentFilter,
|
|
33
|
-
privateTopic?: boolean,
|
|
37
|
+
options?: { privateTopic?: boolean, skipDbSave?: boolean, alwaysGenerateNew?: boolean, excludeIds?: string[] },
|
|
34
38
|
): Promise<SharedContent<T>> {
|
|
35
|
-
|
|
36
|
-
.select("*, scc:shared_content_completed(id)")
|
|
39
|
+
let query = this.supabase.from("shared_content")
|
|
40
|
+
.select("*, scc:shared_content_completed(id, state)")
|
|
37
41
|
.eq('content_type', contentType)
|
|
38
|
-
.
|
|
39
|
-
.is('deleted_at', null)
|
|
40
|
-
|
|
42
|
+
.not('scc.state', 'in', '("completed","ongoing","hidden")')
|
|
43
|
+
.is('deleted_at', null);
|
|
44
|
+
|
|
45
|
+
if (options?.excludeIds && options.excludeIds.length > 0) {
|
|
46
|
+
const excludeIds = options.excludeIds.filter((id) => !id.startsWith('internal-temp-id-'));
|
|
47
|
+
// Supabase expects raw PostgREST syntax like '("id1","id2")'.
|
|
48
|
+
const excludeList = `(${excludeIds.map((id) => `"${id}"`).join(',')})`;
|
|
49
|
+
query = query.not('id', 'in', excludeList);
|
|
50
|
+
}
|
|
41
51
|
|
|
42
52
|
if (filter) {
|
|
43
53
|
query.contains('data', filter);
|
|
44
54
|
}
|
|
45
55
|
|
|
46
|
-
const { data: newAssignments, error } = await query;
|
|
56
|
+
const { data: newAssignments, error } = await query.limit(30);
|
|
47
57
|
|
|
48
58
|
if (error) {
|
|
49
59
|
console.error('error fetching new assignments:', error);
|
|
50
60
|
throw new Error('error fetching new assignments');
|
|
51
61
|
}
|
|
52
62
|
|
|
53
|
-
console.log('newAssignments:', newAssignments);
|
|
63
|
+
// console.log('newAssignments:', newAssignments);
|
|
54
64
|
|
|
55
|
-
if (newAssignments.length > 0) {
|
|
65
|
+
if (!(options?.alwaysGenerateNew) && newAssignments.length > 0) {
|
|
56
66
|
const index = Math.floor(Math.random() * newAssignments.length);
|
|
57
67
|
return newAssignments[index];
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
const fullInstructions = await this.getGeneratorInstructions(contentType, generatorInstructions, filter);
|
|
62
|
-
|
|
63
|
-
console.log('fullInstructions:', fullInstructions);
|
|
64
|
-
|
|
65
|
-
const instructions = await this.rimoriClient.ai.getObject(fullInstructions);
|
|
70
|
+
const instructions = await this.generateNewAssignment(contentType, generatorInstructions, filter);
|
|
66
71
|
|
|
67
72
|
console.log('instructions:', instructions);
|
|
68
73
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
//create the shared content object
|
|
75
|
+
const data: SharedContent<T> = {
|
|
76
|
+
id: "internal-temp-id-" + Math.random().toString(36).substring(2, 15),
|
|
77
|
+
contentType,
|
|
72
78
|
title: instructions.title,
|
|
73
79
|
keywords: instructions.keywords.map(({ text }: { text: string }) => text),
|
|
74
80
|
data: { ...instructions, title: undefined, keywords: undefined, ...generatorInstructions.fixedProperties },
|
|
75
|
-
|
|
81
|
+
privateTopic: options?.privateTopic,
|
|
82
|
+
}
|
|
76
83
|
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
throw new Error('error inserting new assignment');
|
|
84
|
+
if (options?.skipDbSave) {
|
|
85
|
+
return data;
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
return
|
|
88
|
+
return await this.createSharedContent(data);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private async generateNewAssignment(contentType: string, generatorInstructions: SharedContentObjectRequest, filter?: SharedContentFilter): Promise<any> {
|
|
92
|
+
const fullInstructions = await this.getGeneratorInstructions(contentType, generatorInstructions, filter);
|
|
93
|
+
|
|
94
|
+
console.log('fullInstructions:', fullInstructions);
|
|
95
|
+
|
|
96
|
+
return await this.rimoriClient.ai.getObject(fullInstructions);
|
|
83
97
|
}
|
|
84
98
|
|
|
85
99
|
private async getGeneratorInstructions(contentType: string, generatorInstructions: ObjectRequest, filter?: SharedContentFilter): Promise<ObjectRequest> {
|
|
@@ -129,7 +143,55 @@ export class SharedContentController {
|
|
|
129
143
|
}
|
|
130
144
|
|
|
131
145
|
public async completeSharedContent(contentType: string, assignmentId: string) {
|
|
132
|
-
|
|
146
|
+
// Idempotent completion: upsert on (id, user_id) so repeated calls don't fail
|
|
147
|
+
const { error } = await this.supabase
|
|
148
|
+
.from("shared_content_completed")
|
|
149
|
+
.upsert({ content_type: contentType, id: assignmentId } as any, { onConflict: 'id' });
|
|
150
|
+
|
|
151
|
+
if (error) {
|
|
152
|
+
console.error('error completing shared content:', error);
|
|
153
|
+
throw new Error('error completing shared content');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Update state details for a shared content entry in shared_content_completed.
|
|
159
|
+
* Assumes table has columns: state ('completed'|'ongoing'|'hidden'), reaction ('liked'|'disliked'|null), bookmarked boolean.
|
|
160
|
+
* Upserts per (id, content_type, user).
|
|
161
|
+
* @param param
|
|
162
|
+
* @param param.contentType - The content type.
|
|
163
|
+
* @param param.id - The shared content id.
|
|
164
|
+
* @param param.state - The state to set.
|
|
165
|
+
* @param param.reaction - Optional reaction.
|
|
166
|
+
* @param param.bookmarked - Optional bookmark flag.
|
|
167
|
+
*/
|
|
168
|
+
public async updateSharedContentState({
|
|
169
|
+
contentType,
|
|
170
|
+
id,
|
|
171
|
+
state,
|
|
172
|
+
reaction,
|
|
173
|
+
bookmarked,
|
|
174
|
+
}: {
|
|
175
|
+
contentType: string
|
|
176
|
+
id: string
|
|
177
|
+
state?: 'completed' | 'ongoing' | 'hidden'
|
|
178
|
+
reaction?: 'liked' | 'disliked' | null
|
|
179
|
+
bookmarked?: boolean
|
|
180
|
+
}): Promise<void> {
|
|
181
|
+
const payload: Record<string, unknown> = { content_type: contentType, id };
|
|
182
|
+
if (state !== undefined) payload.state = state;
|
|
183
|
+
if (reaction !== undefined) payload.reaction = reaction;
|
|
184
|
+
if (bookmarked !== undefined) payload.bookmarked = bookmarked;
|
|
185
|
+
|
|
186
|
+
// Prefer upsert, fall back to insert/update if upsert not allowed
|
|
187
|
+
const { error } = await this.supabase
|
|
188
|
+
.from('shared_content_completed')
|
|
189
|
+
.upsert(payload as any, { onConflict: 'id' });
|
|
190
|
+
|
|
191
|
+
if (error) {
|
|
192
|
+
console.error('error updating shared content state:', error);
|
|
193
|
+
throw new Error('error updating shared content state');
|
|
194
|
+
}
|
|
133
195
|
}
|
|
134
196
|
|
|
135
197
|
/**
|
|
@@ -1,26 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
export async function getSTTResponse(backendUrl: string, audio: Blob, token: string) {
|
|
2
|
+
const formData = new FormData();
|
|
3
|
+
formData.append('file', audio);
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
return await fetch(`${backendUrl}/voice/stt`, {
|
|
6
|
+
method: 'POST',
|
|
7
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
8
|
+
body: formData,
|
|
9
|
+
}).then(r => r.json()).then(r => {
|
|
10
|
+
// console.log("STT response: ", r);
|
|
11
|
+
return r.text;
|
|
12
|
+
});
|
|
8
13
|
}
|
|
9
14
|
|
|
10
|
-
export async function getTTSResponse(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
export async function getTTSResponse(backendUrl: string, request: TTSRequest, token: string) {
|
|
16
|
+
return await fetch(`${backendUrl}/voice/tts`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: {
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
'Authorization': `Bearer ${token}`
|
|
21
|
+
},
|
|
22
|
+
body: JSON.stringify(request),
|
|
23
|
+
}).then(r => r.blob());
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
interface TTSRequest {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
input: string;
|
|
28
|
+
voice: string;
|
|
29
|
+
speed: number;
|
|
30
|
+
language?: string;
|
|
26
31
|
}
|
package/src/core/core.ts
CHANGED
|
@@ -12,4 +12,5 @@ export { SharedContent } from "./controller/SharedContentController";
|
|
|
12
12
|
export { Message, OnLLMResponse, ToolInvocation } from "./controller/AIController";
|
|
13
13
|
export { MacroAccomplishmentPayload, MicroAccomplishmentPayload } from "../plugin/AccomplishmentHandler";
|
|
14
14
|
export { Tool } from "../fromRimori/PluginTypes";
|
|
15
|
+
export { SharedContentObjectRequest } from "./controller/SharedContentController";
|
|
15
16
|
|
|
@@ -39,7 +39,7 @@ export class EventBusHandler {
|
|
|
39
39
|
private listeners: Map<string, Set<Listeners<EventPayload>>> = new Map();
|
|
40
40
|
private responseResolvers: Map<number, (value: EventBusMessage<unknown>) => void> = new Map();
|
|
41
41
|
private static instance: EventBusHandler | null = null;
|
|
42
|
-
private debugEnabled: boolean =
|
|
42
|
+
private debugEnabled: boolean = false;
|
|
43
43
|
private evName: string = "";
|
|
44
44
|
|
|
45
45
|
private constructor() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// whole configuration of a plugin (from the database)
|
|
2
|
-
export type Plugin = Omit<RimoriPluginConfig
|
|
2
|
+
export type Plugin<T extends {} = {}> = Omit<RimoriPluginConfig<T>, 'context_menu_actions'> & {
|
|
3
3
|
version: string;
|
|
4
4
|
endpoint: string;
|
|
5
5
|
assetEndpoint: string;
|
|
@@ -7,6 +7,8 @@ export type Plugin = Omit<RimoriPluginConfig, 'context_menu_actions'> & {
|
|
|
7
7
|
release_channel: "alpha" | "beta" | "stable";
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export type ActivePlugin = Plugin<{ active?: boolean }>
|
|
11
|
+
|
|
10
12
|
// browsable page of a plugin
|
|
11
13
|
export interface PluginPage {
|
|
12
14
|
id: string;
|
|
@@ -70,7 +72,7 @@ export interface ContextMenuAction {
|
|
|
70
72
|
* Rimori plugin structure representing the complete configuration
|
|
71
73
|
* of a Rimori plugin with all metadata and configuration options.
|
|
72
74
|
*/
|
|
73
|
-
export interface RimoriPluginConfig {
|
|
75
|
+
export interface RimoriPluginConfig<T extends {} = {}> {
|
|
74
76
|
id: string;
|
|
75
77
|
/**
|
|
76
78
|
* Basic information about the plugin including branding and core details.
|
|
@@ -92,9 +94,9 @@ export interface RimoriPluginConfig {
|
|
|
92
94
|
/** Optional external URL where the plugin is hosted instead of the default CDN */
|
|
93
95
|
external_hosted_url?: string;
|
|
94
96
|
/** Array of main plugin pages that appear in the application's main navigation (can be disabled using the 'show' flag) */
|
|
95
|
-
main: PluginPage[];
|
|
97
|
+
main: (PluginPage & T)[];
|
|
96
98
|
/** Array of sidebar pages that appear in the sidebar for quick access (can be disabled using the 'show' flag) */
|
|
97
|
-
sidebar: SidebarPage[];
|
|
99
|
+
sidebar: (SidebarPage & T)[];
|
|
98
100
|
/** Optional path to the plugin's settings/configuration page */
|
|
99
101
|
settings?: string;
|
|
100
102
|
/** Optional array of event topics the plugin pages can listen to for cross-plugin communication */
|
package/src/hooks/UseChatHook.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Tool } from "../fromRimori/PluginTypes";
|
|
3
|
-
import {
|
|
3
|
+
import { useRimori } from "../providers/PluginProvider";
|
|
4
4
|
import { Message, ToolInvocation } from "../core/controller/AIController";
|
|
5
5
|
|
|
6
6
|
export function useChat(tools?: Tool[]) {
|
|
7
7
|
const [messages, setMessages] = React.useState<Message[]>([]);
|
|
8
8
|
const [isLoading, setIsLoading] = React.useState(false);
|
|
9
|
-
const { ai } =
|
|
9
|
+
const { ai } = useRimori();
|
|
10
10
|
|
|
11
11
|
const append = (appendMessages: Message[]) => {
|
|
12
|
-
|
|
12
|
+
const allMessages = [...messages, ...appendMessages];
|
|
13
|
+
setMessages(allMessages);
|
|
14
|
+
ai.getSteamedText(allMessages, (id, message, finished: boolean, toolInvocations?: ToolInvocation[]) => {
|
|
13
15
|
const lastMessage = messages[messages.length - 1];
|
|
14
16
|
setIsLoading(!finished);
|
|
15
17
|
|
|
@@ -17,7 +19,7 @@ export function useChat(tools?: Tool[]) {
|
|
|
17
19
|
lastMessage.content = message;
|
|
18
20
|
setMessages([...messages, lastMessage]);
|
|
19
21
|
} else {
|
|
20
|
-
setMessages([...
|
|
22
|
+
setMessages([...allMessages, { id, role: 'assistant', content: message, toolCalls: toolInvocations }]);
|
|
21
23
|
}
|
|
22
24
|
}, tools);
|
|
23
25
|
};
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { EventBus } from "../fromRimori/EventBus";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AudioController is a class that provides methods to record audio. It is a wrapper around the Capacitor Voice Recorder plugin. For more information, see https://github.com/tchvu3/capacitor-voice-recorder.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const audioController = new AudioController();
|
|
8
|
+
* await audioController.startRecording();
|
|
9
|
+
*/
|
|
10
|
+
export class AudioController {
|
|
11
|
+
private pluginId: string;
|
|
12
|
+
|
|
13
|
+
constructor(pluginId: string) {
|
|
14
|
+
this.pluginId = pluginId;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Start the recording.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const audioController = new AudioController();
|
|
22
|
+
* await audioController.startRecording();
|
|
23
|
+
* @returns void
|
|
24
|
+
*/
|
|
25
|
+
public async startRecording(): Promise<void> {
|
|
26
|
+
EventBus.emit(this.pluginId, "global.microphone.triggerStartRecording");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Stop the recording and return the audio data.
|
|
31
|
+
* @returns The audio data.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* const audioRef = new Audio(`data:${mimeType};base64,${base64Sound}`)
|
|
35
|
+
* audioRef.oncanplaythrough = () => audioRef.play()
|
|
36
|
+
* audioRef.load()
|
|
37
|
+
*/
|
|
38
|
+
public async stopRecording(): Promise<{ recording: Blob, msDuration: number, mimeType: string }> {
|
|
39
|
+
const result = await EventBus.request<{ recording: Blob, msDuration: number, mimeType: string }>(this.pluginId, "global.microphone.triggerStopRecording");
|
|
40
|
+
|
|
41
|
+
return result.data;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public async pauseRecording(): Promise<boolean> {
|
|
45
|
+
const result = await EventBus.request<boolean>(this.pluginId, "global.microphone.triggerPauseRecording");
|
|
46
|
+
return result.data;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public async resumeRecording(): Promise<boolean> {
|
|
50
|
+
const result = await EventBus.request<boolean>(this.pluginId, "global.microphone.triggerResumeRecording");
|
|
51
|
+
return result.data;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public async getCurrentStatus(): Promise<"RECORDING" | "PAUSED" | "NONE"> {
|
|
55
|
+
const result = await EventBus.request<"RECORDING" | "PAUSED" | "NONE">(this.pluginId, "global.microphone.triggerGetCurrentStatus");
|
|
56
|
+
return result.data;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { RimoriClient } from './RimoriClient';
|
|
2
|
+
import html2canvas from 'html2canvas';
|
|
3
|
+
|
|
4
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
5
|
+
|
|
6
|
+
interface LogEntry {
|
|
7
|
+
id: string;
|
|
8
|
+
timestamp: string;
|
|
9
|
+
level: LogLevel;
|
|
10
|
+
message: string;
|
|
11
|
+
data?: any;
|
|
12
|
+
context?: {
|
|
13
|
+
url: string;
|
|
14
|
+
userAgent: string;
|
|
15
|
+
browserInfo: BrowserInfo;
|
|
16
|
+
screenshot?: string;
|
|
17
|
+
mousePosition?: MousePosition;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface BrowserInfo {
|
|
22
|
+
userAgent: string;
|
|
23
|
+
language: string;
|
|
24
|
+
cookieEnabled: boolean;
|
|
25
|
+
onLine: boolean;
|
|
26
|
+
screenResolution: string;
|
|
27
|
+
windowSize: string;
|
|
28
|
+
timestamp: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface MousePosition {
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
timestamp: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Singleton Logger class for Rimori client plugins.
|
|
39
|
+
* Handles all logging levels, production filtering, and log transmission to Rimori.
|
|
40
|
+
* Overrides console methods globally for seamless integration.
|
|
41
|
+
*/
|
|
42
|
+
export class Logger {
|
|
43
|
+
private static instance: Logger;
|
|
44
|
+
private isProduction: boolean;
|
|
45
|
+
private logs: LogEntry[] = [];
|
|
46
|
+
private logIdCounter = 0;
|
|
47
|
+
private originalConsole: {
|
|
48
|
+
log: typeof console.log;
|
|
49
|
+
info: typeof console.info;
|
|
50
|
+
warn: typeof console.warn;
|
|
51
|
+
error: typeof console.error;
|
|
52
|
+
debug: typeof console.debug;
|
|
53
|
+
};
|
|
54
|
+
private mousePosition: MousePosition | null = null;
|
|
55
|
+
|
|
56
|
+
private constructor(rimori: RimoriClient, isProduction?: boolean) {
|
|
57
|
+
this.isProduction = this.validateIsProduction(isProduction);
|
|
58
|
+
|
|
59
|
+
// Store original console methods
|
|
60
|
+
this.originalConsole = {
|
|
61
|
+
log: console.log,
|
|
62
|
+
info: console.info,
|
|
63
|
+
warn: console.warn,
|
|
64
|
+
error: console.error,
|
|
65
|
+
debug: console.debug
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Override console methods globally
|
|
69
|
+
this.overrideConsoleMethods();
|
|
70
|
+
|
|
71
|
+
// Track mouse position
|
|
72
|
+
this.trackMousePosition();
|
|
73
|
+
|
|
74
|
+
// Expose logs to global scope for DevTools access
|
|
75
|
+
this.exposeToDevTools();
|
|
76
|
+
|
|
77
|
+
// Set up navigation clearing
|
|
78
|
+
this.setupNavigationClearing();
|
|
79
|
+
|
|
80
|
+
rimori.event.respond('logging.requestPluginLogs', async () => {
|
|
81
|
+
this.addLogEntry(await this.createLogEntry('info', 'Screenshot capture', undefined, true));
|
|
82
|
+
const logs = {
|
|
83
|
+
logs: this.logs,
|
|
84
|
+
pluginId: rimori.plugin.pluginId,
|
|
85
|
+
timestamp: new Date().toISOString()
|
|
86
|
+
}
|
|
87
|
+
this.logs = [];
|
|
88
|
+
this.logIdCounter = 0;
|
|
89
|
+
return logs;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Initialize the Logger singleton and override console methods globally.
|
|
95
|
+
* @param rimori - Rimori client instance
|
|
96
|
+
* @param isProduction - Whether the environment is production
|
|
97
|
+
* @returns Logger instance
|
|
98
|
+
*/
|
|
99
|
+
public static getInstance(rimori: RimoriClient, isProduction?: boolean): Logger {
|
|
100
|
+
if (!Logger.instance) {
|
|
101
|
+
Logger.instance = new Logger(rimori, isProduction);
|
|
102
|
+
}
|
|
103
|
+
return Logger.instance;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private validateIsProduction(isProduction?: boolean): boolean {
|
|
107
|
+
if (isProduction !== undefined) {
|
|
108
|
+
return isProduction;
|
|
109
|
+
}
|
|
110
|
+
if (typeof window !== 'undefined' && window.location.href) {
|
|
111
|
+
return !window.location.href.includes('localhost');
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Expose log access to global scope for DevTools console access.
|
|
117
|
+
*/
|
|
118
|
+
private exposeToDevTools(): void {
|
|
119
|
+
if (typeof window !== 'undefined') {
|
|
120
|
+
// Expose a global function to access logs from DevTools console
|
|
121
|
+
(window as any).getRimoriLogs = () => this.logs;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Set up navigation event listeners to clear logs on page changes.
|
|
127
|
+
*/
|
|
128
|
+
private setupNavigationClearing(): void {
|
|
129
|
+
if (typeof window === 'undefined' || typeof history === 'undefined') return;
|
|
130
|
+
|
|
131
|
+
// Clear logs on browser back/forward
|
|
132
|
+
window.addEventListener('popstate', () => this.logs = []);
|
|
133
|
+
|
|
134
|
+
// Override history methods to clear logs on programmatic navigation
|
|
135
|
+
const originalPushState = history.pushState;
|
|
136
|
+
const originalReplaceState = history.replaceState;
|
|
137
|
+
|
|
138
|
+
history.pushState = (...args) => {
|
|
139
|
+
originalPushState.apply(history, args);
|
|
140
|
+
this.logs = [];
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
history.replaceState = (...args) => {
|
|
144
|
+
originalReplaceState.apply(history, args);
|
|
145
|
+
this.logs = [];
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Listen for URL changes (works with React Router and other SPAs)
|
|
149
|
+
let currentUrl = window.location.href;
|
|
150
|
+
const checkUrlChange = () => {
|
|
151
|
+
if (window.location.href !== currentUrl) {
|
|
152
|
+
currentUrl = window.location.href;
|
|
153
|
+
this.logs = [];
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Check for URL changes periodically
|
|
158
|
+
setInterval(checkUrlChange, 100);
|
|
159
|
+
|
|
160
|
+
// Also listen for hash changes (for hash-based routing)
|
|
161
|
+
window.addEventListener('hashchange', () => this.logs = []);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Override console methods globally to capture all console calls.
|
|
166
|
+
*/
|
|
167
|
+
private overrideConsoleMethods(): void {
|
|
168
|
+
// Override console.log
|
|
169
|
+
console.log = (...args: any[]) => {
|
|
170
|
+
this.originalConsole.log(...args);
|
|
171
|
+
this.handleConsoleCall('info', args);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Override console.info
|
|
175
|
+
console.info = (...args: any[]) => {
|
|
176
|
+
this.originalConsole.info(...args);
|
|
177
|
+
this.handleConsoleCall('info', args);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Override console.warn
|
|
181
|
+
console.warn = (...args: any[]) => {
|
|
182
|
+
this.originalConsole.warn(...args);
|
|
183
|
+
this.handleConsoleCall('warn', args);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Override console.error
|
|
187
|
+
console.error = (...args: any[]) => {
|
|
188
|
+
this.originalConsole.error(...args);
|
|
189
|
+
this.handleConsoleCall('error', args);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Override console.debug
|
|
193
|
+
console.debug = (...args: any[]) => {
|
|
194
|
+
this.originalConsole.debug(...args);
|
|
195
|
+
this.handleConsoleCall('debug', args);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Track mouse position for screenshot context.
|
|
201
|
+
*/
|
|
202
|
+
private trackMousePosition(): void {
|
|
203
|
+
if (typeof window !== 'undefined') {
|
|
204
|
+
const updateMousePosition = (event: MouseEvent) => {
|
|
205
|
+
this.mousePosition = {
|
|
206
|
+
x: event.clientX,
|
|
207
|
+
y: event.clientY,
|
|
208
|
+
timestamp: new Date().toISOString()
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
window.addEventListener('mousemove', updateMousePosition);
|
|
213
|
+
window.addEventListener('click', updateMousePosition);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Handle console method calls and create log entries.
|
|
219
|
+
* @param level - Log level
|
|
220
|
+
* @param args - Console arguments
|
|
221
|
+
*/
|
|
222
|
+
private async handleConsoleCall(level: LogLevel, args: any[]): Promise<void> {
|
|
223
|
+
// Skip if this is a production log that shouldn't be stored
|
|
224
|
+
if (this.isProduction && (level === 'debug' || level === 'info')) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Convert console arguments to message and data
|
|
229
|
+
const message = args.map(arg =>
|
|
230
|
+
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
231
|
+
).join(' ');
|
|
232
|
+
|
|
233
|
+
const data = args.length > 1 ? args.slice(1) : undefined;
|
|
234
|
+
|
|
235
|
+
const entry = await this.createLogEntry(level, message, data);
|
|
236
|
+
this.addLogEntry(entry);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get browser and system information for debugging.
|
|
241
|
+
* @returns Object with browser and system information
|
|
242
|
+
*/
|
|
243
|
+
private getBrowserInfo(): BrowserInfo {
|
|
244
|
+
return {
|
|
245
|
+
userAgent: navigator.userAgent,
|
|
246
|
+
language: navigator.language,
|
|
247
|
+
cookieEnabled: navigator.cookieEnabled,
|
|
248
|
+
onLine: navigator.onLine,
|
|
249
|
+
screenResolution: `${screen.width}x${screen.height}`,
|
|
250
|
+
windowSize: `${window.innerWidth}x${window.innerHeight}`,
|
|
251
|
+
timestamp: new Date().toISOString()
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Capture a screenshot of the current page.
|
|
257
|
+
* @returns Promise resolving to base64 screenshot or null if failed
|
|
258
|
+
*/
|
|
259
|
+
private async captureScreenshot(): Promise<string | null> {
|
|
260
|
+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
261
|
+
const canvas = await html2canvas(document.body);
|
|
262
|
+
const screenshot = canvas.toDataURL('image/png');
|
|
263
|
+
// this.originalConsole.log("screenshot captured", screenshot)
|
|
264
|
+
return screenshot;
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Create a log entry with context information.
|
|
271
|
+
* @param level - Log level
|
|
272
|
+
* @param message - Log message
|
|
273
|
+
* @param data - Additional data
|
|
274
|
+
* @returns Log entry
|
|
275
|
+
*/
|
|
276
|
+
private async createLogEntry(level: LogLevel, message: string, data?: any, forceScreenshot?: boolean): Promise<LogEntry> {
|
|
277
|
+
const context: Partial<LogEntry['context']> = {};
|
|
278
|
+
|
|
279
|
+
// Add URL if available
|
|
280
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
281
|
+
return {
|
|
282
|
+
id: `log_${++this.logIdCounter}_${Date.now()}`,
|
|
283
|
+
timestamp: new Date().toISOString(),
|
|
284
|
+
level,
|
|
285
|
+
message,
|
|
286
|
+
data,
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
context.url = window.location.href;
|
|
291
|
+
|
|
292
|
+
// Add browser info (this method now handles worker context internally)
|
|
293
|
+
context.browserInfo = this.getBrowserInfo();
|
|
294
|
+
context.userAgent = context.browserInfo.userAgent;
|
|
295
|
+
|
|
296
|
+
// Add screenshot and mouse position if level is error or warn
|
|
297
|
+
if (level === 'error' || level === 'warn' || forceScreenshot) {
|
|
298
|
+
context.screenshot = await this.captureScreenshot() || undefined;
|
|
299
|
+
context.mousePosition = this.mousePosition || undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
id: `log_${++this.logIdCounter}_${Date.now()}`,
|
|
304
|
+
timestamp: new Date().toISOString(),
|
|
305
|
+
level,
|
|
306
|
+
message,
|
|
307
|
+
data,
|
|
308
|
+
context: context as LogEntry['context']
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Add a log entry to the internal log array.
|
|
314
|
+
* @param entry - Log entry to add
|
|
315
|
+
*/
|
|
316
|
+
private addLogEntry(entry: LogEntry): void {
|
|
317
|
+
this.logs.push(entry);
|
|
318
|
+
|
|
319
|
+
// Maintain log size limit (1000 entries)
|
|
320
|
+
if (this.logs.length > 1000) {
|
|
321
|
+
this.logs = this.logs.slice(-1000);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|