@rimori/react-client 0.4.17 → 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.
- package/dist/components/ai/Avatar.d.ts +5 -4
- package/dist/components/ai/Avatar.js +9 -4
- package/dist/components/ai/BuddyAssistant.d.ts +7 -4
- package/dist/components/ai/BuddyAssistant.js +36 -39
- package/dist/components/ai/utils.d.ts +1 -4
- package/dist/components/ai/utils.js +5 -9
- package/dist/components/editor/MarkdownEditor.js +5 -10
- package/dist/hooks/ThemeSetter.d.ts +2 -1
- package/dist/hooks/ThemeSetter.js +5 -2
- package/dist/hooks/UseChatHook.d.ts +0 -2
- package/dist/hooks/UseChatHook.js +16 -11
- package/dist/providers/PluginProvider.d.ts +4 -0
- package/dist/providers/PluginProvider.js +19 -12
- package/package.json +3 -3
- package/src/components/ai/Avatar.tsx +14 -9
- package/src/components/ai/BuddyAssistant.tsx +38 -53
- package/src/components/ai/utils.ts +5 -11
- package/src/components/editor/MarkdownEditor.tsx +5 -11
- package/src/hooks/ThemeSetter.ts +9 -2
- package/src/hooks/UseChatHook.ts +5 -11
- package/src/providers/PluginProvider.tsx +27 -12
- package/README copy.md +0 -1216
|
@@ -9,9 +9,10 @@ interface Props {
|
|
|
9
9
|
children?: React.ReactNode;
|
|
10
10
|
autoStartConversation?: FirstMessages;
|
|
11
11
|
className?: string;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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,
|
|
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,
|
|
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
|
-
const dialectTtsInstruction =
|
|
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, {
|
|
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
|
-
|
|
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
|
-
/**
|
|
19
|
-
|
|
21
|
+
/** Show the buddy name below the avatar. Default: false. */
|
|
22
|
+
showName?: boolean;
|
|
20
23
|
}
|
|
21
|
-
export declare function BuddyAssistant({
|
|
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({
|
|
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();
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
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
|
-
: '';
|
|
13
|
+
const ttsEnabled = plugin.ttsEnabled;
|
|
14
|
+
const { isDark } = useTheme(plugin.theme, true);
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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" }),
|
|
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) => {
|
|
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("
|
|
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
|
-
|
|
8
|
-
export declare function getFirstMessages(instructions: FirstMessages): any[];
|
|
5
|
+
export declare function getFirstMessages(config: FirstMessages): any[];
|
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
export function getFirstMessages(instructions) {
|
|
1
|
+
export function getFirstMessages(config) {
|
|
3
2
|
const messages = [];
|
|
4
|
-
if (
|
|
5
|
-
messages.push({ id: '1', role: '
|
|
3
|
+
if (config.userMessage) {
|
|
4
|
+
messages.push({ id: '1', role: 'user', content: config.userMessage });
|
|
6
5
|
}
|
|
7
|
-
if (
|
|
8
|
-
messages.push({ id: '2', role: '
|
|
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
|
-
|
|
256
|
-
|
|
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,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.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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,
|
|
@@ -4,6 +4,10 @@ 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
13
|
};
|
|
@@ -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,10 +95,12 @@ 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;
|
|
102
|
+
if (isFederated)
|
|
103
|
+
return;
|
|
97
104
|
const checkScrollbar = () => {
|
|
98
105
|
const hasScrollbar = document.documentElement.scrollHeight > window.innerHeight;
|
|
99
106
|
client.event.emit('session.triggerScrollbarChange', { hasScrollbar });
|
|
@@ -109,7 +116,7 @@ export const PluginProvider = ({ children, pluginId, settings }) => {
|
|
|
109
116
|
window.removeEventListener('resize', checkScrollbar);
|
|
110
117
|
resizeObserver.disconnect();
|
|
111
118
|
};
|
|
112
|
-
}, [client]);
|
|
119
|
+
}, [client, isFederated]);
|
|
113
120
|
if (standaloneClient instanceof StandaloneClient) {
|
|
114
121
|
return (_jsx(StandaloneAuth, { onLogin: (email, password) => __awaiter(void 0, void 0, void 0, function* () {
|
|
115
122
|
if (yield standaloneClient.login(email, password))
|
|
@@ -126,9 +133,9 @@ export const useRimori = () => {
|
|
|
126
133
|
if (context === null) {
|
|
127
134
|
throw new Error('useRimori must be used within an PluginProvider');
|
|
128
135
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
return Object.assign(context.client, {
|
|
137
|
+
userInfo: context.userInfo,
|
|
138
|
+
});
|
|
132
139
|
};
|
|
133
140
|
function getUrlParam(name) {
|
|
134
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.
|
|
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": "
|
|
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": "
|
|
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",
|
|
@@ -18,9 +18,10 @@ interface Props {
|
|
|
18
18
|
children?: React.ReactNode;
|
|
19
19
|
autoStartConversation?: FirstMessages;
|
|
20
20
|
className?: string;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
36
|
-
|
|
36
|
+
prompt,
|
|
37
|
+
promptVariables,
|
|
37
38
|
}: Props) {
|
|
38
39
|
const { ai, event, plugin, userInfo } = useRimori();
|
|
39
|
-
const { isDark: isDarkThemeValue } = useTheme(plugin.theme);
|
|
40
|
+
const { isDark: isDarkThemeValue } = useTheme(plugin.theme, true);
|
|
40
41
|
const [agentReplying, setAgentReplying] = useState(false);
|
|
41
42
|
const [isProcessingMessage, setIsProcessingMessage] = useState(false);
|
|
42
|
-
const dialectTtsInstruction =
|
|
43
|
-
|
|
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, {
|
|
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);
|