@rimori/react-client 0.4.18-next.2 → 0.4.18-next.4
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/dist/components/ai/Assistant.js +1 -1
- package/dist/components/ai/Avatar.js +2 -0
- package/dist/components/ai/BuddyAssistant.d.ts +1 -1
- package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.js +26 -26
- package/dist/components/audio/Playbutton.js +2 -0
- package/dist/components/editor/MarkdownEditor.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils/injectFederationCss.d.ts +9 -0
- package/dist/utils/injectFederationCss.js +25 -0
- package/eslint.config.js +3 -0
- package/package.json +6 -6
- package/src/components/ContextMenu.tsx +1 -1
- package/src/components/ai/Assistant.tsx +1 -1
- package/src/components/ai/Avatar.tsx +3 -0
- package/src/components/ai/BuddyAssistant.tsx +1 -1
- package/src/components/ai/EmbeddedAssistent/CircleAudioAvatar.tsx +35 -35
- package/src/components/audio/Playbutton.tsx +2 -0
- package/src/components/editor/MarkdownEditor.tsx +1 -0
- package/src/index.ts +1 -0
- package/src/providers/PluginProvider.tsx +1 -1
- package/src/utils/injectFederationCss.ts +25 -0
|
@@ -11,7 +11,7 @@ export function AssistantChat({ avatarImageUrl, voiceId, onComplete, autoStartCo
|
|
|
11
11
|
var _a;
|
|
12
12
|
const [oralCommunication, setOralCommunication] = React.useState(true);
|
|
13
13
|
const { ai: llm, event } = useRimori();
|
|
14
|
-
const sender = useMemo(() => new MessageSender(llm.getVoice, voiceId), []);
|
|
14
|
+
const sender = useMemo(() => new MessageSender(llm.getVoice, voiceId), [llm.getVoice, voiceId]);
|
|
15
15
|
const { messages, append, isLoading, setMessages } = useChat();
|
|
16
16
|
const lastAssistantMessage = (_a = [...messages].filter((m) => m.role === 'assistant').pop()) === null || _a === void 0 ? void 0 : _a.content;
|
|
17
17
|
useEffect(() => {
|
|
@@ -27,6 +27,7 @@ export function Avatar({ avatarImageUrl, voiceId, agentTools, autoStartConversat
|
|
|
27
27
|
console.log('messages', messages);
|
|
28
28
|
}, [messages]);
|
|
29
29
|
useEffect(() => {
|
|
30
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
30
31
|
if (!isLoading)
|
|
31
32
|
setIsProcessingMessage(false);
|
|
32
33
|
}, [isLoading]);
|
|
@@ -58,6 +59,7 @@ export function Avatar({ avatarImageUrl, voiceId, agentTools, autoStartConversat
|
|
|
58
59
|
sender.handleNewText(lastMessage.content, isLoading);
|
|
59
60
|
if (lastMessage.toolCalls) {
|
|
60
61
|
// console.log("unlocking mic", lastMessage)
|
|
62
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
61
63
|
setAgentReplying(false);
|
|
62
64
|
setIsProcessingMessage(false);
|
|
63
65
|
}
|
|
@@ -6,6 +6,32 @@ export function CircleAudioAvatar({ imageUrl, className, isDarkTheme = false, wi
|
|
|
6
6
|
const currentLoudnessRef = useRef(0);
|
|
7
7
|
const targetLoudnessRef = useRef(0);
|
|
8
8
|
const animationFrameRef = useRef(null);
|
|
9
|
+
const draw = (ctx, canvas, image, loudness) => {
|
|
10
|
+
if (canvas && ctx) {
|
|
11
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
12
|
+
const radius = Math.min(canvas.width, canvas.height) / 3;
|
|
13
|
+
const centerX = canvas.width / 2;
|
|
14
|
+
const centerY = canvas.height / 2;
|
|
15
|
+
const pulseRadius = radius + loudness / 2.5;
|
|
16
|
+
ctx.beginPath();
|
|
17
|
+
ctx.arc(centerX, centerY, pulseRadius, 0, Math.PI * 2, true);
|
|
18
|
+
ctx.strokeStyle = isDarkTheme ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)';
|
|
19
|
+
ctx.lineWidth = 5;
|
|
20
|
+
ctx.stroke();
|
|
21
|
+
ctx.save();
|
|
22
|
+
ctx.beginPath();
|
|
23
|
+
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2, true);
|
|
24
|
+
ctx.closePath();
|
|
25
|
+
ctx.clip();
|
|
26
|
+
ctx.drawImage(image, centerX - radius, centerY - radius, radius * 2, radius * 2);
|
|
27
|
+
ctx.restore();
|
|
28
|
+
ctx.beginPath();
|
|
29
|
+
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2, true);
|
|
30
|
+
ctx.strokeStyle = isDarkTheme ? 'rgba(255, 255, 255, 0.9)' : 'rgba(0, 0, 0, 0.9)';
|
|
31
|
+
ctx.lineWidth = 5;
|
|
32
|
+
ctx.stroke();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
9
35
|
useEffect(() => {
|
|
10
36
|
const canvas = canvasRef.current;
|
|
11
37
|
if (canvas) {
|
|
@@ -49,31 +75,5 @@ export function CircleAudioAvatar({ imageUrl, className, isDarkTheme = false, wi
|
|
|
49
75
|
}
|
|
50
76
|
}
|
|
51
77
|
}, [imageUrl]);
|
|
52
|
-
const draw = (ctx, canvas, image, loudness) => {
|
|
53
|
-
if (canvas && ctx) {
|
|
54
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
55
|
-
const radius = Math.min(canvas.width, canvas.height) / 3;
|
|
56
|
-
const centerX = canvas.width / 2;
|
|
57
|
-
const centerY = canvas.height / 2;
|
|
58
|
-
const pulseRadius = radius + loudness / 2.5;
|
|
59
|
-
ctx.beginPath();
|
|
60
|
-
ctx.arc(centerX, centerY, pulseRadius, 0, Math.PI * 2, true);
|
|
61
|
-
ctx.strokeStyle = isDarkTheme ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)';
|
|
62
|
-
ctx.lineWidth = 5;
|
|
63
|
-
ctx.stroke();
|
|
64
|
-
ctx.save();
|
|
65
|
-
ctx.beginPath();
|
|
66
|
-
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2, true);
|
|
67
|
-
ctx.closePath();
|
|
68
|
-
ctx.clip();
|
|
69
|
-
ctx.drawImage(image, centerX - radius, centerY - radius, radius * 2, radius * 2);
|
|
70
|
-
ctx.restore();
|
|
71
|
-
ctx.beginPath();
|
|
72
|
-
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2, true);
|
|
73
|
-
ctx.strokeStyle = isDarkTheme ? 'rgba(255, 255, 255, 0.9)' : 'rgba(0, 0, 0, 0.9)';
|
|
74
|
-
ctx.lineWidth = 5;
|
|
75
|
-
ctx.stroke();
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
78
|
return _jsx("canvas", { ref: canvasRef, className: className, width: 500, height: 500, style: { width } });
|
|
79
79
|
}
|
|
@@ -23,6 +23,7 @@ export const AudioPlayer = ({ text, voice, language, hide, playListenerEvent, in
|
|
|
23
23
|
const audioRef = useRef(null);
|
|
24
24
|
const eventBusListenerRef = useRef(null);
|
|
25
25
|
useEffect(() => {
|
|
26
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
26
27
|
if (audioUrl)
|
|
27
28
|
setAudioUrl(null);
|
|
28
29
|
return () => {
|
|
@@ -128,6 +129,7 @@ export const AudioPlayer = ({ text, voice, language, hide, playListenerEvent, in
|
|
|
128
129
|
return;
|
|
129
130
|
isFetchingAudio = true;
|
|
130
131
|
// console.log("playOnMount", playOnMount);
|
|
132
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
131
133
|
togglePlayback();
|
|
132
134
|
}, [playOnMount]);
|
|
133
135
|
return (_jsx("div", { className: "group relative", children: _jsxs("div", { className: "flex flex-row items-end", children: [!hide && (_jsx("button", { className: "text-gray-400", onClick: togglePlayback, disabled: isLoading, children: isLoading ? (_jsx(Spinner, { size: size })) : isPlaying ? (_jsx(FaStopCircle, { size: size })) : (_jsx(FaPlayCircle, { size: size })) })), enableSpeedAdjustment && (_jsxs("div", { className: "ml-1 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-row text-sm text-gray-500", children: [_jsx("span", { className: "pr-1", children: "Speed: " }), _jsx("select", { value: speed, className: "appearance-none cursor-pointer pr-0 p-0 rounded shadow leading-tight focus:outline-none focus:bg-gray-800 focus:ring bg-transparent border-0", onChange: (e) => setSpeed(parseFloat(e.target.value)), disabled: isLoading, children: AudioPlayOptions.map((s) => (_jsx("option", { value: s, children: s }, s))) })] }))] }) }));
|
|
@@ -78,6 +78,7 @@ const InlinePanel = ({ panel, onClose, editor, onUpdate, labels, onTransform, is
|
|
|
78
78
|
useEffect(() => {
|
|
79
79
|
if (panel === 'link') {
|
|
80
80
|
const href = editor.getAttributes('link').href;
|
|
81
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
81
82
|
setValue(typeof href === 'string' ? href : 'https://');
|
|
82
83
|
}
|
|
83
84
|
else {
|
package/dist/index.d.ts
CHANGED
|
@@ -12,3 +12,4 @@ export { VoiceRecorder } from './components/ai/EmbeddedAssistent/VoiceRecorder';
|
|
|
12
12
|
export { MarkdownEditor } from './components/editor/MarkdownEditor';
|
|
13
13
|
export type { MarkdownEditorProps, EditorLabels } from './components/editor/MarkdownEditor';
|
|
14
14
|
export { extractImageUrls } from './components/editor/imageUtils';
|
|
15
|
+
export { injectFederationCss } from './utils/injectFederationCss';
|
package/dist/index.js
CHANGED
|
@@ -10,3 +10,4 @@ export { BuddyAssistant } from './components/ai/BuddyAssistant';
|
|
|
10
10
|
export { VoiceRecorder } from './components/ai/EmbeddedAssistent/VoiceRecorder';
|
|
11
11
|
export { MarkdownEditor } from './components/editor/MarkdownEditor';
|
|
12
12
|
export { extractImageUrls } from './components/editor/imageUtils';
|
|
13
|
+
export { injectFederationCss } from './utils/injectFederationCss';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inject a plugin's Tailwind CSS into the document when a federated module loads.
|
|
3
|
+
* Inserts BEFORE existing stylesheets so rimori-main's responsive classes (e.g. md:hidden)
|
|
4
|
+
* keep higher cascade priority and are not overridden by base utility classes from the plugin.
|
|
5
|
+
*
|
|
6
|
+
* @param pluginId - Unique plugin identifier used as the <style> element id (e.g. 'rimori-plugin-translator')
|
|
7
|
+
* @param cssText - The inlined CSS string (imported via `?inline`)
|
|
8
|
+
*/
|
|
9
|
+
export declare function injectFederationCss(pluginId: string, cssText: string): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inject a plugin's Tailwind CSS into the document when a federated module loads.
|
|
3
|
+
* Inserts BEFORE existing stylesheets so rimori-main's responsive classes (e.g. md:hidden)
|
|
4
|
+
* keep higher cascade priority and are not overridden by base utility classes from the plugin.
|
|
5
|
+
*
|
|
6
|
+
* @param pluginId - Unique plugin identifier used as the <style> element id (e.g. 'rimori-plugin-translator')
|
|
7
|
+
* @param cssText - The inlined CSS string (imported via `?inline`)
|
|
8
|
+
*/
|
|
9
|
+
export function injectFederationCss(pluginId, cssText) {
|
|
10
|
+
if (typeof document === 'undefined' || !cssText)
|
|
11
|
+
return;
|
|
12
|
+
const id = `${pluginId}-css`;
|
|
13
|
+
if (document.getElementById(id))
|
|
14
|
+
return;
|
|
15
|
+
const style = document.createElement('style');
|
|
16
|
+
style.id = id;
|
|
17
|
+
style.textContent = cssText;
|
|
18
|
+
const firstStyle = document.head.querySelector('style, link[rel="stylesheet"]');
|
|
19
|
+
if (firstStyle) {
|
|
20
|
+
document.head.insertBefore(style, firstStyle);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
document.head.appendChild(style);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/eslint.config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimori/react-client",
|
|
3
|
-
"version": "0.4.18-next.
|
|
3
|
+
"version": "0.4.18-next.4",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"format": "prettier --write ."
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@rimori/client": "2.5.29-next.
|
|
29
|
-
"react": "^
|
|
30
|
-
"react-dom": "^
|
|
28
|
+
"@rimori/client": "2.5.29-next.3",
|
|
29
|
+
"react": "^19.0.0",
|
|
30
|
+
"react-dom": "^19.0.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@tiptap/core": "^2.26.1",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"tiptap-markdown": "^0.8.10"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@rimori/client": "2.5.29-next.
|
|
52
|
-
"@types/react": "^
|
|
51
|
+
"@rimori/client": "2.5.29-next.3",
|
|
52
|
+
"@types/react": "^19.0.0",
|
|
53
53
|
"eslint-config-prettier": "^10.1.8",
|
|
54
54
|
"eslint-plugin-react-hooks": "^7.0.0",
|
|
55
55
|
"form-data": "^4.0.2",
|
|
@@ -17,7 +17,7 @@ interface Props {
|
|
|
17
17
|
export function AssistantChat({ avatarImageUrl, voiceId, onComplete, autoStartConversation }: Props) {
|
|
18
18
|
const [oralCommunication, setOralCommunication] = React.useState(true);
|
|
19
19
|
const { ai: llm, event } = useRimori();
|
|
20
|
-
const sender = useMemo(() => new MessageSender(llm.getVoice, voiceId), []);
|
|
20
|
+
const sender = useMemo(() => new MessageSender(llm.getVoice, voiceId), [llm.getVoice, voiceId]);
|
|
21
21
|
const { messages, append, isLoading, setMessages } = useChat();
|
|
22
22
|
|
|
23
23
|
const lastAssistantMessage = [...messages].filter((m) => m.role === 'assistant').pop()?.content;
|
|
@@ -61,6 +61,7 @@ export function Avatar({
|
|
|
61
61
|
}, [messages]);
|
|
62
62
|
|
|
63
63
|
useEffect(() => {
|
|
64
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
64
65
|
if (!isLoading) setIsProcessingMessage(false);
|
|
65
66
|
}, [isLoading]);
|
|
66
67
|
|
|
@@ -94,7 +95,9 @@ export function Avatar({
|
|
|
94
95
|
sender.handleNewText(lastMessage.content, isLoading);
|
|
95
96
|
if (lastMessage.toolCalls) {
|
|
96
97
|
// console.log("unlocking mic", lastMessage)
|
|
98
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
97
99
|
setAgentReplying(false);
|
|
100
|
+
|
|
98
101
|
setIsProcessingMessage(false);
|
|
99
102
|
}
|
|
100
103
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
|
1
|
+
import React, { JSX, useState, useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import { CircleAudioAvatar } from './EmbeddedAssistent/CircleAudioAvatar';
|
|
3
3
|
import { VoiceRecorder } from './EmbeddedAssistent/VoiceRecorder';
|
|
4
4
|
import { MessageSender, Tool } from '@rimori/client';
|
|
@@ -19,6 +19,41 @@ export function CircleAudioAvatar({
|
|
|
19
19
|
const targetLoudnessRef = useRef(0);
|
|
20
20
|
const animationFrameRef = useRef<number | null>(null);
|
|
21
21
|
|
|
22
|
+
const draw = (
|
|
23
|
+
ctx: CanvasRenderingContext2D,
|
|
24
|
+
canvas: HTMLCanvasElement,
|
|
25
|
+
image: HTMLImageElement,
|
|
26
|
+
loudness: number,
|
|
27
|
+
) => {
|
|
28
|
+
if (canvas && ctx) {
|
|
29
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
30
|
+
|
|
31
|
+
const radius = Math.min(canvas.width, canvas.height) / 3;
|
|
32
|
+
const centerX = canvas.width / 2;
|
|
33
|
+
const centerY = canvas.height / 2;
|
|
34
|
+
const pulseRadius = radius + loudness / 2.5;
|
|
35
|
+
ctx.beginPath();
|
|
36
|
+
ctx.arc(centerX, centerY, pulseRadius, 0, Math.PI * 2, true);
|
|
37
|
+
ctx.strokeStyle = isDarkTheme ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)';
|
|
38
|
+
ctx.lineWidth = 5;
|
|
39
|
+
ctx.stroke();
|
|
40
|
+
|
|
41
|
+
ctx.save();
|
|
42
|
+
ctx.beginPath();
|
|
43
|
+
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2, true);
|
|
44
|
+
ctx.closePath();
|
|
45
|
+
ctx.clip();
|
|
46
|
+
ctx.drawImage(image, centerX - radius, centerY - radius, radius * 2, radius * 2);
|
|
47
|
+
ctx.restore();
|
|
48
|
+
|
|
49
|
+
ctx.beginPath();
|
|
50
|
+
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2, true);
|
|
51
|
+
ctx.strokeStyle = isDarkTheme ? 'rgba(255, 255, 255, 0.9)' : 'rgba(0, 0, 0, 0.9)';
|
|
52
|
+
ctx.lineWidth = 5;
|
|
53
|
+
ctx.stroke();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
22
57
|
useEffect(() => {
|
|
23
58
|
const canvas = canvasRef.current;
|
|
24
59
|
if (canvas) {
|
|
@@ -68,40 +103,5 @@ export function CircleAudioAvatar({
|
|
|
68
103
|
}
|
|
69
104
|
}, [imageUrl]);
|
|
70
105
|
|
|
71
|
-
const draw = (
|
|
72
|
-
ctx: CanvasRenderingContext2D,
|
|
73
|
-
canvas: HTMLCanvasElement,
|
|
74
|
-
image: HTMLImageElement,
|
|
75
|
-
loudness: number,
|
|
76
|
-
) => {
|
|
77
|
-
if (canvas && ctx) {
|
|
78
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
79
|
-
|
|
80
|
-
const radius = Math.min(canvas.width, canvas.height) / 3;
|
|
81
|
-
const centerX = canvas.width / 2;
|
|
82
|
-
const centerY = canvas.height / 2;
|
|
83
|
-
const pulseRadius = radius + loudness / 2.5;
|
|
84
|
-
ctx.beginPath();
|
|
85
|
-
ctx.arc(centerX, centerY, pulseRadius, 0, Math.PI * 2, true);
|
|
86
|
-
ctx.strokeStyle = isDarkTheme ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)';
|
|
87
|
-
ctx.lineWidth = 5;
|
|
88
|
-
ctx.stroke();
|
|
89
|
-
|
|
90
|
-
ctx.save();
|
|
91
|
-
ctx.beginPath();
|
|
92
|
-
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2, true);
|
|
93
|
-
ctx.closePath();
|
|
94
|
-
ctx.clip();
|
|
95
|
-
ctx.drawImage(image, centerX - radius, centerY - radius, radius * 2, radius * 2);
|
|
96
|
-
ctx.restore();
|
|
97
|
-
|
|
98
|
-
ctx.beginPath();
|
|
99
|
-
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2, true);
|
|
100
|
-
ctx.strokeStyle = isDarkTheme ? 'rgba(255, 255, 255, 0.9)' : 'rgba(0, 0, 0, 0.9)';
|
|
101
|
-
ctx.lineWidth = 5;
|
|
102
|
-
ctx.stroke();
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
106
106
|
return <canvas ref={canvasRef} className={className} width={500} height={500} style={{ width }} />;
|
|
107
107
|
}
|
|
@@ -48,6 +48,7 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
|
|
|
48
48
|
const eventBusListenerRef = useRef<{ off: () => void } | null>(null);
|
|
49
49
|
|
|
50
50
|
useEffect(() => {
|
|
51
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
51
52
|
if (audioUrl) setAudioUrl(null);
|
|
52
53
|
return () => {
|
|
53
54
|
if (audioUrl) URL.revokeObjectURL(audioUrl);
|
|
@@ -170,6 +171,7 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
|
|
|
170
171
|
if (!playOnMount || isFetchingAudio) return;
|
|
171
172
|
isFetchingAudio = true;
|
|
172
173
|
// console.log("playOnMount", playOnMount);
|
|
174
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
173
175
|
togglePlayback();
|
|
174
176
|
}, [playOnMount]);
|
|
175
177
|
|
|
@@ -153,6 +153,7 @@ const InlinePanel = ({
|
|
|
153
153
|
useEffect(() => {
|
|
154
154
|
if (panel === 'link') {
|
|
155
155
|
const href = editor.getAttributes('link').href as string | undefined;
|
|
156
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
156
157
|
setValue(typeof href === 'string' ? href : 'https://');
|
|
157
158
|
} else {
|
|
158
159
|
setValue('');
|
package/src/index.ts
CHANGED
|
@@ -13,3 +13,4 @@ export { VoiceRecorder } from './components/ai/EmbeddedAssistent/VoiceRecorder';
|
|
|
13
13
|
export { MarkdownEditor } from './components/editor/MarkdownEditor';
|
|
14
14
|
export type { MarkdownEditorProps, EditorLabels } from './components/editor/MarkdownEditor';
|
|
15
15
|
export { extractImageUrls } from './components/editor/imageUtils';
|
|
16
|
+
export { injectFederationCss } from './utils/injectFederationCss';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { createContext, useContext, ReactNode, useEffect, useState } from 'react';
|
|
1
|
+
import React, { createContext, useContext, JSX, ReactNode, useEffect, useState } from 'react';
|
|
2
2
|
import { EventBusHandler, RimoriClient, StandaloneClient } from '@rimori/client';
|
|
3
3
|
import type { UserInfo } from '@rimori/client';
|
|
4
4
|
import posthog from 'posthog-js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inject a plugin's Tailwind CSS into the document when a federated module loads.
|
|
3
|
+
* Inserts BEFORE existing stylesheets so rimori-main's responsive classes (e.g. md:hidden)
|
|
4
|
+
* keep higher cascade priority and are not overridden by base utility classes from the plugin.
|
|
5
|
+
*
|
|
6
|
+
* @param pluginId - Unique plugin identifier used as the <style> element id (e.g. 'rimori-plugin-translator')
|
|
7
|
+
* @param cssText - The inlined CSS string (imported via `?inline`)
|
|
8
|
+
*/
|
|
9
|
+
export function injectFederationCss(pluginId: string, cssText: string): void {
|
|
10
|
+
if (typeof document === 'undefined' || !cssText) return;
|
|
11
|
+
|
|
12
|
+
const id = `${pluginId}-css`;
|
|
13
|
+
if (document.getElementById(id)) return;
|
|
14
|
+
|
|
15
|
+
const style = document.createElement('style');
|
|
16
|
+
style.id = id;
|
|
17
|
+
style.textContent = cssText;
|
|
18
|
+
|
|
19
|
+
const firstStyle = document.head.querySelector('style, link[rel="stylesheet"]');
|
|
20
|
+
if (firstStyle) {
|
|
21
|
+
document.head.insertBefore(style, firstStyle);
|
|
22
|
+
} else {
|
|
23
|
+
document.head.appendChild(style);
|
|
24
|
+
}
|
|
25
|
+
}
|