@rimori/react-client 0.4.17 → 0.4.18-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.
@@ -9,9 +9,10 @@ interface Props {
9
9
  children?: React.ReactNode;
10
10
  autoStartConversation?: FirstMessages;
11
11
  className?: string;
12
- knowledgeId?: string;
13
- /** Set to true to disable automatic dialect TTS from userInfo. Default: false (dialect enabled). */
14
- disableDialect?: boolean;
12
+ /** Server-side prompt name (e.g. 'studyplan.reflection'). When set, the backend resolves the system prompt. */
13
+ prompt?: string;
14
+ /** Variables for the server-side prompt template. */
15
+ promptVariables?: Record<string, any>;
15
16
  }
16
- export declare function Avatar({ avatarImageUrl, voiceId, agentTools, autoStartConversation, children, circleSize, className, cache, knowledgeId, disableDialect, }: Props): import("react/jsx-runtime").JSX.Element;
17
+ export declare function Avatar({ avatarImageUrl, voiceId, agentTools, autoStartConversation, children, circleSize, className, cache, prompt, promptVariables, }: Props): import("react/jsx-runtime").JSX.Element;
17
18
  export {};
@@ -7,17 +7,22 @@ import { useChat } from '../../hooks/UseChatHook';
7
7
  import { useRimori } from '../../providers/PluginProvider';
8
8
  import { getFirstMessages } from './utils';
9
9
  import { useTheme } from '../../hooks/ThemeSetter';
