@rimori/react-client 0.4.18 → 0.4.19-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ai/Assistant.js +1 -1
- package/dist/components/ai/Avatar.js +3 -1
- package/dist/components/ai/BuddyAssistant.d.ts +1 -1
- package/dist/components/ai/BuddyAssistant.js +2 -2
- 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/providers/PluginProvider.d.ts +4 -6
- package/dist/providers/PluginProvider.js +18 -13
- package/dist/utils/injectFederationCss.d.ts +9 -0
- package/dist/utils/injectFederationCss.js +25 -0
- package/eslint.config.js +7 -25
- package/package.json +7 -10
- package/src/components/ContextMenu.tsx +1 -1
- package/src/components/ai/Assistant.tsx +1 -1
- package/src/components/ai/Avatar.tsx +4 -1
- package/src/components/ai/BuddyAssistant.tsx +3 -3
- 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 +28 -20
- 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(() => {
|
|
@@ -9,7 +9,7 @@ import { getFirstMessages } from './utils';
|
|
|
9
9
|
import { useTheme } from '../../hooks/ThemeSetter';
|
|
10
10
|
export function Avatar({ avatarImageUrl, voiceId, agentTools, autoStartConversation, children, circleSize = '300px', className, cache = false, prompt, promptVariables, }) {
|
|
11
11
|
const { ai, event, plugin, userInfo } = useRimori();
|
|
12
|
-
const { isDark: isDarkThemeValue } = useTheme(plugin.theme);
|
|
12
|
+
const { isDark: isDarkThemeValue } = useTheme(plugin.theme, true);
|
|
13
13
|
const [agentReplying, setAgentReplying] = useState(false);
|
|
14
14
|
const [isProcessingMessage, setIsProcessingMessage] = useState(false);
|
|
15
15
|
const dialectTtsInstruction = (userInfo === null || userInfo === void 0 ? void 0 : userInfo.dialect)
|
|
@@ -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
|
}
|
|
@@ -11,7 +11,7 @@ const genId = () => `ba-${++idCounter}`;
|
|
|
11
11
|
export function BuddyAssistant({ prompt, promptVariables, autoStartConversation, circleSize = '160px', chatPlaceholder, bottomAction, className, voiceSpeed = 1, tools, showName = false, }) {
|
|
12
12
|
const { ai, event, plugin, userInfo } = useRimori();
|
|
13
13
|
const ttsEnabled = plugin.ttsEnabled;
|
|
14
|
-
const { isDark } = useTheme(plugin.theme);
|
|
14
|
+
const { isDark } = useTheme(plugin.theme, true);
|
|
15
15
|
const buddy = userInfo.study_buddy;
|
|
16
16
|
const dialect = userInfo === null || userInfo === void 0 ? void 0 : userInfo.dialect;
|
|
17
17
|
const dialectTtsInstruction = dialect ? `Speak with a ${dialect} accent and pronunciation.` : undefined;
|
|
@@ -99,7 +99,7 @@ export function BuddyAssistant({ prompt, promptVariables, autoStartConversation,
|
|
|
99
99
|
triggerAI(newMessages);
|
|
100
100
|
};
|
|
101
101
|
const lastAssistantMessage = [...messages].filter((m) => m.role === 'assistant').pop();
|
|
102
|
-
return (_jsxs("div", { className: `flex flex-col items-center ${className || ''}`, children: [_jsx(CircleAudioAvatar, { width: circleSize, imageUrl: buddy.avatarUrl, isDarkTheme: isDark, className: "mx-auto" }), showName && (_jsx("div", { className: "flex items-center gap-2", children: _jsx("span", { className: "text-3xl font-semibold", children: buddy.name }) })), !ttsEnabled ? (_jsx("div", { className: "w-full rounded-xl bg-gray-200/70 dark:bg-gray-800/70 px-4 py-3 text-sm text-gray-800 dark:text-gray-200 leading-relaxed border border-gray-300/40 dark:border-gray-700/40 mt-4", children: !(lastAssistantMessage === null || lastAssistantMessage === void 0 ? void 0 : lastAssistantMessage.content) && isLoading ? (_jsxs("span", { className: "inline-flex gap-1 py-0.5", children: [_jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce" }), _jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce", style: { animationDelay: '0.15s' } }), _jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce", style: { animationDelay: '0.3s' } })] })) : (_jsx("span", { className: "whitespace-pre-wrap", children: lastAssistantMessage === null || lastAssistantMessage === void 0 ? void 0 : lastAssistantMessage.content })) })) : null, _jsxs("div", { className: 'w-full relative mt-4 ' + (ttsEnabled ? 'max-w-md' : ''), children: [_jsx("input", { value: chatInput, onChange: (e) => setChatInput(e.target.value), onKeyDown: (e) => {
|
|
102
|
+
return (_jsxs("div", { className: `flex flex-col items-center ${className || ''}`, children: [_jsx(CircleAudioAvatar, { width: circleSize, imageUrl: buddy.avatarUrl, isDarkTheme: isDark, className: "mx-auto" }), showName && (_jsx("div", { className: "flex items-center gap-2", children: _jsx("span", { className: "text-3xl font-semibold", children: buddy.name }) })), !plugin.ttsEnabled && messages.length > 1 ? (_jsx("div", { className: "w-full rounded-xl bg-gray-200/70 dark:bg-gray-800/70 px-4 py-3 text-sm text-gray-800 dark:text-gray-200 leading-relaxed border border-gray-300/40 dark:border-gray-700/40 mt-4", children: !(lastAssistantMessage === null || lastAssistantMessage === void 0 ? void 0 : lastAssistantMessage.content) && isLoading ? (_jsxs("span", { className: "inline-flex gap-1 py-0.5", children: [_jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce" }), _jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce", style: { animationDelay: '0.15s' } }), _jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce", style: { animationDelay: '0.3s' } })] })) : (_jsx("span", { className: "whitespace-pre-wrap", children: lastAssistantMessage === null || lastAssistantMessage === void 0 ? void 0 : lastAssistantMessage.content })) })) : null, _jsxs("div", { className: 'w-full relative mt-4 ' + (ttsEnabled ? 'max-w-md' : ''), children: [_jsx("input", { value: chatInput, onChange: (e) => setChatInput(e.target.value), onKeyDown: (e) => {
|
|
103
103
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
104
104
|
e.preventDefault();
|
|
105
105
|
sendMessage(chatInput);
|
|
@@ -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';
|
|
@@ -4,14 +4,12 @@ import type { UserInfo } from '@rimori/client';
|
|
|
4
4
|
interface PluginProviderProps {
|
|
5
5
|
children: ReactNode;
|
|
6
6
|
pluginId: string;
|
|
7
|
+
/** Pre-constructed RimoriClient (federation mode). When provided, skips the handshake entirely. */
|
|
8
|
+
client?: RimoriClient;
|
|
9
|
+
/** Pre-loaded user info (federation mode). Required when client is provided. */
|
|
10
|
+
userInfo?: UserInfo;
|
|
7
11
|
settings?: {
|
|
8
12
|
disableContextMenu?: boolean;
|
|
9
|
-
disableThemeSetting?: boolean;
|
|
10
|
-
/**
|
|
11
|
-
* Skip the scrollbar detection that emits 'session.triggerScrollbarChange'.
|
|
12
|
-
* In federation mode, the host (FederatedPluginRenderer) handles this instead.
|
|
13
|
-
*/
|
|
14
|
-
disableScrollbarDetection?: boolean;
|
|
15
13
|
};
|
|
16
14
|
}
|
|
17
15
|
export declare const PluginProvider: React.FC<PluginProviderProps>;
|
|
@@ -14,13 +14,14 @@ import posthog from 'posthog-js';
|
|
|
14
14
|
import ContextMenu from '../components/ContextMenu';
|
|
15
15
|
import { useTheme } from '../hooks/ThemeSetter';
|
|
16
16
|
const PluginContext = createContext(null);
|
|
17
|
-
export const PluginProvider = ({ children, pluginId, settings }) => {
|
|
18
|
-
const
|
|
17
|
+
export const PluginProvider = ({ children, pluginId, client: injectedClient, userInfo: injectedUserInfo, settings, }) => {
|
|
18
|
+
const isFederated = !!injectedClient;
|
|
19
|
+
const [client, setClient] = useState(injectedClient !== null && injectedClient !== void 0 ? injectedClient : null);
|
|
19
20
|
const [standaloneClient, setStandaloneClient] = useState(false);
|
|
20
21
|
const [applicationMode, setApplicationMode] = useState(null);
|
|
21
22
|
const [theme, setTheme] = useState(undefined);
|
|
22
|
-
const [userInfo, setUserInfo] = useState(null);
|
|
23
|
-
useTheme(theme,
|
|
23
|
+
const [userInfo, setUserInfo] = useState(injectedUserInfo !== null && injectedUserInfo !== void 0 ? injectedUserInfo : null);
|
|
24
|
+
useTheme(theme, isFederated);
|
|
24
25
|
// Init PostHog once per plugin iframe
|
|
25
26
|
useEffect(() => {
|
|
26
27
|
if (!posthog.__loaded) {
|
|
@@ -40,6 +41,9 @@ export const PluginProvider = ({ children, pluginId, settings }) => {
|
|
|
40
41
|
const isSidebar = applicationMode === 'sidebar';
|
|
41
42
|
const isSettings = applicationMode === 'settings';
|
|
42
43
|
useEffect(() => {
|
|
44
|
+
// In federation mode the client is already ready — skip handshake
|
|
45
|
+
if (isFederated)
|
|
46
|
+
return;
|
|
43
47
|
initEventBus(pluginId);
|
|
44
48
|
// Check if we're in an iframe context - if not, we're standalone
|
|
45
49
|
const standaloneDetected = window === window.parent;
|
|
@@ -51,7 +55,6 @@ export const PluginProvider = ({ children, pluginId, settings }) => {
|
|
|
51
55
|
if ((!standaloneDetected && !client) || (standaloneDetected && standaloneClient === true)) {
|
|
52
56
|
void RimoriClient.getInstance(pluginId).then((client) => {
|
|
53
57
|
setClient(client);
|
|
54
|
-
// Set initial userInfo
|
|
55
58
|
setUserInfo(client.plugin.getUserInfo());
|
|
56
59
|
// Get applicationMode and theme from MessageChannel query params
|
|
57
60
|
if (!standaloneDetected) {
|
|
@@ -61,7 +64,7 @@ export const PluginProvider = ({ children, pluginId, settings }) => {
|
|
|
61
64
|
}
|
|
62
65
|
});
|
|
63
66
|
}
|
|
64
|
-
}, [pluginId, standaloneClient, client]);
|
|
67
|
+
}, [pluginId, standaloneClient, client, isFederated]);
|
|
65
68
|
// Identify user in PostHog when userInfo is available
|
|
66
69
|
useEffect(() => {
|
|
67
70
|
if (userInfo === null || userInfo === void 0 ? void 0 : userInfo.user_id) {
|
|
@@ -73,7 +76,7 @@ export const PluginProvider = ({ children, pluginId, settings }) => {
|
|
|
73
76
|
if (!client)
|
|
74
77
|
return;
|
|
75
78
|
const unsubscribe = client.plugin.onRimoriInfoUpdate((info) => {
|
|
76
|
-
console.log('[PluginProvider] Received RimoriInfo update, updating userInfo');
|
|
79
|
+
// console.log('[PluginProvider] Received RimoriInfo update, updating userInfo + ttsEnabled:', info.ttsEnabled);
|
|
77
80
|
setUserInfo(info.profile);
|
|
78
81
|
});
|
|
79
82
|
return () => unsubscribe();
|
|
@@ -83,6 +86,8 @@ export const PluginProvider = ({ children, pluginId, settings }) => {
|
|
|
83
86
|
return;
|
|
84
87
|
if (isSidebar)
|
|
85
88
|
return; //sidebar pages should not report url changes
|
|
89
|
+
if (isFederated)
|
|
90
|
+
return; // federation mode: host handles URL sync
|
|
86
91
|
// react router overwrites native pushstate so it gets wrapped to detect url changes
|
|
87
92
|
const originalPushState = history.pushState;
|
|
88
93
|
history.pushState = (...args) => {
|
|
@@ -90,11 +95,11 @@ export const PluginProvider = ({ children, pluginId, settings }) => {
|
|
|
90
95
|
client.event.emit('session.triggerUrlChange', { url: location.hash });
|
|
91
96
|
return result;
|
|
92
97
|
};
|
|
93
|
-
}, [client, isSidebar]);
|
|
98
|
+
}, [client, isSidebar, isFederated]);
|
|
94
99
|
useEffect(() => {
|
|
95
100
|
if (!client)
|
|
96
101
|
return;
|
|
97
|
-
if (
|
|
102
|
+
if (isFederated)
|
|
98
103
|
return;
|
|
99
104
|
const checkScrollbar = () => {
|
|
100
105
|
const hasScrollbar = document.documentElement.scrollHeight > window.innerHeight;
|
|
@@ -111,7 +116,7 @@ export const PluginProvider = ({ children, pluginId, settings }) => {
|
|
|
111
116
|
window.removeEventListener('resize', checkScrollbar);
|
|
112
117
|
resizeObserver.disconnect();
|
|
113
118
|
};
|
|
114
|
-
}, [client,
|
|
119
|
+
}, [client, isFederated]);
|
|
115
120
|
if (standaloneClient instanceof StandaloneClient) {
|
|
116
121
|
return (_jsx(StandaloneAuth, { onLogin: (email, password) => __awaiter(void 0, void 0, void 0, function* () {
|
|
117
122
|
if (yield standaloneClient.login(email, password))
|
|
@@ -128,9 +133,9 @@ export const useRimori = () => {
|
|
|
128
133
|
if (context === null) {
|
|
129
134
|
throw new Error('useRimori must be used within an PluginProvider');
|
|
130
135
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
return Object.assign(context.client, {
|
|
137
|
+
userInfo: context.userInfo,
|
|
138
|
+
});
|
|
134
139
|
};
|
|
135
140
|
function getUrlParam(name) {
|
|
136
141
|
// First try to get from URL hash query params (for compatibility)
|
|
@@ -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
|
@@ -1,53 +1,35 @@
|
|
|
1
|
-
import js from '@eslint/js';
|
|
2
1
|
import globals from 'globals';
|
|
3
2
|
import reactHooks from 'eslint-plugin-react-hooks';
|
|
4
|
-
import reactRefresh from 'eslint-plugin-react-refresh';
|
|
5
3
|
import tseslint from 'typescript-eslint';
|
|
6
|
-
import prettier from 'eslint-plugin-prettier';
|
|
7
4
|
import prettierConfig from 'eslint-config-prettier';
|
|
8
5
|
|
|
9
6
|
export default [
|
|
10
|
-
{ ignores: ['dist', 'node_modules', 'build', '*.js'] },
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
{ ignores: ['dist', 'node_modules', 'build', '*.js', '*.cjs', 'scripts/**'] },
|
|
8
|
+
...tseslint.configs.recommended.map((config) => ({
|
|
9
|
+
...config,
|
|
10
|
+
files: ['src/**/*.{ts,tsx}'],
|
|
11
|
+
})),
|
|
13
12
|
prettierConfig,
|
|
14
13
|
{
|
|
15
|
-
files: ['
|
|
14
|
+
files: ['src/**/*.{ts,tsx}'],
|
|
16
15
|
languageOptions: {
|
|
17
16
|
ecmaVersion: 2020,
|
|
18
17
|
globals: {
|
|
19
18
|
...globals.browser,
|
|
20
19
|
...globals.node,
|
|
21
|
-
...globals.jest,
|
|
22
20
|
},
|
|
23
21
|
sourceType: 'module',
|
|
24
22
|
parserOptions: {
|
|
25
|
-
projectService: true,
|
|
26
23
|
tsconfigRootDir: import.meta.dirname,
|
|
27
24
|
},
|
|
28
25
|
},
|
|
29
26
|
plugins: {
|
|
30
27
|
'react-hooks': reactHooks,
|
|
31
|
-
'react-refresh': reactRefresh,
|
|
32
|
-
prettier: prettier,
|
|
33
28
|
},
|
|
34
29
|
rules: {
|
|
35
30
|
...reactHooks.configs.recommended.rules,
|
|
36
|
-
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
|
37
|
-
'@typescript-eslint/no-unused-vars': 'warn',
|
|
38
31
|
'@typescript-eslint/no-explicit-any': 'warn',
|
|
39
|
-
'@typescript-eslint/no-
|
|
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',
|
|
32
|
+
'@typescript-eslint/no-unused-vars': 'warn',
|
|
51
33
|
},
|
|
52
34
|
},
|
|
53
35
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimori/react-client",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.19-next.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
"dev": "tsc -w --preserveWatchOutput",
|
|
22
22
|
"dev:watch": "tsc -w --preserveWatchOutput",
|
|
23
23
|
"css-dev": "sass --watch src/style.scss:dist/style.css",
|
|
24
|
-
"lint": "eslint . --fix",
|
|
24
|
+
"lint": "npx eslint . --fix",
|
|
25
25
|
"format": "prettier --write ."
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@rimori/client": "^2.5.
|
|
29
|
-
"react": "^
|
|
30
|
-
"react-dom": "^
|
|
28
|
+
"@rimori/client": "^2.5.30",
|
|
29
|
+
"react": "^19.0.0",
|
|
30
|
+
"react-dom": "^19.0.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@tiptap/core": "^2.26.1",
|
|
@@ -48,13 +48,10 @@
|
|
|
48
48
|
"tiptap-markdown": "^0.8.10"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@
|
|
52
|
-
"@
|
|
53
|
-
"@types/react": "^18.3.21",
|
|
51
|
+
"@rimori/client": "^2.5.30",
|
|
52
|
+
"@types/react": "^19.0.0",
|
|
54
53
|
"eslint-config-prettier": "^10.1.8",
|
|
55
|
-
"eslint-plugin-prettier": "^5.5.4",
|
|
56
54
|
"eslint-plugin-react-hooks": "^7.0.0",
|
|
57
|
-
"eslint-plugin-react-refresh": "^0.4.23",
|
|
58
55
|
"form-data": "^4.0.2",
|
|
59
56
|
"globals": "^16.4.0",
|
|
60
57
|
"node-fetch": "^3.3.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;
|
|
@@ -37,7 +37,7 @@ export function Avatar({
|
|
|
37
37
|
promptVariables,
|
|
38
38
|
}: Props) {
|
|
39
39
|
const { ai, event, plugin, userInfo } = useRimori();
|
|
40
|
-
const { isDark: isDarkThemeValue } = useTheme(plugin.theme);
|
|
40
|
+
const { isDark: isDarkThemeValue } = useTheme(plugin.theme, true);
|
|
41
41
|
const [agentReplying, setAgentReplying] = useState(false);
|
|
42
42
|
const [isProcessingMessage, setIsProcessingMessage] = useState(false);
|
|
43
43
|
const dialectTtsInstruction = userInfo?.dialect
|
|
@@ -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';
|
|
@@ -48,7 +48,7 @@ export function BuddyAssistant({
|
|
|
48
48
|
}: BuddyAssistantProps): JSX.Element {
|
|
49
49
|
const { ai, event, plugin, userInfo } = useRimori();
|
|
50
50
|
const ttsEnabled = plugin.ttsEnabled;
|
|
51
|
-
const { isDark } = useTheme(plugin.theme);
|
|
51
|
+
const { isDark } = useTheme(plugin.theme, true);
|
|
52
52
|
const buddy = userInfo.study_buddy;
|
|
53
53
|
const dialect = userInfo?.dialect;
|
|
54
54
|
const dialectTtsInstruction = dialect ? `Speak with a ${dialect} accent and pronunciation.` : undefined;
|
|
@@ -158,7 +158,7 @@ export function BuddyAssistant({
|
|
|
158
158
|
</div>
|
|
159
159
|
)}
|
|
160
160
|
|
|
161
|
-
{!ttsEnabled ? (
|
|
161
|
+
{!plugin.ttsEnabled && messages.length > 1 ? (
|
|
162
162
|
<div className="w-full rounded-xl bg-gray-200/70 dark:bg-gray-800/70 px-4 py-3 text-sm text-gray-800 dark:text-gray-200 leading-relaxed border border-gray-300/40 dark:border-gray-700/40 mt-4">
|
|
163
163
|
{!lastAssistantMessage?.content && isLoading ? (
|
|
164
164
|
<span className="inline-flex gap-1 py-0.5">
|
|
@@ -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';
|
|
@@ -9,14 +9,12 @@ import { Theme } from '@rimori/client';
|
|
|
9
9
|
interface PluginProviderProps {
|
|
10
10
|
children: ReactNode;
|
|
11
11
|
pluginId: string;
|
|
12
|
+
/** Pre-constructed RimoriClient (federation mode). When provided, skips the handshake entirely. */
|
|
13
|
+
client?: RimoriClient;
|
|
14
|
+
/** Pre-loaded user info (federation mode). Required when client is provided. */
|
|
15
|
+
userInfo?: UserInfo;
|
|
12
16
|
settings?: {
|
|
13
17
|
disableContextMenu?: boolean;
|
|
14
|
-
disableThemeSetting?: boolean;
|
|
15
|
-
/**
|
|
16
|
-
* Skip the scrollbar detection that emits 'session.triggerScrollbarChange'.
|
|
17
|
-
* In federation mode, the host (FederatedPluginRenderer) handles this instead.
|
|
18
|
-
*/
|
|
19
|
-
disableScrollbarDetection?: boolean;
|
|
20
18
|
};
|
|
21
19
|
}
|
|
22
20
|
|
|
@@ -27,14 +25,21 @@ interface PluginContextValue {
|
|
|
27
25
|
|
|
28
26
|
const PluginContext = createContext<PluginContextValue | null>(null);
|
|
29
27
|
|
|
30
|
-
export const PluginProvider: React.FC<PluginProviderProps> = ({
|
|
31
|
-
|
|
28
|
+
export const PluginProvider: React.FC<PluginProviderProps> = ({
|
|
29
|
+
children,
|
|
30
|
+
pluginId,
|
|
31
|
+
client: injectedClient,
|
|
32
|
+
userInfo: injectedUserInfo,
|
|
33
|
+
settings,
|
|
34
|
+
}) => {
|
|
35
|
+
const isFederated = !!injectedClient;
|
|
36
|
+
const [client, setClient] = useState<RimoriClient | null>(injectedClient ?? null);
|
|
32
37
|
const [standaloneClient, setStandaloneClient] = useState<StandaloneClient | boolean>(false);
|
|
33
38
|
const [applicationMode, setApplicationMode] = useState<string | null>(null);
|
|
34
39
|
const [theme, setTheme] = useState<Theme | undefined>(undefined);
|
|
35
|
-
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
|
40
|
+
const [userInfo, setUserInfo] = useState<UserInfo | null>(injectedUserInfo ?? null);
|
|
36
41
|
|
|
37
|
-
useTheme(theme,
|
|
42
|
+
useTheme(theme, isFederated);
|
|
38
43
|
|
|
39
44
|
// Init PostHog once per plugin iframe
|
|
40
45
|
useEffect(() => {
|
|
@@ -57,6 +62,9 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children, plugin
|
|
|
57
62
|
const isSettings = applicationMode === 'settings';
|
|
58
63
|
|
|
59
64
|
useEffect(() => {
|
|
65
|
+
// In federation mode the client is already ready — skip handshake
|
|
66
|
+
if (isFederated) return;
|
|
67
|
+
|
|
60
68
|
initEventBus(pluginId);
|
|
61
69
|
|
|
62
70
|
// Check if we're in an iframe context - if not, we're standalone
|
|
@@ -71,7 +79,6 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children, plugin
|
|
|
71
79
|
if ((!standaloneDetected && !client) || (standaloneDetected && standaloneClient === true)) {
|
|
72
80
|
void RimoriClient.getInstance(pluginId).then((client) => {
|
|
73
81
|
setClient(client);
|
|
74
|
-
// Set initial userInfo
|
|
75
82
|
setUserInfo(client.plugin.getUserInfo());
|
|
76
83
|
|
|
77
84
|
// Get applicationMode and theme from MessageChannel query params
|
|
@@ -82,7 +89,7 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children, plugin
|
|
|
82
89
|
}
|
|
83
90
|
});
|
|
84
91
|
}
|
|
85
|
-
}, [pluginId, standaloneClient, client]);
|
|
92
|
+
}, [pluginId, standaloneClient, client, isFederated]);
|
|
86
93
|
|
|
87
94
|
// Identify user in PostHog when userInfo is available
|
|
88
95
|
useEffect(() => {
|
|
@@ -96,7 +103,7 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children, plugin
|
|
|
96
103
|
if (!client) return;
|
|
97
104
|
|
|
98
105
|
const unsubscribe = client.plugin.onRimoriInfoUpdate((info) => {
|
|
99
|
-
console.log('[PluginProvider] Received RimoriInfo update, updating userInfo');
|
|
106
|
+
// console.log('[PluginProvider] Received RimoriInfo update, updating userInfo + ttsEnabled:', info.ttsEnabled);
|
|
100
107
|
setUserInfo(info.profile);
|
|
101
108
|
});
|
|
102
109
|
|
|
@@ -106,6 +113,7 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children, plugin
|
|
|
106
113
|
useEffect(() => {
|
|
107
114
|
if (!client) return;
|
|
108
115
|
if (isSidebar) return; //sidebar pages should not report url changes
|
|
116
|
+
if (isFederated) return; // federation mode: host handles URL sync
|
|
109
117
|
|
|
110
118
|
// react router overwrites native pushstate so it gets wrapped to detect url changes
|
|
111
119
|
const originalPushState = history.pushState;
|
|
@@ -114,11 +122,11 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children, plugin
|
|
|
114
122
|
client.event.emit('session.triggerUrlChange', { url: location.hash });
|
|
115
123
|
return result;
|
|
116
124
|
};
|
|
117
|
-
}, [client, isSidebar]);
|
|
125
|
+
}, [client, isSidebar, isFederated]);
|
|
118
126
|
|
|
119
127
|
useEffect(() => {
|
|
120
128
|
if (!client) return;
|
|
121
|
-
if (
|
|
129
|
+
if (isFederated) return;
|
|
122
130
|
|
|
123
131
|
const checkScrollbar = (): void => {
|
|
124
132
|
const hasScrollbar = document.documentElement.scrollHeight > window.innerHeight;
|
|
@@ -140,7 +148,7 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children, plugin
|
|
|
140
148
|
window.removeEventListener('resize', checkScrollbar);
|
|
141
149
|
resizeObserver.disconnect();
|
|
142
150
|
};
|
|
143
|
-
}, [client,
|
|
151
|
+
}, [client, isFederated]);
|
|
144
152
|
|
|
145
153
|
if (standaloneClient instanceof StandaloneClient) {
|
|
146
154
|
return (
|
|
@@ -169,9 +177,9 @@ export const useRimori = (): RimoriClient & { userInfo: UserInfo } => {
|
|
|
169
177
|
if (context === null) {
|
|
170
178
|
throw new Error('useRimori must be used within an PluginProvider');
|
|
171
179
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
180
|
+
return Object.assign(context.client, {
|
|
181
|
+
userInfo: context.userInfo,
|
|
182
|
+
}) as RimoriClient & { userInfo: UserInfo };
|
|
175
183
|
};
|
|
176
184
|
|
|
177
185
|
function getUrlParam(name: string): string | null {
|
|
@@ -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
|
+
}
|