@rimori/react-client 0.4.18-next.0 → 0.4.18-next.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.
@@ -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)
@@ -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);
@@ -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 [client, setClient] = useState(null);
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, settings === null || settings === void 0 ? void 0 : settings.disableThemeSetting);
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 (settings === null || settings === void 0 ? void 0 : settings.disableScrollbarDetection)
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, settings === null || settings === void 0 ? void 0 : settings.disableScrollbarDetection]);
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
- // Return client with userInfo at root level for easy access
132
- // Maintains backwards compatibility - all client properties are still accessible
133
- return Object.assign(context.client, { userInfo: context.userInfo });
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimori/react-client",
3
- "version": "0.4.18-next.0",
3
+ "version": "0.4.18-next.1",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,7 +25,7 @@
25
25
  "format": "prettier --write ."
26
26
  },
27
27
  "peerDependencies": {
28
- "@rimori/client": "^2.5.29",
28
+ "@rimori/client": "2.5.29-next.1",
29
29
  "react": "^18.1.0",
30
30
  "react-dom": "^18.1.0"
31
31
  },
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "devDependencies": {
51
51
  "@eslint/js": "^9.37.0",
52
- "@rimori/client": "^2.5.29",
52
+ "@rimori/client": "2.5.29-next.1",
53
53
  "@types/react": "^18.3.21",
54
54
  "eslint-config-prettier": "^10.1.8",
55
55
  "eslint-plugin-prettier": "^5.5.4",
@@ -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
@@ -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">
@@ -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> = ({ children, pluginId, settings }) => {
31
- const [client, setClient] = useState<RimoriClient | null>(null);
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, settings?.disableThemeSetting);
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 (settings?.disableScrollbarDetection) return;
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, settings?.disableScrollbarDetection]);
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
- // Return client with userInfo at root level for easy access
173
- // Maintains backwards compatibility - all client properties are still accessible
174
- return Object.assign(context.client, { userInfo: context.userInfo }) as RimoriClient & { userInfo: UserInfo };
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 {