@rimori/react-client 0.4.11-next.3 → 0.4.11-next.5
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 +3 -1
- package/dist/components/ai/Avatar.js +8 -2
- package/dist/components/ai/BuddyAssistant.d.ts +3 -1
- package/dist/components/ai/BuddyAssistant.js +12 -4
- package/dist/components/audio/Playbutton.d.ts +4 -0
- package/dist/components/audio/Playbutton.js +4 -3
- package/dist/components/editor/MarkdownEditor.js +18 -5
- package/package.json +3 -3
- package/src/components/ai/Avatar.tsx +11 -1
- package/src/components/ai/BuddyAssistant.tsx +15 -3
- package/src/components/audio/Playbutton.tsx +10 -2
- package/src/components/editor/MarkdownEditor.tsx +70 -14
|
@@ -10,6 +10,8 @@ interface Props {
|
|
|
10
10
|
autoStartConversation?: FirstMessages;
|
|
11
11
|
className?: string;
|
|
12
12
|
knowledgeId?: string;
|
|
13
|
+
/** Set to true to disable automatic dialect TTS from userInfo. Default: false (dialect enabled). */
|
|
14
|
+
disableDialect?: boolean;
|
|
13
15
|
}
|
|
14
|
-
export declare function Avatar({ avatarImageUrl, voiceId, agentTools, autoStartConversation, children, circleSize, className, cache, knowledgeId, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export declare function Avatar({ avatarImageUrl, voiceId, agentTools, autoStartConversation, children, circleSize, className, cache, knowledgeId, disableDialect, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
15
17
|
export {};
|
|
@@ -7,12 +7,18 @@ 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, }) {
|
|
11
|
-
const { ai, event, plugin } = useRimori();
|
|
10
|
+
export function Avatar({ avatarImageUrl, voiceId, agentTools, autoStartConversation, children, circleSize = '300px', className, cache = false, knowledgeId, disableDialect = false, }) {
|
|
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)
|
|
16
|
+
? `Speak with a ${userInfo.dialect} accent and pronunciation.`
|
|
17
|
+
: undefined;
|
|
15
18
|
const sender = useMemo(() => new MessageSender((...args) => ai.getVoice(...args), voiceId, cache), [voiceId, ai, cache]);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
sender.setInstructions(dialectTtsInstruction);
|
|
21
|
+
}, [sender, dialectTtsInstruction]);
|
|
16
22
|
const { messages, append, isLoading, lastMessage, setMessages } = useChat(agentTools, { knowledgeId });
|
|
17
23
|
useEffect(() => {
|
|
18
24
|
console.log('messages', messages);
|
|
@@ -15,5 +15,7 @@ export interface BuddyAssistantProps {
|
|
|
15
15
|
className?: string;
|
|
16
16
|
voiceSpeed?: number;
|
|
17
17
|
tools?: Tool[];
|
|
18
|
+
/** Set to true to disable automatic dialect from userInfo. Default: false (dialect enabled). */
|
|
19
|
+
disableDialect?: boolean;
|
|
18
20
|
}
|
|
19
|
-
export declare function BuddyAssistant({ systemPrompt, autoStartConversation, circleSize, chatPlaceholder, bottomAction, className, voiceSpeed, tools, }: BuddyAssistantProps): JSX.Element;
|
|
21
|
+
export declare function BuddyAssistant({ systemPrompt, autoStartConversation, circleSize, chatPlaceholder, bottomAction, className, voiceSpeed, tools, disableDialect, }: BuddyAssistantProps): JSX.Element;
|
|
@@ -9,11 +9,16 @@ import { HiMiniSpeakerWave, HiMiniSpeakerXMark } from 'react-icons/hi2';
|
|
|
9
9
|
import { BiSolidRightArrow } from 'react-icons/bi';
|
|
10
10
|
let idCounter = 0;
|
|
11
11
|
const genId = () => `ba-${++idCounter}`;
|
|
12
|
-
export function BuddyAssistant({ systemPrompt, autoStartConversation, circleSize = '160px', chatPlaceholder, bottomAction, className, voiceSpeed = 1, tools, }) {
|
|
12
|
+
export function BuddyAssistant({ systemPrompt, autoStartConversation, circleSize = '160px', chatPlaceholder, bottomAction, className, voiceSpeed = 1, tools, disableDialect = false, }) {
|
|
13
13
|
var _a;
|
|
14
|
-
const { ai, event, plugin } = useRimori();
|
|
14
|
+
const { ai, event, plugin, userInfo } = useRimori();
|
|
15
15
|
const { isDark } = useTheme(plugin.theme);
|
|
16
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
|
+
: '';
|
|
21
|
+
const dialectTtsInstruction = dialect ? `Speak with a ${dialect} accent and pronunciation.` : undefined;
|
|
17
22
|
const [ttsEnabled, setTtsEnabled] = useState(true);
|
|
18
23
|
const [chatInput, setChatInput] = useState('');
|
|
19
24
|
const [messages, setMessages] = useState([]);
|
|
@@ -24,6 +29,9 @@ export function BuddyAssistant({ systemPrompt, autoStartConversation, circleSize
|
|
|
24
29
|
ttsEnabledRef.current = ttsEnabled;
|
|
25
30
|
}, [ttsEnabled]);
|
|
26
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
|
+
useEffect(() => {
|
|
33
|
+
sender.setInstructions(dialectTtsInstruction);
|
|
34
|
+
}, [sender, dialectTtsInstruction]);
|
|
27
35
|
// Setup sender callbacks and cleanup
|
|
28
36
|
useEffect(() => {
|
|
29
37
|
sender.setVoiceSpeed(voiceSpeed);
|
|
@@ -31,9 +39,9 @@ export function BuddyAssistant({ systemPrompt, autoStartConversation, circleSize
|
|
|
31
39
|
sender.setOnEndOfSpeech(() => setIsSpeaking(false));
|
|
32
40
|
return () => sender.cleanup();
|
|
33
41
|
}, [sender]);
|
|
34
|
-
// Build full API message list with system prompt
|
|
42
|
+
// Build full API message list with system prompt (dialect appended when enabled)
|
|
35
43
|
const buildApiMessages = (history) => [
|
|
36
|
-
{ role: 'system', content: systemPrompt },
|
|
44
|
+
{ role: 'system', content: systemPrompt + dialectSystemSuffix },
|
|
37
45
|
...history.map((m) => ({ role: m.role, content: m.content })),
|
|
38
46
|
];
|
|
39
47
|
const triggerAI = (history) => {
|
|
@@ -10,6 +10,10 @@ type AudioPlayerProps = {
|
|
|
10
10
|
enableSpeedAdjustment?: boolean;
|
|
11
11
|
playListenerEvent?: string;
|
|
12
12
|
size?: string;
|
|
13
|
+
/** Explicit TTS instruction string. If provided, overrides auto-dialect. */
|
|
14
|
+
ttsInstructions?: string;
|
|
15
|
+
/** Set to true to disable automatic dialect from userInfo. Default: false (dialect enabled). */
|
|
16
|
+
disableDialect?: boolean;
|
|
13
17
|
};
|
|
14
18
|
export declare const AudioPlayOptions: number[];
|
|
15
19
|
export type AudioPlayOptionType = 0.8 | 0.9 | 1.0 | 1.1 | 1.2 | 1.5;
|
|
@@ -14,12 +14,12 @@ import { useRimori } from '../../providers/PluginProvider';
|
|
|
14
14
|
import { EventBus } from '@rimori/client';
|
|
15
15
|
export const AudioPlayOptions = [0.8, 0.9, 1.0, 1.1, 1.2, 1.5];
|
|
16
16
|
let isFetchingAudio = false;
|
|
17
|
-
export const AudioPlayer = ({ text, voice, language, hide, playListenerEvent, initialSpeed = 1.0, playOnMount = false, enableSpeedAdjustment = false, cache = true, size = '25px', }) => {
|
|
17
|
+
export const AudioPlayer = ({ text, voice, language, hide, playListenerEvent, initialSpeed = 1.0, playOnMount = false, enableSpeedAdjustment = false, cache = true, size = '25px', ttsInstructions, disableDialect = false, }) => {
|
|
18
18
|
const [audioUrl, setAudioUrl] = useState(null);
|
|
19
19
|
const [speed, setSpeed] = useState(initialSpeed);
|
|
20
20
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
21
21
|
const [isLoading, setIsLoading] = useState(false);
|
|
22
|
-
const { ai } = useRimori();
|
|
22
|
+
const { ai, userInfo } = useRimori();
|
|
23
23
|
const audioRef = useRef(null);
|
|
24
24
|
const eventBusListenerRef = useRef(null);
|
|
25
25
|
useEffect(() => {
|
|
@@ -33,7 +33,8 @@ export const AudioPlayer = ({ text, voice, language, hide, playListenerEvent, in
|
|
|
33
33
|
// Function to generate audio from text using API
|
|
34
34
|
const generateAudio = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
35
35
|
setIsLoading(true);
|
|
36
|
-
const
|
|
36
|
+
const effectiveInstructions = ttsInstructions !== null && ttsInstructions !== void 0 ? ttsInstructions : (!disableDialect && (userInfo === null || userInfo === void 0 ? void 0 : userInfo.dialect) ? `Speak with a ${userInfo.dialect} accent and pronunciation.` : undefined);
|
|
37
|
+
const blob = yield ai.getVoice(text, voice || (language ? 'aws_default' : 'openai_alloy'), 1, language, cache, effectiveInstructions);
|
|
37
38
|
setAudioUrl(URL.createObjectURL(blob));
|
|
38
39
|
setIsLoading(false);
|
|
39
40
|
});
|
|
@@ -24,7 +24,7 @@ import { PiCodeBlock } from 'react-icons/pi';
|
|
|
24
24
|
import { TbBlockquote, TbTable, TbColumnInsertRight, TbRowInsertBottom, TbColumnRemove, TbRowRemove, TbArrowMergeBoth, TbBrandYoutube, TbPhoto, } from 'react-icons/tb';
|
|
25
25
|
import { GoListOrdered } from 'react-icons/go';
|
|
26
26
|
import { AiOutlineUnorderedList } from 'react-icons/ai';
|
|
27
|
-
import { LuClipboardPaste, LuHeading1, LuHeading2, LuHeading3, LuLink, LuUnlink } from 'react-icons/lu';
|
|
27
|
+
import { LuClipboardPaste, LuHeading1, LuHeading2, LuHeading3, LuLink, LuUnlink, LuCopy, LuCheck, LuMaximize2, LuMinimize2, } from 'react-icons/lu';
|
|
28
28
|
import { FaBold, FaCode, FaItalic, FaParagraph, FaStrikethrough } from 'react-icons/fa';
|
|
29
29
|
import { ImageUploadExtension, triggerImageUpload } from './ImageUploadExtension';
|
|
30
30
|
// Extends TipTap's Paragraph to serialize empty paragraphs as <p></p>.
|
|
@@ -126,7 +126,7 @@ const InlinePanel = ({ panel, onClose, editor, onUpdate, labels }) => {
|
|
|
126
126
|
? labels.addYoutubeConfirm
|
|
127
127
|
: labels.appendMarkdownConfirm })] })] }));
|
|
128
128
|
};
|
|
129
|
-
const MenuBar = ({ editor, onUpdate, uploadImage, labels }) => {
|
|
129
|
+
const MenuBar = ({ editor, onUpdate, uploadImage, labels, onCopy, copied, isFullscreen, onToggleFullscreen, }) => {
|
|
130
130
|
const [activePanel, setActivePanel] = useState(null);
|
|
131
131
|
const toggle = (panel) => setActivePanel((prev) => (prev === panel ? null : panel));
|
|
132
132
|
const inTable = editor.isActive('table');
|
|
@@ -135,7 +135,7 @@ const MenuBar = ({ editor, onUpdate, uploadImage, labels }) => {
|
|
|
135
135
|
'text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-40 disabled:pointer-events-none';
|
|
136
136
|
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "bg-muted/50 border-b border-border text-base flex flex-row flex-wrap items-center gap-0.5 p-1.5", children: [_jsx(EditorButton, { editor: editor, action: "toggleBold", isActive: editor.isActive('bold'), label: _jsx(FaBold, {}), disabled: true, title: labels.bold }), _jsx(EditorButton, { editor: editor, action: "toggleItalic", isActive: editor.isActive('italic'), label: _jsx(FaItalic, {}), disabled: true, title: labels.italic }), _jsx(EditorButton, { editor: editor, action: "toggleStrike", isActive: editor.isActive('strike'), label: _jsx(FaStrikethrough, {}), disabled: true, title: labels.strike }), _jsx(EditorButton, { editor: editor, action: "toggleCode", isActive: editor.isActive('code'), label: _jsx(FaCode, {}), disabled: true, title: labels.code }), _jsx(EditorButton, { editor: editor, action: "setParagraph", isActive: editor.isActive('paragraph'), label: _jsx(FaParagraph, {}), title: labels.paragraph }), _jsx(EditorButton, { editor: editor, action: "setHeading1", isActive: editor.isActive('heading', { level: 1 }), label: _jsx(LuHeading1, { size: "24px" }), title: labels.heading1 }), _jsx(EditorButton, { editor: editor, action: "setHeading2", isActive: editor.isActive('heading', { level: 2 }), label: _jsx(LuHeading2, { size: "24px" }), title: labels.heading2 }), _jsx(EditorButton, { editor: editor, action: "setHeading3", isActive: editor.isActive('heading', { level: 3 }), label: _jsx(LuHeading3, { size: "24px" }), title: labels.heading3 }), _jsx(EditorButton, { editor: editor, action: "toggleBulletList", isActive: editor.isActive('bulletList'), label: _jsx(AiOutlineUnorderedList, { size: "24px" }), title: labels.bulletList }), _jsx(EditorButton, { editor: editor, action: "toggleOrderedList", isActive: editor.isActive('orderedList'), label: _jsx(GoListOrdered, { size: "24px" }), title: labels.orderedList }), _jsx(EditorButton, { editor: editor, action: "toggleCodeBlock", isActive: editor.isActive('codeBlock'), label: _jsx(PiCodeBlock, { size: "24px" }), title: labels.codeBlock }), _jsx(EditorButton, { editor: editor, action: "toggleBlockquote", isActive: editor.isActive('blockquote'), label: _jsx(TbBlockquote, { size: "24px" }), title: labels.blockquote }), _jsx("div", { className: "w-px h-5 bg-border mx-0.5" }), _jsxs("div", { className: "inline-flex rounded-md border border-border bg-transparent overflow-hidden", children: [_jsx("button", { type: "button", onClick: () => toggle('link'), title: labels.setLink, className: 'w-8 h-8 flex items-center justify-center rounded-none first:rounded-l-md last:rounded-r-md transition-colors duration-150 ' +
|
|
137
137
|
'text-muted-foreground hover:bg-accent hover:text-accent-foreground border-r border-border last:border-r-0' +
|
|
138
|
-
(isLink ? ' bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-foreground' : ''), children: _jsx(LuLink, { size: 18 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().unsetLink().run(), disabled: !isLink, title: labels.unsetLink, className: "w-8 h-8 flex items-center justify-center rounded-none first:rounded-l-md last:rounded-r-md transition-colors duration-150 text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-40 disabled:pointer-events-none border-r border-border last:border-r-0", children: _jsx(LuUnlink, { size: 18 }) })] }), _jsx("button", { type: "button", onClick: () => toggle('youtube'), className: tableBtnClass, title: labels.addYoutube, children: _jsx(TbBrandYoutube, { size: 18 }) }), uploadImage && (_jsx("button", { type: "button", onClick: () => triggerImageUpload(uploadImage, editor), className: tableBtnClass, title: labels.insertImage, children: _jsx(TbPhoto, { size: 18 }) })), _jsx("div", { className: "w-px h-5 bg-border mx-0.5" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(), className: tableBtnClass, title: labels.insertTable, children: _jsx(TbTable, { size: 18 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().addColumnAfter().run(), className: tableBtnClass, disabled: !inTable, title: labels.addColumnAfter, children: _jsx(TbColumnInsertRight, { size: 18 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().addRowAfter().run(), className: tableBtnClass, disabled: !inTable, title: labels.addRowAfter, children: _jsx(TbRowInsertBottom, { size: 18 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().deleteColumn().run(), className: tableBtnClass, disabled: !inTable, title: labels.deleteColumn, children: _jsx(TbColumnRemove, { size: 18 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().deleteRow().run(), className: tableBtnClass, disabled: !inTable, title: labels.deleteRow, children: _jsx(TbRowRemove, { size: 18 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().mergeOrSplit().run(), className: tableBtnClass, disabled: !inTable, title: labels.mergeOrSplit, children: _jsx(TbArrowMergeBoth, { size: 18 }) }), _jsx("div", { className: "w-px h-5 bg-border mx-0.5" }), _jsx("button", { type: "button", onClick: () => toggle('markdown'), className: tableBtnClass, title: labels.appendMarkdown, children: _jsx(LuClipboardPaste, { size: 18 }) })] }), _jsx(InlinePanel, { panel: activePanel, onClose: () => setActivePanel(null), editor: editor, onUpdate: onUpdate, labels: labels })] }));
|
|
138
|
+
(isLink ? ' bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-foreground' : ''), children: _jsx(LuLink, { size: 18 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().unsetLink().run(), disabled: !isLink, title: labels.unsetLink, className: "w-8 h-8 flex items-center justify-center rounded-none first:rounded-l-md last:rounded-r-md transition-colors duration-150 text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-40 disabled:pointer-events-none border-r border-border last:border-r-0", children: _jsx(LuUnlink, { size: 18 }) })] }), _jsx("button", { type: "button", onClick: () => toggle('youtube'), className: tableBtnClass, title: labels.addYoutube, children: _jsx(TbBrandYoutube, { size: 18 }) }), uploadImage && (_jsx("button", { type: "button", onClick: () => triggerImageUpload(uploadImage, editor), className: tableBtnClass, title: labels.insertImage, children: _jsx(TbPhoto, { size: 18 }) })), _jsx("div", { className: "w-px h-5 bg-border mx-0.5" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(), className: tableBtnClass, title: labels.insertTable, children: _jsx(TbTable, { size: 18 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().addColumnAfter().run(), className: tableBtnClass, disabled: !inTable, title: labels.addColumnAfter, children: _jsx(TbColumnInsertRight, { size: 18 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().addRowAfter().run(), className: tableBtnClass, disabled: !inTable, title: labels.addRowAfter, children: _jsx(TbRowInsertBottom, { size: 18 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().deleteColumn().run(), className: tableBtnClass, disabled: !inTable, title: labels.deleteColumn, children: _jsx(TbColumnRemove, { size: 18 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().deleteRow().run(), className: tableBtnClass, disabled: !inTable, title: labels.deleteRow, children: _jsx(TbRowRemove, { size: 18 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().mergeOrSplit().run(), className: tableBtnClass, disabled: !inTable, title: labels.mergeOrSplit, children: _jsx(TbArrowMergeBoth, { size: 18 }) }), _jsx("div", { className: "w-px h-5 bg-border mx-0.5" }), _jsx("button", { type: "button", onClick: () => toggle('markdown'), className: tableBtnClass, title: labels.appendMarkdown, children: _jsx(LuClipboardPaste, { size: 18, style: { transform: 'scaleX(-1)' } }) }), _jsxs("div", { className: 'ml-auto flex items-center gap-0.5 ' + (isFullscreen ? 'pr-10' : ''), children: [_jsx("button", { type: "button", onClick: onCopy, title: "Copy as Markdown", className: tableBtnClass, children: copied ? _jsx(LuCheck, { size: 16, className: "text-green-500" }) : _jsx(LuCopy, { size: 16 }) }), _jsx("button", { type: "button", onClick: onToggleFullscreen, title: isFullscreen ? 'Exit fullscreen' : 'Fullscreen', className: tableBtnClass, children: isFullscreen ? _jsx(LuMinimize2, { size: 16 }) : _jsx(LuMaximize2, { size: 16 }) })] })] }), _jsx(InlinePanel, { panel: activePanel, onClose: () => setActivePanel(null), editor: editor, onUpdate: onUpdate, labels: labels })] }));
|
|
139
139
|
};
|
|
140
140
|
const DEFAULT_LABELS = {
|
|
141
141
|
bold: 'Bold',
|
|
@@ -176,6 +176,8 @@ export const MarkdownEditor = ({ content, editable, className, onUpdate, labels,
|
|
|
176
176
|
const { storage } = useRimori();
|
|
177
177
|
const mergedLabels = Object.assign(Object.assign({}, DEFAULT_LABELS), labels);
|
|
178
178
|
const lastEmittedRef = useRef(content !== null && content !== void 0 ? content : '');
|
|
179
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
180
|
+
const [copied, setCopied] = useState(false);
|
|
179
181
|
const stableUpload = useCallback((pngBlob) => __awaiter(void 0, void 0, void 0, function* () {
|
|
180
182
|
const { data, error } = yield storage.uploadImage(pngBlob);
|
|
181
183
|
if (error)
|
|
@@ -208,6 +210,14 @@ export const MarkdownEditor = ({ content, editable, className, onUpdate, labels,
|
|
|
208
210
|
onUpdate === null || onUpdate === void 0 ? void 0 : onUpdate(markdown);
|
|
209
211
|
},
|
|
210
212
|
});
|
|
213
|
+
const handleCopy = useCallback(() => {
|
|
214
|
+
if (!editor)
|
|
215
|
+
return;
|
|
216
|
+
navigator.clipboard.writeText(getMarkdown(editor)).then(() => {
|
|
217
|
+
setCopied(true);
|
|
218
|
+
setTimeout(() => setCopied(false), 2000);
|
|
219
|
+
});
|
|
220
|
+
}, [editor]);
|
|
211
221
|
// Sync external content changes (e.g. AI autofill) without triggering update loop.
|
|
212
222
|
// Pass false to setContent to prevent onUpdate from firing and re-normalizing the content.
|
|
213
223
|
useEffect(() => {
|
|
@@ -225,8 +235,11 @@ export const MarkdownEditor = ({ content, editable, className, onUpdate, labels,
|
|
|
225
235
|
return;
|
|
226
236
|
editor.setEditable(editable);
|
|
227
237
|
}, [editor, editable]);
|
|
228
|
-
|
|
238
|
+
const wrapperClass = isFullscreen
|
|
239
|
+
? 'text-md overflow-hidden rounded-lg border border-border bg-card shadow-sm fixed inset-0 z-50 flex flex-col'
|
|
240
|
+
: 'text-md overflow-hidden rounded-lg ' +
|
|
229
241
|
(editable ? 'border border-border bg-card shadow-sm' : 'bg-transparent') +
|
|
230
242
|
' ' +
|
|
231
|
-
(className !== null && className !== void 0 ? className : '')
|
|
243
|
+
(className !== null && className !== void 0 ? className : '');
|
|
244
|
+
return (_jsxs("div", { className: wrapperClass, onClick: onContentClick, children: [editor && editable && (_jsx(MenuBar, { editor: editor, onUpdate: onUpdate !== null && onUpdate !== void 0 ? onUpdate : (() => { }), uploadImage: stableUpload, labels: mergedLabels, onCopy: handleCopy, copied: copied, isFullscreen: isFullscreen, onToggleFullscreen: () => setIsFullscreen((v) => !v) })), _jsx(EditorContent, { editor: editor, className: isFullscreen ? 'flex-1 overflow-y-auto' : '' })] }));
|
|
232
245
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimori/react-client",
|
|
3
|
-
"version": "0.4.11-next.
|
|
3
|
+
"version": "0.4.11-next.5",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"format": "prettier --write ."
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
|
-
"@rimori/client": "2.5.
|
|
27
|
+
"@rimori/client": "2.5.20-next.1",
|
|
28
28
|
"react": "^18.1.0",
|
|
29
29
|
"react-dom": "^18.1.0"
|
|
30
30
|
},
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@eslint/js": "^9.37.0",
|
|
50
|
-
"@rimori/client": "2.5.
|
|
50
|
+
"@rimori/client": "2.5.20-next.1",
|
|
51
51
|
"@types/react": "^18.3.21",
|
|
52
52
|
"eslint-config-prettier": "^10.1.8",
|
|
53
53
|
"eslint-plugin-prettier": "^5.5.4",
|
|
@@ -19,6 +19,8 @@ interface Props {
|
|
|
19
19
|
autoStartConversation?: FirstMessages;
|
|
20
20
|
className?: string;
|
|
21
21
|
knowledgeId?: string;
|
|
22
|
+
/** Set to true to disable automatic dialect TTS from userInfo. Default: false (dialect enabled). */
|
|
23
|
+
disableDialect?: boolean;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export function Avatar({
|
|
@@ -31,15 +33,23 @@ export function Avatar({
|
|
|
31
33
|
className,
|
|
32
34
|
cache = false,
|
|
33
35
|
knowledgeId,
|
|
36
|
+
disableDialect = false,
|
|
34
37
|
}: Props) {
|
|
35
|
-
const { ai, event, plugin } = useRimori();
|
|
38
|
+
const { ai, event, plugin, userInfo } = useRimori();
|
|
36
39
|
const { isDark: isDarkThemeValue } = useTheme(plugin.theme);
|
|
37
40
|
const [agentReplying, setAgentReplying] = useState(false);
|
|
38
41
|
const [isProcessingMessage, setIsProcessingMessage] = useState(false);
|
|
42
|
+
const dialectTtsInstruction = !disableDialect && userInfo?.dialect
|
|
43
|
+
? `Speak with a ${userInfo.dialect} accent and pronunciation.`
|
|
44
|
+
: undefined;
|
|
39
45
|
const sender = useMemo(
|
|
40
46
|
() => new MessageSender((...args) => ai.getVoice(...args), voiceId, cache),
|
|
41
47
|
[voiceId, ai, cache],
|
|
42
48
|
);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
sender.setInstructions(dialectTtsInstruction);
|
|
52
|
+
}, [sender, dialectTtsInstruction]);
|
|
43
53
|
const { messages, append, isLoading, lastMessage, setMessages } = useChat(agentTools, { knowledgeId });
|
|
44
54
|
|
|
45
55
|
useEffect(() => {
|
|
@@ -25,6 +25,8 @@ export interface BuddyAssistantProps {
|
|
|
25
25
|
className?: string;
|
|
26
26
|
voiceSpeed?: number;
|
|
27
27
|
tools?: Tool[];
|
|
28
|
+
/** Set to true to disable automatic dialect from userInfo. Default: false (dialect enabled). */
|
|
29
|
+
disableDialect?: boolean;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
let idCounter = 0;
|
|
@@ -39,10 +41,16 @@ export function BuddyAssistant({
|
|
|
39
41
|
className,
|
|
40
42
|
voiceSpeed = 1,
|
|
41
43
|
tools,
|
|
44
|
+
disableDialect = false,
|
|
42
45
|
}: BuddyAssistantProps): JSX.Element {
|
|
43
|
-
const { ai, event, plugin } = useRimori();
|
|
46
|
+
const { ai, event, plugin, userInfo } = useRimori();
|
|
44
47
|
const { isDark } = useTheme(plugin.theme);
|
|
45
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
|
+
: '';
|
|
53
|
+
const dialectTtsInstruction = dialect ? `Speak with a ${dialect} accent and pronunciation.` : undefined;
|
|
46
54
|
|
|
47
55
|
const [ttsEnabled, setTtsEnabled] = useState(true);
|
|
48
56
|
const [chatInput, setChatInput] = useState('');
|
|
@@ -60,6 +68,10 @@ export function BuddyAssistant({
|
|
|
60
68
|
[buddy?.voiceId, ai],
|
|
61
69
|
);
|
|
62
70
|
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
sender.setInstructions(dialectTtsInstruction);
|
|
73
|
+
}, [sender, dialectTtsInstruction]);
|
|
74
|
+
|
|
63
75
|
// Setup sender callbacks and cleanup
|
|
64
76
|
useEffect(() => {
|
|
65
77
|
sender.setVoiceSpeed(voiceSpeed);
|
|
@@ -68,9 +80,9 @@ export function BuddyAssistant({
|
|
|
68
80
|
return () => sender.cleanup();
|
|
69
81
|
}, [sender]);
|
|
70
82
|
|
|
71
|
-
// Build full API message list with system prompt
|
|
83
|
+
// Build full API message list with system prompt (dialect appended when enabled)
|
|
72
84
|
const buildApiMessages = (history: ChatMessage[]) => [
|
|
73
|
-
{ role: 'system' as const, content: systemPrompt },
|
|
85
|
+
{ role: 'system' as const, content: systemPrompt + dialectSystemSuffix },
|
|
74
86
|
...history.map((m) => ({ role: m.role, content: m.content })),
|
|
75
87
|
];
|
|
76
88
|
|
|
@@ -14,6 +14,10 @@ type AudioPlayerProps = {
|
|
|
14
14
|
enableSpeedAdjustment?: boolean;
|
|
15
15
|
playListenerEvent?: string;
|
|
16
16
|
size?: string;
|
|
17
|
+
/** Explicit TTS instruction string. If provided, overrides auto-dialect. */
|
|
18
|
+
ttsInstructions?: string;
|
|
19
|
+
/** Set to true to disable automatic dialect from userInfo. Default: false (dialect enabled). */
|
|
20
|
+
disableDialect?: boolean;
|
|
17
21
|
};
|
|
18
22
|
|
|
19
23
|
export const AudioPlayOptions = [0.8, 0.9, 1.0, 1.1, 1.2, 1.5];
|
|
@@ -32,12 +36,14 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
|
|
|
32
36
|
enableSpeedAdjustment = false,
|
|
33
37
|
cache = true,
|
|
34
38
|
size = '25px',
|
|
39
|
+
ttsInstructions,
|
|
40
|
+
disableDialect = false,
|
|
35
41
|
}) => {
|
|
36
42
|
const [audioUrl, setAudioUrl] = useState<string | null>(null);
|
|
37
43
|
const [speed, setSpeed] = useState(initialSpeed);
|
|
38
44
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
39
45
|
const [isLoading, setIsLoading] = useState(false);
|
|
40
|
-
const { ai } = useRimori();
|
|
46
|
+
const { ai, userInfo } = useRimori();
|
|
41
47
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
42
48
|
const eventBusListenerRef = useRef<{ off: () => void } | null>(null);
|
|
43
49
|
|
|
@@ -52,7 +58,9 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
|
|
|
52
58
|
const generateAudio = async () => {
|
|
53
59
|
setIsLoading(true);
|
|
54
60
|
|
|
55
|
-
const
|
|
61
|
+
const effectiveInstructions = ttsInstructions
|
|
62
|
+
?? (!disableDialect && userInfo?.dialect ? `Speak with a ${userInfo.dialect} accent and pronunciation.` : undefined);
|
|
63
|
+
const blob = await ai.getVoice(text, voice || (language ? 'aws_default' : 'openai_alloy'), 1, language, cache, effectiveInstructions);
|
|
56
64
|
setAudioUrl(URL.createObjectURL(blob));
|
|
57
65
|
setIsLoading(false);
|
|
58
66
|
};
|
|
@@ -25,7 +25,18 @@ import {
|
|
|
25
25
|
} from 'react-icons/tb';
|
|
26
26
|
import { GoListOrdered } from 'react-icons/go';
|
|
27
27
|
import { AiOutlineUnorderedList } from 'react-icons/ai';
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
LuClipboardPaste,
|
|
30
|
+
LuHeading1,
|
|
31
|
+
LuHeading2,
|
|
32
|
+
LuHeading3,
|
|
33
|
+
LuLink,
|
|
34
|
+
LuUnlink,
|
|
35
|
+
LuCopy,
|
|
36
|
+
LuCheck,
|
|
37
|
+
LuMaximize2,
|
|
38
|
+
LuMinimize2,
|
|
39
|
+
} from 'react-icons/lu';
|
|
29
40
|
import { FaBold, FaCode, FaItalic, FaParagraph, FaStrikethrough } from 'react-icons/fa';
|
|
30
41
|
import { ImageUploadExtension, triggerImageUpload } from './ImageUploadExtension';
|
|
31
42
|
|
|
@@ -230,9 +241,22 @@ interface MenuBarProps {
|
|
|
230
241
|
onUpdate: (content: string) => void;
|
|
231
242
|
uploadImage?: (pngBlob: Blob) => Promise<string | null>;
|
|
232
243
|
labels: Required<EditorLabels>;
|
|
244
|
+
onCopy: () => void;
|
|
245
|
+
copied: boolean;
|
|
246
|
+
isFullscreen: boolean;
|
|
247
|
+
onToggleFullscreen: () => void;
|
|
233
248
|
}
|
|
234
249
|
|
|
235
|
-
const MenuBar = ({
|
|
250
|
+
const MenuBar = ({
|
|
251
|
+
editor,
|
|
252
|
+
onUpdate,
|
|
253
|
+
uploadImage,
|
|
254
|
+
labels,
|
|
255
|
+
onCopy,
|
|
256
|
+
copied,
|
|
257
|
+
isFullscreen,
|
|
258
|
+
onToggleFullscreen,
|
|
259
|
+
}: MenuBarProps): JSX.Element => {
|
|
236
260
|
const [activePanel, setActivePanel] = useState<PanelType>(null);
|
|
237
261
|
|
|
238
262
|
const toggle = (panel: PanelType): void => setActivePanel((prev) => (prev === panel ? null : panel));
|
|
@@ -453,8 +477,22 @@ const MenuBar = ({ editor, onUpdate, uploadImage, labels }: MenuBarProps): JSX.E
|
|
|
453
477
|
className={tableBtnClass}
|
|
454
478
|
title={labels.appendMarkdown}
|
|
455
479
|
>
|
|
456
|
-
<LuClipboardPaste size={18} />
|
|
480
|
+
<LuClipboardPaste size={18} style={{ transform: 'scaleX(-1)' }} />
|
|
457
481
|
</button>
|
|
482
|
+
|
|
483
|
+
<div className={'ml-auto flex items-center gap-0.5 ' + (isFullscreen ? 'pr-10' : '')}>
|
|
484
|
+
<button type="button" onClick={onCopy} title="Copy as Markdown" className={tableBtnClass}>
|
|
485
|
+
{copied ? <LuCheck size={16} className="text-green-500" /> : <LuCopy size={16} />}
|
|
486
|
+
</button>
|
|
487
|
+
<button
|
|
488
|
+
type="button"
|
|
489
|
+
onClick={onToggleFullscreen}
|
|
490
|
+
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
|
|
491
|
+
className={tableBtnClass}
|
|
492
|
+
>
|
|
493
|
+
{isFullscreen ? <LuMinimize2 size={16} /> : <LuMaximize2 size={16} />}
|
|
494
|
+
</button>
|
|
495
|
+
</div>
|
|
458
496
|
</div>
|
|
459
497
|
|
|
460
498
|
<InlinePanel
|
|
@@ -570,6 +608,8 @@ export const MarkdownEditor = ({
|
|
|
570
608
|
const { storage } = useRimori();
|
|
571
609
|
const mergedLabels: Required<EditorLabels> = { ...DEFAULT_LABELS, ...labels };
|
|
572
610
|
const lastEmittedRef = useRef(content ?? '');
|
|
611
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
612
|
+
const [copied, setCopied] = useState(false);
|
|
573
613
|
|
|
574
614
|
const stableUpload = useCallback(
|
|
575
615
|
async (pngBlob: Blob): Promise<string | null> => {
|
|
@@ -611,6 +651,14 @@ export const MarkdownEditor = ({
|
|
|
611
651
|
},
|
|
612
652
|
});
|
|
613
653
|
|
|
654
|
+
const handleCopy = useCallback(() => {
|
|
655
|
+
if (!editor) return;
|
|
656
|
+
navigator.clipboard.writeText(getMarkdown(editor)).then(() => {
|
|
657
|
+
setCopied(true);
|
|
658
|
+
setTimeout(() => setCopied(false), 2000);
|
|
659
|
+
});
|
|
660
|
+
}, [editor]);
|
|
661
|
+
|
|
614
662
|
// Sync external content changes (e.g. AI autofill) without triggering update loop.
|
|
615
663
|
// Pass false to setContent to prevent onUpdate from firing and re-normalizing the content.
|
|
616
664
|
useEffect(() => {
|
|
@@ -627,20 +675,28 @@ export const MarkdownEditor = ({
|
|
|
627
675
|
editor.setEditable(editable);
|
|
628
676
|
}, [editor, editable]);
|
|
629
677
|
|
|
678
|
+
const wrapperClass = isFullscreen
|
|
679
|
+
? 'text-md overflow-hidden rounded-lg border border-border bg-card shadow-sm fixed inset-0 z-50 flex flex-col'
|
|
680
|
+
: 'text-md overflow-hidden rounded-lg ' +
|
|
681
|
+
(editable ? 'border border-border bg-card shadow-sm' : 'bg-transparent') +
|
|
682
|
+
' ' +
|
|
683
|
+
(className ?? '');
|
|
684
|
+
|
|
630
685
|
return (
|
|
631
|
-
<div
|
|
632
|
-
className={
|
|
633
|
-
'text-md overflow-hidden rounded-lg ' +
|
|
634
|
-
(editable ? 'border border-border bg-card shadow-sm' : 'bg-transparent') +
|
|
635
|
-
' ' +
|
|
636
|
-
(className ?? '')
|
|
637
|
-
}
|
|
638
|
-
onClick={onContentClick}
|
|
639
|
-
>
|
|
686
|
+
<div className={wrapperClass} onClick={onContentClick}>
|
|
640
687
|
{editor && editable && (
|
|
641
|
-
<MenuBar
|
|
688
|
+
<MenuBar
|
|
689
|
+
editor={editor}
|
|
690
|
+
onUpdate={onUpdate ?? (() => {})}
|
|
691
|
+
uploadImage={stableUpload}
|
|
692
|
+
labels={mergedLabels}
|
|
693
|
+
onCopy={handleCopy}
|
|
694
|
+
copied={copied}
|
|
695
|
+
isFullscreen={isFullscreen}
|
|
696
|
+
onToggleFullscreen={() => setIsFullscreen((v) => !v)}
|
|
697
|
+
/>
|
|
642
698
|
)}
|
|
643
|
-
<EditorContent editor={editor} />
|
|
699
|
+
<EditorContent editor={editor} className={isFullscreen ? 'flex-1 overflow-y-auto' : ''} />
|
|
644
700
|
</div>
|
|
645
701
|
);
|
|
646
702
|
};
|