@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.
- package/dist/components/ai/Assistant.js +2 -2
- package/dist/components/editor/ImageUploadExtension.d.ts +13 -0
- package/dist/components/editor/ImageUploadExtension.js +93 -0
- package/dist/components/editor/MarkdownEditor.d.ts +47 -0
- package/dist/components/editor/MarkdownEditor.js +218 -0
- package/dist/components/editor/imageUtils.d.ts +10 -0
- package/dist/components/editor/imageUtils.js +52 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/style.css +62 -74
- package/dist/style.css.map +1 -1
- package/package.json +17 -5
- package/src/components/ai/Assistant.tsx +2 -2
- package/src/components/editor/ImageUploadExtension.ts +88 -0
- package/src/components/editor/MarkdownEditor.tsx +632 -0
- package/src/components/editor/imageUtils.ts +58 -0
- package/src/index.ts +3 -0
- package/src/style.scss +61 -81
|
@@ -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(
|
|
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 `` 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 `` 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
|
-
|
|
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
|
|
36
|
-
@apply
|
|
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
|
|
39
|
-
@apply text-
|
|
28
|
+
.tiptap pre {
|
|
29
|
+
@apply bg-muted rounded-lg p-4 my-3 overflow-x-auto text-sm font-mono;
|
|
40
30
|
}
|
|
41
|
-
.tiptap
|
|
42
|
-
@apply text-
|
|
31
|
+
.tiptap code {
|
|
32
|
+
@apply bg-muted px-1.5 py-0.5 rounded text-sm font-mono;
|
|
43
33
|
}
|
|
44
|
-
.tiptap
|
|
45
|
-
@apply
|
|
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-
|
|
49
|
-
text-decoration: none;
|
|
41
|
+
@apply text-primary underline underline-offset-2;
|
|
50
42
|
}
|
|
51
|
-
.tiptap
|
|
52
|
-
|
|
43
|
+
.tiptap {
|
|
44
|
+
/* Basic editor styles */
|
|
53
45
|
}
|
|
54
|
-
.tiptap
|
|
55
|
-
|
|
46
|
+
.tiptap :first-child {
|
|
47
|
+
margin-top: 0;
|
|
56
48
|
}
|
|
57
|
-
.tiptap
|
|
49
|
+
.tiptap li {
|
|
58
50
|
@apply mb-1;
|
|
59
51
|
}
|
|
60
|
-
.tiptap
|
|
61
|
-
@apply list-
|
|
52
|
+
.tiptap ul {
|
|
53
|
+
@apply list-disc pl-4 m-0;
|
|
62
54
|
}
|
|
63
|
-
.tiptap
|
|
64
|
-
@apply
|
|
55
|
+
.tiptap ul p {
|
|
56
|
+
@apply m-0;
|
|
65
57
|
}
|
|
66
|
-
.tiptap
|
|
67
|
-
@apply
|
|
68
|
-
border-color: #ccc;
|
|
58
|
+
.tiptap ul li {
|
|
59
|
+
@apply m-0;
|
|
69
60
|
}
|
|
70
|
-
.tiptap
|
|
71
|
-
|
|
61
|
+
.tiptap ol {
|
|
62
|
+
@apply list-decimal pl-7 m-0;
|
|
72
63
|
}
|
|
73
|
-
.tiptap
|
|
74
|
-
@apply
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
64
|
+
.tiptap ol p {
|
|
65
|
+
@apply m-0;
|
|
66
|
+
}
|
|
67
|
+
.tiptap ol li {
|
|
68
|
+
@apply m-0;
|
|
78
69
|
}
|
|
79
|
-
.tiptap
|
|
80
|
-
|
|
70
|
+
.tiptap {
|
|
71
|
+
/* Table-specific styling */
|
|
81
72
|
}
|
|
82
73
|
.tiptap table {
|
|
83
|
-
@apply
|
|
84
|
-
}
|
|
85
|
-
.tiptap
|
|
86
|
-
.tiptap
|
|
87
|
-
@apply border
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
@
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 */
|
package/dist/style.css.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sourceRoot":"","sources":["../src/style.scss"],"names":[],"mappings":"AAAA;EACE;;;AAIF;EACE;;;AAGF;EACE
|
|
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"}
|