@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.
@@ -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 blob = yield ai.getVoice(text, voice || (language ? 'aws_default' : 'openai_alloy'), 1, language, cache);
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
- return (_jsxs("div", { className: 'text-md overflow-hidden rounded-lg ' +
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 : ''), onClick: onContentClick, children: [editor && editable && (_jsx(MenuBar, { editor: editor, onUpdate: onUpdate !== null && onUpdate !== void 0 ? onUpdate : (() => { }), uploadImage: stableUpload, labels: mergedLabels })), _jsx(EditorContent, { editor: editor })] }));
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",
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.19-next.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.19-next.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 blob = await ai.getVoice(text, voice || (language ? 'aws_default' : 'openai_alloy'), 1, language, cache);
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 { LuClipboardPaste, LuHeading1, LuHeading2, LuHeading3, LuLink, LuUnlink } from 'react-icons/lu';
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 = ({ editor, onUpdate, uploadImage, labels }: MenuBarProps): JSX.Element => {
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 editor={editor} onUpdate={onUpdate ?? (() => {})} uploadImage={stableUpload} labels={mergedLabels} />
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
  };