10
- export function Avatar({ avatarImageUrl, voiceId, agentTools, autoStartConversation, children, circleSize = '300px', className, cache = false, knowledgeId, disableDialect = false, }) {
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
12
  const { isDark: isDarkThemeValue } = useTheme(plugin.theme);
13
13
  const [agentReplying, setAgentReplying] = useState(false);
14
14
  const [isProcessingMessage, setIsProcessingMessage] = useState(false);
15
- const dialectTtsInstruction = !disableDialect && (userInfo === null || userInfo === void 0 ? void 0 : userInfo.dialect) ? `Speak with a ${userInfo.dialect} accent and pronunciation.` : undefined;
15
+ const dialectTtsInstruction = (userInfo === null || userInfo === void 0 ? void 0 : userInfo.dialect)
16
+ ? `Speak with a ${userInfo.dialect} accent and pronunciation.`
17
+ : undefined;
16
18
  const sender = useMemo(() => new MessageSender((...args) => ai.getVoice(...args), voiceId, cache), [voiceId, ai, cache]);
17
19
  useEffect(() => {
18
20
  sender.setInstructions(dialectTtsInstruction);
19
21
  }, [sender, dialectTtsInstruction]);
20
- const { messages, append, isLoading, lastMessage, setMessages } = useChat(agentTools, { knowledgeId });
22
+ const { messages, append, isLoading, lastMessage, setMessages } = useChat(agentTools, {
23
+ prompt,
24
+ promptVariables,
25
+ });
21
26
  useEffect(() => {
22
27
  console.log('messages', messages);
23
28
  }, [messages]);
@@ -7,7 +7,10 @@ export interface BuddyAssistantAutoStart {
7
7
  userMessage?: string;
8
8
  }
9
9
  export interface BuddyAssistantProps {
10
- systemPrompt: string;
10
+ /** Server-side prompt name (e.g. 'studyplan.goalSummary'). The backend resolves the system prompt. */
11
+ prompt: string;
12
+ /** Variables for the server-side prompt template. */
13
+ promptVariables?: Record<string, any>;
11
14
  autoStartConversation?: BuddyAssistantAutoStart;
12
15
  circleSize?: string;
13
16
  chatPlaceholder?: string;
@@ -15,7 +18,7 @@ export interface BuddyAssistantProps {
15
18
  className?: string;
16
19
  voiceSpeed?: number;
17
20
  tools?: Tool[];
18
- /** Set to true to disable automatic dialect from userInfo. Default: false (dialect enabled). */
19
- disableDialect?: boolean;
21
+ /** Show the buddy name below the avatar. Default: false. */
22
+ showName?: boolean;
20
23
  }
21
- export declare function BuddyAssistant({ systemPrompt, autoStartConversation, circleSize, chatPlaceholder, bottomAction, className, voiceSpeed, tools, disableDialect, }: BuddyAssistantProps): JSX.Element;
24
+ export declare function BuddyAssistant({ prompt, promptVariables, autoStartConversation, circleSize, chatPlaceholder, bottomAction, className, voiceSpeed, tools, showName, }: BuddyAssistantProps): JSX.Element;
@@ -5,21 +5,16 @@ import { VoiceRecorder } from './EmbeddedAssistent/VoiceRecorder';
5
5
  import { MessageSender } from '@rimori/client';
6
6
  import { useRimori } from '../../providers/PluginProvider';
7
7
  import { useTheme } from '../../hooks/ThemeSetter';
8
- import { HiMiniSpeakerWave, HiMiniSpeakerXMark } from 'react-icons/hi2';
9
8
  import { BiSolidRightArrow } from 'react-icons/bi';
10
9
  let idCounter = 0;
11
10
  const genId = () => `ba-${++idCounter}`;
12
- export function BuddyAssistant({ systemPrompt, autoStartConversation, circleSize = '160px', chatPlaceholder, bottomAction, className, voiceSpeed = 1, tools, disableDialect = false, }) {
13
- var _a;
11
+ export function BuddyAssistant({ prompt, promptVariables, autoStartConversation, circleSize = '160px', chatPlaceholder, bottomAction, className, voiceSpeed = 1, tools, showName = false, }) {
14
12
  const { ai, event, plugin, userInfo } = useRimori();
13
+ const ttsEnabled = plugin.ttsEnabled;
15
14
  const { isDark } = useTheme(plugin.theme);
16
- const buddy = (_a = plugin.getUserInfo()) === null || _a === void 0 ? void 0 : _a.study_buddy;
17
- const dialect = !disableDialect ? userInfo === null || userInfo === void 0 ? void 0 : userInfo.dialect : undefined;
18
- const dialectSystemSuffix = dialect
19
- ? `\n\nThe user is learning the regional ${dialect} dialect. Occasionally use typical regional vocabulary and expressions from this dialect to help them learn local language naturally.`
20
- : '';
15
+ const buddy = userInfo.study_buddy;
16
+ const dialect = userInfo === null || userInfo === void 0 ? void 0 : userInfo.dialect;
21
17
  const dialectTtsInstruction = dialect ? `Speak with a ${dialect} accent and pronunciation.` : undefined;
22
- const [ttsEnabled, setTtsEnabled] = useState(true);
23
18
  const [chatInput, setChatInput] = useState('');
24
19
  const [messages, setMessages] = useState([]);
25
20
  const [isLoading, setIsLoading] = useState(false);
@@ -27,6 +22,11 @@ export function BuddyAssistant({ systemPrompt, autoStartConversation, circleSize
27
22
  const ttsEnabledRef = useRef(ttsEnabled);
28
23
  useEffect(() => {
29
24
  ttsEnabledRef.current = ttsEnabled;
25
+ // Stop speaking when TTS is turned off globally
26
+ if (!ttsEnabled && isSpeaking) {
27
+ sender.stop();
28
+ setIsSpeaking(false);
29
+ }
30
30
  }, [ttsEnabled]);
31
31
  const sender = useMemo(() => { var _a; return new MessageSender((...args) => ai.getVoice(...args), (_a = buddy === null || buddy === void 0 ? void 0 : buddy.voiceId) !== null && _a !== void 0 ? _a : ''); }, [buddy === null || buddy === void 0 ? void 0 : buddy.voiceId, ai]);
32
32
  useEffect(() => {
@@ -39,29 +39,33 @@ export function BuddyAssistant({ systemPrompt, autoStartConversation, circleSize
39
39
  sender.setOnEndOfSpeech(() => setIsSpeaking(false));
40
40
  return () => sender.cleanup();
41
41
  }, [sender]);
42
- // Build full API message list with system prompt (dialect appended when enabled)
43
- const buildApiMessages = (history) => [
44
- { role: 'system', content: systemPrompt + dialectSystemSuffix },
45
- ...history.map((m) => ({ role: m.role, content: m.content })),
46
- ];
42
+ const buildApiMessages = (history) => {
43
+ return history.map((m) => ({ role: m.role, content: m.content }));
44
+ };
47
45
  const triggerAI = (history) => {
48
46
  setIsLoading(true);
49
- void ai.getSteamedText(buildApiMessages(history), (id, partial, finished) => {
50
- setIsLoading(!finished);
51
- const assistantId = `ai-${id}`;
52
- setMessages((prev) => {
53
- const last = prev[prev.length - 1];
54
- if ((last === null || last === void 0 ? void 0 : last.id) === assistantId) {
55
- return [...prev.slice(0, -1), Object.assign(Object.assign({}, last), { content: partial })];
47
+ void ai.getStreamedText({
48
+ messages: buildApiMessages(history),
49
+ onMessage: (id, partial, finished) => {
50
+ setIsLoading(!finished);
51
+ const assistantId = `ai-${id}`;
52
+ setMessages((prev) => {
53
+ const last = prev[prev.length - 1];
54
+ if ((last === null || last === void 0 ? void 0 : last.id) === assistantId) {
55
+ return [...prev.slice(0, -1), Object.assign(Object.assign({}, last), { content: partial })];
56
+ }
57
+ return [...prev, { id: assistantId, role: 'assistant', content: partial }];
58
+ });
59
+ if (ttsEnabledRef.current) {
60
+ void sender.handleNewText(partial, !finished);
61
+ if (partial)
62
+ setIsSpeaking(true);
56
63
  }
57
- return [...prev, { id: assistantId, role: 'assistant', content: partial }];
58
- });
59
- if (ttsEnabledRef.current) {
60
- void sender.handleNewText(partial, !finished);
61
- if (partial)
62
- setIsSpeaking(true);
63
- }
64
- }, tools);
64
+ },
65
+ tools,
66
+ prompt,
67
+ variables: promptVariables,
68
+ });
65
69
  };
66
70
  // Auto-start conversation on mount
67
71
  const autoStartedRef = useRef(false);
@@ -94,21 +98,14 @@ export function BuddyAssistant({ systemPrompt, autoStartConversation, circleSize
94
98
  setMessages(newMessages);
95
99
  triggerAI(newMessages);
96
100
  };
97
- const handleToggleTts = () => {
98
- if (ttsEnabled && isSpeaking) {
99
- sender.stop();
100
- setIsSpeaking(false);
101
- }
102
- setTtsEnabled((prev) => !prev);
103
- };
104
101
  const lastAssistantMessage = [...messages].filter((m) => m.role === 'assistant').pop();
105
- return (_jsxs("div", { className: `flex flex-col items-center ${className || ''}`, children: [_jsx(CircleAudioAvatar, { width: circleSize, imageUrl: buddy.avatarUrl, isDarkTheme: isDark, className: "mx-auto" }), _jsxs("div", { className: "flex items-center gap-2 pl-10", children: [_jsx("span", { className: "text-3xl font-semibold", children: buddy.name }), _jsx("button", { type: "button", onClick: handleToggleTts, className: "p-1 rounded-md hover:bg-gray-700/50 transition-colors", title: ttsEnabled ? 'Disable voice' : 'Enable voice', children: ttsEnabled ? (_jsx(HiMiniSpeakerWave, { className: `w-5 h-5 mt-0.5 ${isSpeaking ? 'text-blue-400' : 'text-gray-300'}` })) : (_jsx(HiMiniSpeakerXMark, { className: "w-5 h-5 mt-0.5 text-gray-500" })) })] }), !ttsEnabled && (_jsx("div", { className: "w-full max-w-md rounded-xl bg-gray-800/70 px-4 py-3 text-sm text-gray-200 leading-relaxed border 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 })) })), _jsxs("div", { className: "w-full max-w-md relative mt-4", 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 }) })), !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) => {
106
103
  if (e.key === 'Enter' && !e.shiftKey) {
107
104
  e.preventDefault();
108
105
  sendMessage(chatInput);
109
106
  setChatInput('');
110
107
  }
111
- }, placeholder: chatPlaceholder !== null && chatPlaceholder !== void 0 ? chatPlaceholder : `Ask ${buddy.name} a question…`, disabled: isLoading, className: "w-full bg-gray-800/50 border border-gray-700 rounded-lg px-3 py-2 pr-16 text-sm text-gray-200 placeholder:text-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-60" }), _jsxs("div", { className: "absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1", children: [_jsx(VoiceRecorder, { iconSize: "14", className: "p-1 text-gray-400 hover:text-white transition-colors", disabled: isLoading, onVoiceRecorded: (text) => sendMessage(text), onRecordingStatusChange: () => { } }), _jsx("div", { className: "w-px h-3.5 bg-gray-600" }), _jsx("button", { type: "button", onClick: () => {
108
+ }, placeholder: chatPlaceholder !== null && chatPlaceholder !== void 0 ? chatPlaceholder : `Ask ${buddy.name} a question…`, disabled: isLoading, className: "w-full bg-gray-800/50 border border-gray-700 rounded-lg px-3 py-2 pr-16 text-sm text-gray-200 placeholder:text-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-60" }), _jsxs("div", { className: "absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1", children: [_jsx(VoiceRecorder, { iconSize: "14", className: "p-1 text-gray-400 hover:text-white transition-colors", disabled: isLoading, onVoiceRecorded: (text) => sendMessage(text), onRecordingStatusChange: () => { } }), _jsx("button", { type: "button", onClick: () => {
112
109
  sendMessage(chatInput);
113
110
  setChatInput('');
114
111
  }, disabled: isLoading || !chatInput.trim(), className: "p-1 text-gray-400 hover:text-white disabled:opacity-40 transition-colors", children: _jsx(BiSolidRightArrow, { className: "w-4 h-4" }) })] })] }), bottomAction && _jsx("div", { className: "w-full max-w-md border-t border-gray-700/60 pt-3", children: bottomAction })] }));
@@ -1,8 +1,5 @@
1
- /** @deprecated Use server-side prompt definitions (prompt + variables) instead of building messages client-side. */
2
1
  export interface FirstMessages {
3
- instructions?: string;
4
2
  userMessage?: string;
5
3
  assistantMessage?: string;
6
4
  }
7
- /** @deprecated Use server-side prompt definitions (prompt + variables) instead of building messages client-side. */
8
- export declare function getFirstMessages(instructions: FirstMessages): any[];
5
+ export declare function getFirstMessages(config: FirstMessages): any[];
@@ -1,14 +1,10 @@
1
- /** @deprecated Use server-side prompt definitions (prompt + variables) instead of building messages client-side. */
2
- export function getFirstMessages(instructions) {
1
+ export function getFirstMessages(config) {
3
2
  const messages = [];
4
- if (instructions.instructions) {
5
- messages.push({ id: '1', role: 'system', content: instructions.instructions });
3
+ if (config.userMessage) {
4
+ messages.push({ id: '1', role: 'user', content: config.userMessage });
6
5
  }
7
- if (instructions.userMessage) {
8
- messages.push({ id: '2', role: 'user', content: instructions.userMessage });
9
- }
10
- if (instructions.assistantMessage) {
11
- messages.push({ id: '3', role: 'assistant', content: instructions.assistantMessage });
6
+ if (config.assistantMessage) {
7
+ messages.push({ id: '2', role: 'assistant', content: config.assistantMessage });
12
8
  }
13
9
  return messages;
14
10
  }
@@ -250,16 +250,11 @@ export const MarkdownEditor = ({ content, editable, className, onUpdate, labels,
250
250
  return;
251
251
  const selectedText = editor.state.doc.textBetween(from, to, '\n');
252
252
  setIsTransforming(true);
253
- const transformed = yield ai.getText([
254
- {
255
- role: 'system',
256
- content: 'You are a text editor assistant. Transform the provided text according to the user instruction. Return only the transformed text, no explanations. If formattation is wished, use markdown syntax in the output.',
257
- },
258
- {
259
- role: 'user',
260
- content: `Instruction: ${prompt}\n\nText:\n${selectedText}`,
261
- },
262
- ]);
253
+ const transformed = yield ai.getText({
254
+ messages: [],
255
+ prompt: 'global.editor.transform-text',
256
+ variables: { instruction: prompt, text: selectedText },
257
+ });
263
258
  setIsTransforming(false);
264
259
  if (!transformed)
265
260
  return;
@@ -1,5 +1,6 @@
1
1
  import { Theme } from '@rimori/client';
2
- export declare function useTheme(theme?: Theme): {
2
+ export declare function useTheme(theme?: Theme, isDisabled?: boolean): {
3
3
  isDark: boolean;
4
4
  theme: Theme;
5
+ isDisabled: boolean;
5
6
  };
@@ -1,12 +1,15 @@
1
- export function useTheme(theme = 'system') {
1
+ export function useTheme(theme = 'system', isDisabled = false) {
2
2
  const isDark = theme === 'system' ? systenUsesDarkMode() : theme === 'dark';
3
+ if (isDisabled) {
4
+ return { isDark, theme, isDisabled };
5
+ }
3
6
  const dom = document.documentElement;
4
7
  dom.dataset.theme = isDark ? 'dark' : 'light';
5
8
  dom.style.colorScheme = isDark ? 'dark' : 'light';
6
9
  dom.classList[isDark ? 'add' : 'remove']('dark');
7
10
  const root = document.querySelector('#root');
8
11
  root.classList.add('dark:bg-gradient-to-br', 'dark:from-gray-900', 'dark:to-gray-800', 'dark:text-white', 'min-h-screen', 'bg-fixed');
9
- return { isDark, theme };
12
+ return { isDark, theme, isDisabled };
10
13
  }
11
14
  function systenUsesDarkMode() {
12
15
  return window.matchMedia('(prefers-color-scheme: dark)').matches;
@@ -8,8 +8,6 @@ export interface UseChatConfig {
8
8
  promptVariables?: Record<string, any>;
9
9
  }
10
10
  export declare function useChat(tools?: Tool[], options?: {
11
- /** @deprecated Use uuid variable with resolver 'knowledgeEntry' in prompt definitions instead. */
12
- knowledgeId?: string;
13
11
  /** Server-side prompt name (e.g. 'storytelling.story'). When set, the backend resolves the system prompt. */
14
12
  prompt?: string;
15
13
  /** Variables for the server-side prompt template. */
@@ -21,19 +21,24 @@ export function useChat(tools, options) {
21
21
  const allMessages = [...messages, ...appendMessages];
22
22
  setMessages(allMessages);
23
23
  setIsLoading(true);
24
- const knowledgeId = options === null || options === void 0 ? void 0 : options.knowledgeId;
25
24
  const promptVariables = (_a = configRef.current.promptVariables) !== null && _a !== void 0 ? _a : options === null || options === void 0 ? void 0 : options.promptVariables;
26
25
  const prompt = (_b = configRef.current.prompt) !== null && _b !== void 0 ? _b : options === null || options === void 0 ? void 0 : options.prompt;
27
- void ai.getSteamedText(allMessages, (id, message, finished, toolInvocations) => {
28
- setIsLoading(!finished);
29
- setMessages((prev) => {
30
- const last = prev[prev.length - 1];
31
- if ((last === null || last === void 0 ? void 0 : last.id) === id) {
32
- return [...prev.slice(0, -1), Object.assign(Object.assign({}, last), { content: message, toolCalls: toolInvocations })];
33
- }
34
- return [...prev, { id, role: 'assistant', content: message, toolCalls: toolInvocations }];
35
- });
36
- }, tools, false, undefined, knowledgeId, prompt, promptVariables);
26
+ void ai.getStreamedText({
27
+ messages: allMessages,
28
+ onMessage: (id, message, finished, toolInvocations) => {
29
+ setIsLoading(!finished);
30
+ setMessages((prev) => {
31
+ const last = prev[prev.length - 1];
32
+ if ((last === null || last === void 0 ? void 0 : last.id) === id) {
33
+ return [...prev.slice(0, -1), Object.assign(Object.assign({}, last), { content: message, toolCalls: toolInvocations })];
34
+ }
35
+ return [...prev, { id, role: 'assistant', content: message, toolCalls: toolInvocations }];
36
+ });
37
+ },
38
+ tools,
39
+ prompt,
40
+ variables: promptVariables,
41
+ });
37
42
  };
38
43
  return {
39
44
  messages,
@@ -6,6 +6,12 @@ interface PluginProviderProps {
6
6
  pluginId: string;
7
7
  settings?: {
8
8
  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;
9
15
  };
10
16
  }
11
17
  export declare const PluginProvider: React.FC<PluginProviderProps>;
@@ -20,7 +20,7 @@ export const PluginProvider = ({ children, pluginId, settings }) => {
20
20
  const [applicationMode, setApplicationMode] = useState(null);
21
21
  const [theme, setTheme] = useState(undefined);
22
22
  const [userInfo, setUserInfo] = useState(null);
23
- useTheme(theme);
23
+ useTheme(theme, settings === null || settings === void 0 ? void 0 : settings.disableThemeSetting);
24
24
  // Init PostHog once per plugin iframe
25
25
  useEffect(() => {
26
26
  if (!posthog.__loaded) {
@@ -94,6 +94,8 @@ export const PluginProvider = ({ children, pluginId, settings }) => {
94
94
  useEffect(() => {
95
95
  if (!client)
96
96
  return;
97
+ if (settings === null || settings === void 0 ? void 0 : settings.disableScrollbarDetection)
98
+ return;
97
99
  const checkScrollbar = () => {
98
100
  const hasScrollbar = document.documentElement.scrollHeight > window.innerHeight;
99
101
  client.event.emit('session.triggerScrollbarChange', { hasScrollbar });
@@ -109,7 +111,7 @@ export const PluginProvider = ({ children, pluginId, settings }) => {
109
111
  window.removeEventListener('resize', checkScrollbar);
110
112
  resizeObserver.disconnect();
111
113
  };
112
- }, [client]);
114
+ }, [client, settings === null || settings === void 0 ? void 0 : settings.disableScrollbarDetection]);
113
115
  if (standaloneClient instanceof StandaloneClient) {
114
116
  return (_jsx(StandaloneAuth, { onLogin: (email, password) => __awaiter(void 0, void 0, void 0, function* () {
115
117
  if (yield standaloneClient.login(email, password))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimori/react-client",
3
- "version": "0.4.17",
3
+ "version": "0.4.18-next.0",
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.28",
28
+ "@rimori/client": "^2.5.29",
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.28",
52
+ "@rimori/client": "^2.5.29",
53
53
  "@types/react": "^18.3.21",
54
54
  "eslint-config-prettier": "^10.1.8",
55
55
  "eslint-plugin-prettier": "^5.5.4",
@@ -18,9 +18,10 @@ interface Props {
18
18
  children?: React.ReactNode;
19
19
  autoStartConversation?: FirstMessages;
20
20
  className?: string;
21
- knowledgeId?: string;
22
- /** Set to true to disable automatic dialect TTS from userInfo. Default: false (dialect enabled). */
23
- disableDialect?: boolean;
21
+ /** Server-side prompt name (e.g. 'studyplan.reflection'). When set, the backend resolves the system prompt. */
22
+ prompt?: string;
23
+ /** Variables for the server-side prompt template. */
24
+ promptVariables?: Record<string, any>;
24
25
  }
25
26
 
26
27
  export function Avatar({
@@ -32,15 +33,16 @@ export function Avatar({
32
33
  circleSize = '300px',
33
34
  className,
34
35
  cache = false,
35
- knowledgeId,
36
- disableDialect = false,
36
+ prompt,
37
+ promptVariables,
37
38
  }: Props) {
38
39
  const { ai, event, plugin, userInfo } = useRimori();
39
40
  const { isDark: isDarkThemeValue } = useTheme(plugin.theme);
40
41
  const [agentReplying, setAgentReplying] = useState(false);
41
42
  const [isProcessingMessage, setIsProcessingMessage] = useState(false);
42
- const dialectTtsInstruction =
43
- !disableDialect && userInfo?.dialect ? `Speak with a ${userInfo.dialect} accent and pronunciation.` : undefined;
43
+ const dialectTtsInstruction = userInfo?.dialect
44
+ ? `Speak with a ${userInfo.dialect} accent and pronunciation.`
45
+ : undefined;
44
46
  const sender = useMemo(
45
47
  () => new MessageSender((...args) => ai.getVoice(...args), voiceId, cache),
46
48
  [voiceId, ai, cache],
@@ -49,7 +51,10 @@ export function Avatar({
49
51
  useEffect(() => {
50
52
  sender.setInstructions(dialectTtsInstruction);
51
53
  }, [sender, dialectTtsInstruction]);
52
- const { messages, append, isLoading, lastMessage, setMessages } = useChat(agentTools, { knowledgeId });
54
+ const { messages, append, isLoading, lastMessage, setMessages } = useChat(agentTools, {
55
+ prompt,
56
+ promptVariables,
57
+ });
53
58
 
54
59
  useEffect(() => {
55
60
  console.log('messages', messages);
@@ -4,7 +4,6 @@ import { VoiceRecorder } from './EmbeddedAssistent/VoiceRecorder';
4
4
  import { MessageSender, Tool } from '@rimori/client';
5
5
  import { useRimori } from '../../providers/PluginProvider';
6
6
  import { useTheme } from '../../hooks/ThemeSetter';
7
- import { HiMiniSpeakerWave, HiMiniSpeakerXMark } from 'react-icons/hi2';
8
7
  import { BiSolidRightArrow } from 'react-icons/bi';
9
8
 
10
9
  type ChatMessage = { id: string; role: 'user' | 'assistant'; content: string };
@@ -17,7 +16,10 @@ export interface BuddyAssistantAutoStart {
17
16
  }
18
17
 
19
18
  export interface BuddyAssistantProps {
20
- systemPrompt: string;
19
+ /** Server-side prompt name (e.g. 'studyplan.goalSummary'). The backend resolves the system prompt. */
20
+ prompt: string;
21
+ /** Variables for the server-side prompt template. */
22
+ promptVariables?: Record<string, any>;
21
23
  autoStartConversation?: BuddyAssistantAutoStart;
22
24
  circleSize?: string;
23
25
  chatPlaceholder?: string;
@@ -25,15 +27,16 @@ export interface BuddyAssistantProps {
25
27
  className?: string;
26
28
  voiceSpeed?: number;
27
29
  tools?: Tool[];
28
- /** Set to true to disable automatic dialect from userInfo. Default: false (dialect enabled). */
29
- disableDialect?: boolean;
30
+ /** Show the buddy name below the avatar. Default: false. */
31
+ showName?: boolean;
30
32
  }
31
33
 
32
34
  let idCounter = 0;
33
35
  const genId = () => `ba-${++idCounter}`;
34
36
 
35
37
  export function BuddyAssistant({
36
- systemPrompt,
38
+ prompt,
39
+ promptVariables,
37
40
  autoStartConversation,
38
41
  circleSize = '160px',
39
42
  chatPlaceholder,
@@ -41,18 +44,14 @@ export function BuddyAssistant({
41
44
  className,
42
45
  voiceSpeed = 1,
43
46
  tools,
44
- disableDialect = false,
47
+ showName = false,
45
48
  }: BuddyAssistantProps): JSX.Element {
46
49
  const { ai, event, plugin, userInfo } = useRimori();
50
+ const ttsEnabled = plugin.ttsEnabled;
47
51
  const { isDark } = useTheme(plugin.theme);
48
- const buddy = plugin.getUserInfo()?.study_buddy;
49
- const dialect = !disableDialect ? userInfo?.dialect : undefined;
50
- const dialectSystemSuffix = dialect
51
- ? `\n\nThe user is learning the regional ${dialect} dialect. Occasionally use typical regional vocabulary and expressions from this dialect to help them learn local language naturally.`
52
- : '';
52
+ const buddy = userInfo.study_buddy;
53
+ const dialect = userInfo?.dialect;
53
54
  const dialectTtsInstruction = dialect ? `Speak with a ${dialect} accent and pronunciation.` : undefined;
54
-
55
- const [ttsEnabled, setTtsEnabled] = useState(true);
56
55
  const [chatInput, setChatInput] = useState('');
57
56
  const [messages, setMessages] = useState<ChatMessage[]>([]);
58
57
  const [isLoading, setIsLoading] = useState(false);
@@ -61,6 +60,11 @@ export function BuddyAssistant({
61
60
  const ttsEnabledRef = useRef(ttsEnabled);
62
61
  useEffect(() => {
63
62
  ttsEnabledRef.current = ttsEnabled;
63
+ // Stop speaking when TTS is turned off globally
64
+ if (!ttsEnabled && isSpeaking) {
65
+ sender.stop();
66
+ setIsSpeaking(false);
67
+ }
64
68
  }, [ttsEnabled]);
65
69
 
66
70
  const sender = useMemo(
@@ -80,17 +84,15 @@ export function BuddyAssistant({
80
84
  return () => sender.cleanup();
81
85
  }, [sender]);
82
86
 
83
- // Build full API message list with system prompt (dialect appended when enabled)
84
- const buildApiMessages = (history: ChatMessage[]) => [
85
- { role: 'system' as const, content: systemPrompt + dialectSystemSuffix },
86
- ...history.map((m) => ({ role: m.role, content: m.content })),
87
- ];
87
+ const buildApiMessages = (history: ChatMessage[]) => {
88
+ return history.map((m) => ({ role: m.role, content: m.content }));
89
+ };
88
90
 
89
91
  const triggerAI = (history: ChatMessage[]) => {
90
92
  setIsLoading(true);
91
- void ai.getSteamedText(
92
- buildApiMessages(history),
93
- (id: string, partial: string, finished: boolean) => {
93
+ void ai.getStreamedText({
94
+ messages: buildApiMessages(history),
95
+ onMessage: (id: string, partial: string, finished: boolean) => {
94
96
  setIsLoading(!finished);
95
97
  const assistantId = `ai-${id}`;
96
98
  setMessages((prev) => {
@@ -106,7 +108,9 @@ export function BuddyAssistant({
106
108
  }
107
109
  },
108
110
  tools,
109
- );
111
+ prompt,
112
+ variables: promptVariables,
113
+ });
110
114
  };
111
115
 
112
116
  // Auto-start conversation on mount
@@ -140,14 +144,6 @@ export function BuddyAssistant({
140
144
  triggerAI(newMessages);
141
145
  };
142
146
 
143
- const handleToggleTts = () => {
144
- if (ttsEnabled && isSpeaking) {
145
- sender.stop();
146
- setIsSpeaking(false);
147
- }
148
- setTtsEnabled((prev) => !prev);
149
- };
150
-
151
147
  const lastAssistantMessage = [...messages].filter((m) => m.role === 'assistant').pop();
152
148
 
153
149
  return (
@@ -155,26 +151,15 @@ export function BuddyAssistant({
155
151
  {/* Animated circle avatar */}
156
152
  <CircleAudioAvatar width={circleSize} imageUrl={buddy.avatarUrl} isDarkTheme={isDark} className="mx-auto" />
157
153
 
158
- {/* Buddy name + TTS toggle */}
159
- <div className="flex items-center gap-2 pl-10">
160
- <span className="text-3xl font-semibold">{buddy.name}</span>
161
- <button
162
- type="button"
163
- onClick={handleToggleTts}
164
- className="p-1 rounded-md hover:bg-gray-700/50 transition-colors"
165
- title={ttsEnabled ? 'Disable voice' : 'Enable voice'}
166
- >
167
- {ttsEnabled ? (
168
- <HiMiniSpeakerWave className={`w-5 h-5 mt-0.5 ${isSpeaking ? 'text-blue-400' : 'text-gray-300'}`} />
169
- ) : (
170
- <HiMiniSpeakerXMark className="w-5 h-5 mt-0.5 text-gray-500" />
171
- )}
172
- </button>
173
- </div>
154
+ {/* Buddy name (hidden by default) */}
155
+ {showName && (
156
+ <div className="flex items-center gap-2">
157
+ <span className="text-3xl font-semibold">{buddy.name}</span>
158
+ </div>
159
+ )}
174
160
 
175
- {/* Last buddy message card — only shown when TTS is disabled */}
176
- {!ttsEnabled && (
177
- <div className="w-full max-w-md rounded-xl bg-gray-800/70 px-4 py-3 text-sm text-gray-200 leading-relaxed border border-gray-700/40 mt-4">
161
+ {!ttsEnabled ? (
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">
178
163
  {!lastAssistantMessage?.content && isLoading ? (
179
164
  <span className="inline-flex gap-1 py-0.5">
180
165
  <span className="w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce" />
@@ -191,10 +176,10 @@ export function BuddyAssistant({
191
176
  <span className="whitespace-pre-wrap">{lastAssistantMessage?.content}</span>
192
177
  )}
193
178
  </div>
194
- )}
179
+ ) : null}
195
180
 
196
181
  {/* Chat input */}
197
- <div className="w-full max-w-md relative mt-4">
182
+ <div className={'w-full relative mt-4 ' + (ttsEnabled ? 'max-w-md' : '')}>
198
183
  <input
199
184
  value={chatInput}
200
185
  onChange={(e) => setChatInput(e.target.value)}
@@ -217,7 +202,7 @@ export function BuddyAssistant({
217
202
  onVoiceRecorded={(text) => sendMessage(text)}
218
203
  onRecordingStatusChange={() => {}}
219
204
  />
220
- <div className="w-px h-3.5 bg-gray-600" />
205
+ {/* <div className="w-px h-3.5 bg-gray-600" /> */}
221
206
  <button
222
207
  type="button"
223
208
  onClick={() => {