@rimori/react-client 0.4.9-next.2 → 0.4.9-next.3

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.
@@ -3,10 +3,10 @@ import React, { useEffect, useMemo } from 'react';
3
3
  import { CircleAudioAvatar } from './EmbeddedAssistent/CircleAudioAvatar';
4
4
  import { AudioInputField } from './EmbeddedAssistent/AudioInputField';
5
5
  import { MessageSender } from '@rimori/client';
6
- import Markdown from 'react-markdown';
7
6
  import { useChat } from '../../hooks/UseChatHook';
8
7
  import { useRimori } from '../../providers/PluginProvider';
9
8
  import { getFirstMessages } from './utils';
9
+ import { MarkdownEditor } from '../editor/MarkdownEditor';
10
10
  export function AssistantChat({ avatarImageUrl, voiceId, onComplete, autoStartConversation }) {
11
11
  var _a;
12
12
  const [oralCommunication, setOralCommunication] = React.useState(true);
@@ -55,7 +55,7 @@ export function AssistantChat({ avatarImageUrl, voiceId, onComplete, autoStartCo
55
55
  const success = args.explanationUnderstood === 'TRUE' || args.studentKnowsTopic === 'TRUE';
56
56
  return (_jsxs("div", { className: "px-5 pt-5 overflow-y-auto text-center", style: { height: '478px' }, children: [_jsx("h1", { className: "text-center mt-5 mb-5", children: success ? 'Great job!' : 'You failed' }), _jsx("p", { children: args.improvementHints })] }));
57
57
  }
58
- return (_jsxs("div", { children: [oralCommunication && _jsx(CircleAudioAvatar, { imageUrl: avatarImageUrl, className: "mx-auto my-10" }), _jsx("div", { className: "w-full", children: lastAssistantMessage && (_jsx("div", { className: "px-5 pt-5 overflow-y-auto remirror-theme", style: { height: '4k78px' }, children: _jsx(Markdown, { children: lastAssistantMessage }) })) }), _jsx(AudioInputField, { blockSubmission: isLoading, onSubmit: (message) => {
58
+ return (_jsxs("div", { children: [oralCommunication && _jsx(CircleAudioAvatar, { imageUrl: avatarImageUrl, className: "mx-auto my-10" }), _jsx("div", { className: "w-full", children: lastAssistantMessage && (_jsx("div", { className: "px-5 pt-5 overflow-y-auto remirror-theme", style: { height: '4k78px' }, children: _jsx(MarkdownEditor, { editable: false, content: lastAssistantMessage }) })) }), _jsx(AudioInputField, { blockSubmission: isLoading, onSubmit: (message) => {
59
59
  append([{ role: 'user', content: message, id: messages.length.toString() }]);
60
60
  }, onAudioControl: (voice) => {
61
61
  setOralCommunication(voice);
@@ -0,0 +1,13 @@
1
+ import type { Editor } from '@tiptap/core';
2
+ /**
3
+ * Tiptap extension that adds image upload via:
4
+ * - Drag-and-drop of image files onto the editor
5
+ * - Paste of image data from the clipboard
6
+ * - A dedicated toolbar button (triggers a hidden <input type="file">)
7
+ *
8
+ * Provide the `uploadImage` callback to handle uploading to your backend.
9
+ * If no callback is provided the extension is still registered but upload is disabled.
10
+ */
11
+ export declare const ImageUploadExtension: (uploadImage?: (pngBlob: Blob) => Promise<string | null>) => import("@tiptap/core").Node<import("@tiptap/extension-image").ImageOptions, any>;
12
+ /** Trigger an image file picker and upload the selected file. */
13
+ export declare function triggerImageUpload(uploadImage: (pngBlob: Blob) => Promise<string | null>, editor: Editor): void;
@@ -0,0 +1,93 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import Image from '@tiptap/extension-image';
11
+ import { Plugin, PluginKey } from '@tiptap/pm/state';
12
+ import { convertToPng } from './imageUtils';
13
+ const pluginKey = new PluginKey('imageUpload');
14
+ /**
15
+ * Tiptap extension that adds image upload via:
16
+ * - Drag-and-drop of image files onto the editor
17
+ * - Paste of image data from the clipboard
18
+ * - A dedicated toolbar button (triggers a hidden <input type="file">)
19
+ *
20
+ * Provide the `uploadImage` callback to handle uploading to your backend.
21
+ * If no callback is provided the extension is still registered but upload is disabled.
22
+ */
23
+ export const ImageUploadExtension = (uploadImage) => Image.extend({
24
+ addProseMirrorPlugins() {
25
+ const editor = this.editor;
26
+ return [
27
+ new Plugin({
28
+ key: pluginKey,
29
+ props: {
30
+ handleDrop(view, event) {
31
+ var _a, _b;
32
+ if (!uploadImage)
33
+ return false;
34
+ const files = Array.from((_b = (_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) !== null && _b !== void 0 ? _b : []);
35
+ const imageFile = files.find((f) => f.type.startsWith('image/'));
36
+ if (!imageFile)
37
+ return false;
38
+ event.preventDefault();
39
+ // Insert a placeholder position then replace with real URL
40
+ void handleImageFile(imageFile, uploadImage, editor);
41
+ return true;
42
+ },
43
+ handlePaste(view, event) {
44
+ var _a, _b;
45
+ if (!uploadImage)
46
+ return false;
47
+ const items = Array.from((_b = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.items) !== null && _b !== void 0 ? _b : []);
48
+ const imageItem = items.find((i) => i.type.startsWith('image/'));
49
+ if (!imageItem)
50
+ return false;
51
+ const file = imageItem.getAsFile();
52
+ if (!file)
53
+ return false;
54
+ event.preventDefault();
55
+ void handleImageFile(file, uploadImage, editor);
56
+ return true;
57
+ },
58
+ },
59
+ }),
60
+ ];
61
+ },
62
+ }).configure({
63
+ HTMLAttributes: {
64
+ class: 'max-w-full h-auto rounded-lg my-4',
65
+ },
66
+ });
67
+ function handleImageFile(file, uploadImage, editor) {
68
+ return __awaiter(this, void 0, void 0, function* () {
69
+ try {
70
+ const pngBlob = yield convertToPng(file);
71
+ const url = yield uploadImage(pngBlob);
72
+ if (url) {
73
+ editor.chain().focus().setImage({ src: url }).run();
74
+ }
75
+ }
76
+ catch (_a) {
77
+ // Upload failed – silently ignore (caller is responsible for showing errors)
78
+ }
79
+ });
80
+ }
81
+ /** Trigger an image file picker and upload the selected file. */
82
+ export function triggerImageUpload(uploadImage, editor) {
83
+ const input = document.createElement('input');
84
+ input.type = 'file';
85
+ input.accept = 'image/*';
86
+ input.onchange = () => __awaiter(this, void 0, void 0, function* () {
87
+ var _a;
88
+ const file = (_a = input.files) === null || _a === void 0 ? void 0 : _a[0];
89
+ if (file)
90
+ yield handleImageFile(file, uploadImage, editor);
91
+ });
92
+ input.click();
93
+ }
@@ -0,0 +1,47 @@
1
+ import { JSX } from 'react';
2
+ export interface EditorLabels {
3
+ bold?: string;
4
+ italic?: string;
5
+ strike?: string;
6
+ code?: string;
7
+ paragraph?: string;
8
+ heading1?: string;
9
+ heading2?: string;
10
+ heading3?: string;
11
+ bulletList?: string;
12
+ orderedList?: string;
13
+ codeBlock?: string;
14
+ blockquote?: string;
15
+ setLink?: string;
16
+ setLinkTitle?: string;
17
+ setLinkUrlPlaceholder?: string;
18
+ setLinkConfirm?: string;
19
+ unsetLink?: string;
20
+ insertImage?: string;
21
+ addYoutube?: string;
22
+ addYoutubeTitle?: string;
23
+ addYoutubeUrlPlaceholder?: string;
24
+ addYoutubeConfirm?: string;
25
+ insertTable?: string;
26
+ addColumnAfter?: string;
27
+ addRowAfter?: string;
28
+ deleteColumn?: string;
29
+ deleteRow?: string;
30
+ mergeOrSplit?: string;
31
+ appendMarkdown?: string;
32
+ appendMarkdownTitle?: string;
33
+ appendMarkdownPlaceholder?: string;
34
+ appendMarkdownConfirm?: string;
35
+ cancel?: string;
36
+ }
37
+ export interface MarkdownEditorProps {
38
+ content?: string;
39
+ editable: boolean;
40
+ className?: string;
41
+ onUpdate?: (content: string) => void;
42
+ /** Override any subset of toolbar/dialog labels. */
43
+ labels?: EditorLabels;
44
+ /** Called when the user clicks anywhere inside the editor area (read-only mode). */
45
+ onContentClick?: () => void;
46
+ }
47
+ export declare const MarkdownEditor: ({ content, editable, className, onUpdate, labels, onContentClick, }: MarkdownEditorProps) => JSX.Element;
@@ -0,0 +1,218 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
11
+ import { useCallback, useEffect, useRef, useState } from 'react';
12
+ import { useRimori } from '../../providers/PluginProvider';
13
+ import { Markdown } from 'tiptap-markdown';
14
+ import StarterKit from '@tiptap/starter-kit';
15
+ import Table from '@tiptap/extension-table';
16
+ import TableCell from '@tiptap/extension-table-cell';
17
+ import TableHeader from '@tiptap/extension-table-header';
18
+ import TableRow from '@tiptap/extension-table-row';
19
+ import Link from '@tiptap/extension-link';
20
+ import Youtube from '@tiptap/extension-youtube';
21
+ import { useEditor, EditorContent } from '@tiptap/react';
22
+ import { PiCodeBlock } from 'react-icons/pi';
23
+ import { TbBlockquote, TbTable, TbColumnInsertRight, TbRowInsertBottom, TbColumnRemove, TbRowRemove, TbArrowMergeBoth, TbBrandYoutube, TbPhoto, } from 'react-icons/tb';
24
+ import { GoListOrdered } from 'react-icons/go';
25
+ import { AiOutlineUnorderedList } from 'react-icons/ai';
26
+ import { LuClipboardPaste, LuHeading1, LuHeading2, LuHeading3, LuLink, LuUnlink } from 'react-icons/lu';
27
+ import { FaBold, FaCode, FaItalic, FaParagraph, FaStrikethrough } from 'react-icons/fa';
28
+ import { ImageUploadExtension, triggerImageUpload } from './ImageUploadExtension';
29
+ function getMarkdown(editor) {
30
+ return editor.storage.markdown.getMarkdown();
31
+ }
32
+ const EditorButton = ({ editor, action, isActive, label, disabled, title }) => {
33
+ const baseClass = 'w-8 h-8 flex items-center justify-center rounded-md transition-colors duration-150 ' +
34
+ (isActive
35
+ ? 'bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-foreground'
36
+ : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground');
37
+ if (action.toLowerCase().includes('heading')) {
38
+ const level = parseInt(action[action.length - 1]);
39
+ return (_jsx("button", { type: "button", title: title, onClick: () => editor
40
+ .chain()
41
+ .focus()
42
+ .toggleHeading({ level: level })
43
+ .run(), className: baseClass, children: label }));
44
+ }
45
+ // ChainedCommands is not index-typed; cast to call dynamic action names while keeping `.run()`
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
+ const chain = editor.chain().focus();
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ const canChain = editor.can().chain().focus();
50
+ return (_jsx("button", { type: "button", title: title, onClick: () => chain[action]().run(), disabled: disabled ? !canChain[action]().run() : false, className: baseClass, children: label }));
51
+ };
52
+ const InlinePanel = ({ panel, onClose, editor, onUpdate, labels }) => {
53
+ const [value, setValue] = useState('');
54
+ // Reset value when panel changes
55
+ useEffect(() => {
56
+ if (panel === 'link') {
57
+ const href = editor.getAttributes('link').href;
58
+ setValue(typeof href === 'string' ? href : 'https://');
59
+ }
60
+ else {
61
+ setValue('');
62
+ }
63
+ }, [panel, editor]);
64
+ if (!panel)
65
+ return null;
66
+ const handleConfirm = () => {
67
+ if (panel === 'link') {
68
+ const href = value.trim();
69
+ if (href && href !== 'https://')
70
+ editor.chain().focus().setLink({ href }).run();
71
+ onClose();
72
+ }
73
+ else if (panel === 'youtube') {
74
+ const src = value.trim();
75
+ if (src)
76
+ editor.commands.setYoutubeVideo({ src });
77
+ onClose();
78
+ }
79
+ else if (panel === 'markdown') {
80
+ if (!value.trim())
81
+ return;
82
+ const current = getMarkdown(editor);
83
+ const combined = current + '\n\n' + value.trim();
84
+ editor.commands.setContent(combined);
85
+ editor.commands.focus('end');
86
+ onUpdate(combined);
87
+ onClose();
88
+ }
89
+ };
90
+ const handleKeyDown = (e) => {
91
+ if (e.key === 'Enter')
92
+ handleConfirm();
93
+ if (e.key === 'Escape')
94
+ onClose();
95
+ };
96
+ const isMarkdown = panel === 'markdown';
97
+ return (_jsxs("div", { className: "bg-muted/80 border-b border-border px-2 py-1.5 flex flex-col gap-1.5", children: [_jsx("p", { className: "text-xs text-muted-foreground", children: panel === 'link'
98
+ ? labels.setLinkTitle
99
+ : panel === 'youtube'
100
+ ? labels.addYoutubeTitle
101
+ : labels.appendMarkdownTitle }), isMarkdown ? (_jsx("textarea", { autoFocus: true, rows: 4, value: value, onChange: (e) => setValue(e.target.value), onKeyDown: (e) => e.key === 'Escape' && onClose(), placeholder: labels.appendMarkdownPlaceholder, className: "w-full text-sm font-mono rounded border border-border bg-background px-2 py-1 resize-y outline-none focus:ring-1 focus:ring-ring" })) : (_jsx("input", { autoFocus: true, type: "url", value: value, onChange: (e) => setValue(e.target.value), onKeyDown: handleKeyDown, placeholder: panel === 'link' ? labels.setLinkUrlPlaceholder : labels.addYoutubeUrlPlaceholder, className: "w-full text-sm font-mono rounded border border-border bg-background px-2 py-1 outline-none focus:ring-1 focus:ring-ring" })), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx("button", { type: "button", onClick: onClose, className: "text-xs px-3 py-1 rounded border border-border hover:bg-accent transition-colors", children: labels.cancel }), _jsx("button", { type: "button", onClick: handleConfirm, disabled: !value.trim() || (panel === 'link' && value.trim() === 'https://'), className: "text-xs px-3 py-1 rounded bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-40 transition-colors", children: panel === 'link'
102
+ ? labels.setLinkConfirm
103
+ : panel === 'youtube'
104
+ ? labels.addYoutubeConfirm
105
+ : labels.appendMarkdownConfirm })] })] }));
106
+ };
107
+ const MenuBar = ({ editor, onUpdate, uploadImage, labels }) => {
108
+ const [activePanel, setActivePanel] = useState(null);
109
+ const toggle = (panel) => setActivePanel((prev) => (prev === panel ? null : panel));
110
+ const inTable = editor.isActive('table');
111
+ const isLink = editor.isActive('link');
112
+ const tableBtnClass = 'w-8 h-8 flex items-center justify-center rounded-md transition-colors duration-150 ' +
113
+ 'text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-40 disabled:pointer-events-none';
114
+ 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 ' +
115
+ 'text-muted-foreground hover:bg-accent hover:text-accent-foreground border-r border-border last:border-r-0' +
116
+ (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 })] }));
117
+ };
118
+ const DEFAULT_LABELS = {
119
+ bold: 'Bold',
120
+ italic: 'Italic',
121
+ strike: 'Strikethrough',
122
+ code: 'Inline code',
123
+ paragraph: 'Paragraph',
124
+ heading1: 'Heading 1',
125
+ heading2: 'Heading 2',
126
+ heading3: 'Heading 3',
127
+ bulletList: 'Bullet list',
128
+ orderedList: 'Ordered list',
129
+ codeBlock: 'Code block',
130
+ blockquote: 'Blockquote',
131
+ setLink: 'Set link',
132
+ setLinkTitle: 'Set link',
133
+ setLinkUrlPlaceholder: 'https://example.com',
134
+ setLinkConfirm: 'Apply',
135
+ unsetLink: 'Remove link',
136
+ insertImage: 'Insert image',
137
+ addYoutube: 'Add YouTube video',
138
+ addYoutubeTitle: 'Add YouTube video',
139
+ addYoutubeUrlPlaceholder: 'https://www.youtube.com/watch?v=…',
140
+ addYoutubeConfirm: 'Insert',
141
+ insertTable: 'Insert table',
142
+ addColumnAfter: 'Add column',
143
+ addRowAfter: 'Add row',
144
+ deleteColumn: 'Delete column',
145
+ deleteRow: 'Delete row',
146
+ mergeOrSplit: 'Merge or split cells',
147
+ appendMarkdown: 'Append markdown',
148
+ appendMarkdownTitle: 'Append Markdown',
149
+ appendMarkdownPlaceholder: 'Paste markdown here…',
150
+ appendMarkdownConfirm: 'Append',
151
+ cancel: 'Cancel',
152
+ };
153
+ export const MarkdownEditor = ({ content, editable, className, onUpdate, labels, onContentClick, }) => {
154
+ const { storage } = useRimori();
155
+ const mergedLabels = Object.assign(Object.assign({}, DEFAULT_LABELS), labels);
156
+ const lastEmittedRef = useRef(content !== null && content !== void 0 ? content : '');
157
+ const stableUpload = useCallback((pngBlob) => __awaiter(void 0, void 0, void 0, function* () {
158
+ const { data, error } = yield storage.uploadImage(pngBlob);
159
+ if (error)
160
+ return null;
161
+ return data.url;
162
+ }), [storage]);
163
+ const extensions = [
164
+ StarterKit.configure({
165
+ bulletList: {
166
+ HTMLAttributes: {
167
+ class: 'list-disc list-inside dark:text-white p-1 mt-1 [&_li]:mb-1 [&_p]:inline m-0',
168
+ },
169
+ },
170
+ orderedList: {
171
+ HTMLAttributes: {
172
+ className: 'list-decimal list-inside dark:text-white p-1 mt-1 [&_li]:mb-1 [&_p]:inline m-0',
173
+ },
174
+ },
175
+ }),
176
+ Table.configure({ resizable: false }),
177
+ TableRow,
178
+ TableHeader,
179
+ TableCell,
180
+ Link.configure({ protocols: ['mailto'], defaultProtocol: 'https' }),
181
+ Youtube,
182
+ Markdown,
183
+ ImageUploadExtension(stableUpload),
184
+ ];
185
+ const editor = useEditor({
186
+ extensions,
187
+ content,
188
+ editable,
189
+ editorProps: {
190
+ attributes: { class: 'outline-none min-h-[200px] p-3' },
191
+ },
192
+ onUpdate: ({ editor: ed }) => {
193
+ const markdown = getMarkdown(ed);
194
+ lastEmittedRef.current = markdown;
195
+ onUpdate === null || onUpdate === void 0 ? void 0 : onUpdate(markdown);
196
+ },
197
+ });
198
+ // Sync external content changes (e.g. AI autofill) without triggering update loop
199
+ useEffect(() => {
200
+ if (!editor)
201
+ return;
202
+ const incoming = content !== null && content !== void 0 ? content : '';
203
+ if (incoming === lastEmittedRef.current)
204
+ return;
205
+ lastEmittedRef.current = incoming;
206
+ editor.commands.setContent(incoming);
207
+ }, [editor, content]);
208
+ // Sync editable prop
209
+ useEffect(() => {
210
+ if (!editor)
211
+ return;
212
+ editor.setEditable(editable);
213
+ }, [editor, editable]);
214
+ return (_jsxs("div", { className: 'text-md overflow-hidden rounded-lg ' +
215
+ (editable ? 'border border-border bg-card shadow-sm' : 'bg-transparent') +
216
+ ' ' +
217
+ (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 })] }));
218
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Convert any image File/Blob to a PNG Blob, downscaled to max full-HD.
3
+ * Uses native Canvas API – no third-party packages.
4
+ */
5
+ export declare function convertToPng(file: File | Blob): Promise<Blob>;
6
+ /**
7
+ * Extract all image URLs from a markdown string (standard `![alt](url)` syntax).
8
+ * Use this to collect URLs before calling `plugin.storage.confirmImages`.
9
+ */
10
+ export declare function extractImageUrls(markdown: string): string[];
@@ -0,0 +1,52 @@
1
+ const MAX_WIDTH = 1920;
2
+ const MAX_HEIGHT = 1080;
3
+ /**
4
+ * Convert any image File/Blob to a PNG Blob, downscaled to max full-HD.
5
+ * Uses native Canvas API – no third-party packages.
6
+ */
7
+ export function convertToPng(file) {
8
+ return new Promise((resolve, reject) => {
9
+ const img = new Image();
10
+ const objectUrl = URL.createObjectURL(file);
11
+ img.onload = () => {
12
+ URL.revokeObjectURL(objectUrl);
13
+ let { width, height } = img;
14
+ if (width > MAX_WIDTH || height > MAX_HEIGHT) {
15
+ const ratio = Math.min(MAX_WIDTH / width, MAX_HEIGHT / height);
16
+ width = Math.round(width * ratio);
17
+ height = Math.round(height * ratio);
18
+ }
19
+ const canvas = document.createElement('canvas');
20
+ canvas.width = width;
21
+ canvas.height = height;
22
+ const ctx = canvas.getContext('2d');
23
+ if (!ctx)
24
+ return reject(new Error('Could not get 2D canvas context'));
25
+ ctx.drawImage(img, 0, 0, width, height);
26
+ canvas.toBlob((blob) => {
27
+ if (blob)
28
+ resolve(blob);
29
+ else
30
+ reject(new Error('canvas.toBlob returned null'));
31
+ }, 'image/png');
32
+ };
33
+ img.onerror = () => {
34
+ URL.revokeObjectURL(objectUrl);
35
+ reject(new Error('Failed to load image for conversion'));
36
+ };
37
+ img.src = objectUrl;
38
+ });
39
+ }
40
+ /**
41
+ * Extract all image URLs from a markdown string (standard `![alt](url)` syntax).
42
+ * Use this to collect URLs before calling `plugin.storage.confirmImages`.
43
+ */
44
+ export function extractImageUrls(markdown) {
45
+ const regex = /!\[[^\]]*\]\(([^)\s]+)\)/g;
46
+ const urls = [];
47
+ let match;
48
+ while ((match = regex.exec(markdown)) !== null) {
49
+ urls.push(match[1]);
50
+ }
51
+ return urls;
52
+ }
package/dist/index.d.ts CHANGED
@@ -6,3 +6,6 @@ export { FirstMessages } from './components/ai/utils';
6
6
  export { useTranslation } from './hooks/I18nHooks';
7
7
  export { Avatar } from './components/ai/Avatar';
8
8
  export { VoiceRecorder } from './components/ai/EmbeddedAssistent/VoiceRecorder';
9
+ export { MarkdownEditor } from './components/editor/MarkdownEditor';
10
+ export type { MarkdownEditorProps, EditorLabels } from './components/editor/MarkdownEditor';
11
+ export { extractImageUrls } from './components/editor/imageUtils';
package/dist/index.js CHANGED
@@ -6,3 +6,5 @@ export * from './components/ai/Avatar';
6
6
  export { useTranslation } from './hooks/I18nHooks';
7
7
  export { Avatar } from './components/ai/Avatar';
8
8
  export { VoiceRecorder } from './components/ai/EmbeddedAssistent/VoiceRecorder';
9
+ export { MarkdownEditor } from './components/editor/MarkdownEditor';
10
+ export { extractImageUrls } from './components/editor/imageUtils';
package/dist/style.css CHANGED
@@ -6,105 +6,93 @@ dialog::backdrop {
6
6
  background: transparent;
7
7
  }
8
8
 
9
+ /* Tiptap editor content styles */
9
10
  .tiptap {
10
- padding-top: 5px;
11
- padding-left: 7px;
12
- /* min-height: 300px; */
13
- }
14
- .tiptap:focus-visible {
15
- outline: none;
16
- }
17
- .tiptap h1,
18
- .tiptap h2,
19
- .tiptap h3,
20
- .tiptap h4,
21
- .tiptap h5,
22
- .tiptap h6 {
23
- @apply font-bold;
24
- margin-bottom: 1rem;
11
+ @apply focus:outline-none min-h-[200px];
25
12
  }
26
13
  .tiptap h1 {
27
- @apply text-4xl;
14
+ @apply text-4xl font-bold mt-6 mb-3 first:mt-0;
28
15
  }
29
16
  .tiptap h2 {
30
- @apply text-3xl;
17
+ @apply text-3xl font-semibold mt-5 mb-2 first:mt-0;
31
18
  }
32
19
  .tiptap h3 {
33
- @apply text-2xl;
20
+ @apply text-2xl font-semibold mt-4 mb-2 first:mt-0;
34
21
  }
35
- .tiptap h4 {
36
- @apply text-xl;
22
+ .tiptap p {
23
+ @apply mb-2 leading-relaxed;
24
+ }
25
+ .tiptap blockquote {
26
+ @apply border-l-4 border-primary/30 pl-4 py-1 my-3 text-muted-foreground italic;
37
27
  }
38
- .tiptap h5 {
39
- @apply text-lg;
28
+ .tiptap pre {
29
+ @apply bg-muted rounded-lg p-4 my-3 overflow-x-auto text-sm font-mono;
40
30
  }
41
- .tiptap h6 {
42
- @apply text-base;
31
+ .tiptap code {
32
+ @apply bg-muted px-1.5 py-0.5 rounded text-sm font-mono;
43
33
  }
44
- .tiptap p {
45
- @apply mb-4;
34
+ .tiptap pre code {
35
+ @apply bg-transparent p-0;
36
+ }
37
+ .tiptap hr {
38
+ @apply border-border my-6;
46
39
  }
47
40
  .tiptap a {
48
- @apply text-blue-600 hover:text-blue-800;
49
- text-decoration: none;
41
+ @apply text-primary underline underline-offset-2;
50
42
  }
51
- .tiptap a:hover {
52
- @apply underline;
43
+ .tiptap {
44
+ /* Basic editor styles */
53
45
  }
54
- .tiptap ul {
55
- @apply list-disc pl-8;
46
+ .tiptap :first-child {
47
+ margin-top: 0;
56
48
  }
57
- .tiptap ul li > p {
49
+ .tiptap li {
58
50
  @apply mb-1;
59
51
  }
60
- .tiptap ol {
61
- @apply list-decimal pl-7;
52
+ .tiptap ul {
53
+ @apply list-disc pl-4 m-0;
62
54
  }
63
- .tiptap ol li > p {
64
- @apply mb-1;
55
+ .tiptap ul p {
56
+ @apply m-0;
65
57
  }
66
- .tiptap blockquote {
67
- @apply border-l-4 pl-4 italic text-gray-600 my-4;
68
- border-color: #ccc;
58
+ .tiptap ul li {
59
+ @apply m-0;
69
60
  }
70
- .tiptap code {
71
- font-family: monospace;
61
+ .tiptap ol {
62
+ @apply list-decimal pl-7 m-0;
72
63
  }
73
- .tiptap pre {
74
- @apply bg-gray-800 text-gray-500 p-4 rounded-lg overflow-x-auto;
75
- font-family: monospace;
76
- white-space: pre-wrap;
77
- word-wrap: break-word;
64
+ .tiptap ol p {
65
+ @apply m-0;
66
+ }
67
+ .tiptap ol li {
68
+ @apply m-0;
78
69
  }
79
- .tiptap img {
80
- @apply max-w-full h-auto rounded-lg my-4;
70
+ .tiptap {
71
+ /* Table-specific styling */
81
72
  }
82
73
  .tiptap table {
83
- @apply table-auto w-full border-collapse mb-4;
84
- }
85
- .tiptap th,
86
- .tiptap td {
87
- @apply border px-4 py-2 text-left;
88
- }
89
- .tiptap th {
90
- @apply bg-gray-500 font-semibold;
91
- }
92
- .tiptap tr:nth-child(even) {
93
- @apply bg-gray-400;
94
- }
95
- @media (max-width: 768px) {
96
- .tiptap h1 {
97
- @apply text-3xl;
98
- }
99
- .tiptap h2 {
100
- @apply text-2xl;
101
- }
102
- .tiptap p {
103
- @apply text-base;
104
- }
105
- .tiptap img {
106
- @apply max-w-full;
107
- }
74
+ @apply border-collapse m-0 overflow-hidden w-full mb-1;
75
+ }
76
+ .tiptap table td,
77
+ .tiptap table th {
78
+ @apply dark:border-gray-700 border;
79
+ @apply mb-0 align-baseline pl-1.5;
80
+ }
81
+ .tiptap table td p,
82
+ .tiptap table th p {
83
+ @apply mb-0;
84
+ }
85
+ .tiptap table th {
86
+ /* @apply bg-gray-500; */
87
+ @apply font-bold text-left;
88
+ }
89
+ .tiptap .tableWrapper {
90
+ margin: 1.5rem 0;
91
+ @apply overflow-x-auto;
92
+ }
93
+ .tiptap.resize-cursor {
94
+ @apply cursor-ew-resize;
95
+ cursor: col-resize;
108
96
  }
109
97
 
110
98
  /*# sourceMappingURL=style.css.map */
@@ -1 +1 @@
1
- {"version":3,"sourceRoot":"","sources":["../src/style.scss"],"names":[],"mappings":"AAAA;EACE;;;AAIF;EACE;;;AAGF;EACE;EACA;AACA;;AAEA;EACE;;AAGF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;EACA;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;EACA;;AAGF;EACE;;AAGF;EACE;;AAEA;EACE;;AAIJ;EACE;;AAEA;EACE;;AAIJ;EACE;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;;AAGF;EACE;;AAGF;AAAA;EAEE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;IACE;;EAGF;IACE;;EAGF;IACE;;EAGF;IACE","file":"style.css"}
1
+ {"version":3,"sourceRoot":"","sources":["../src/style.scss"],"names":[],"mappings":"AAAA;EACE;;;AAIF;EACE;;;AAGF;AACA;EACE;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAxCJ;AA2CE;;AACA;EACE;;AAGF;EACE;;AAGF;EACE;;AAEA;EACE;;AAGF;EACE;;AAIJ;EACE;;AAEA;EACE;;AAGF;EACE;;AAxEN;AA4EE;;AACA;EACE;;AAEA;AAAA;EAEE;EACA;;AAEA;AAAA;EACE;;AAIJ;AACE;EACA;;AAIJ;EACE;EACA;;AAGF;EACE;EACA","file":"style.css"}