refac/enh: rich text input

This commit is contained in:
Timothy Jaeryang Baek 2025-07-18 15:58:06 +04:00
parent 032fa52190
commit c1c589d609
4 changed files with 211 additions and 151 deletions

View file

@ -120,6 +120,53 @@
export let image = false;
export let fileHandler = false;
export let onFileDrop = (currentEditor, files, pos) => {
files.forEach((file) => {
const fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = () => {
currentEditor
.chain()
.insertContentAt(pos, {
type: 'image',
attrs: {
src: fileReader.result
}
})
.focus()
.run();
};
});
};
export let onFilePaste = (currentEditor, files, htmlContent) => {
files.forEach((file) => {
if (htmlContent) {
// if there is htmlContent, stop manual insertion & let other extensions handle insertion via inputRule
// you could extract the pasted file from this url string and upload it to a server for example
console.log(htmlContent); // eslint-disable-line no-console
return false;
}
const fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = () => {
currentEditor
.chain()
.insertContentAt(currentEditor.state.selection.anchor, {
type: 'image',
attrs: {
src: fileReader.result
}
})
.focus()
.run();
};
});
};
export let id = '';
export let value = '';
export let html = '';
@ -847,57 +894,12 @@
}
}),
CharacterCount.configure({}),
...(image ? [Image] : []),
...(fileHandler
? [
FileHandler.configure({
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
onDrop: (currentEditor, files, pos) => {
files.forEach((file) => {
const fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = () => {
currentEditor
.chain()
.insertContentAt(pos, {
type: 'image',
attrs: {
src: fileReader.result
}
})
.focus()
.run();
};
});
},
onPaste: (currentEditor, files, htmlContent) => {
files.forEach((file) => {
if (htmlContent) {
// if there is htmlContent, stop manual insertion & let other extensions handle insertion via inputRule
// you could extract the pasted file from this url string and upload it to a server for example
console.log(htmlContent); // eslint-disable-line no-console
return false;
}
const fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = () => {
currentEditor
.chain()
.insertContentAt(currentEditor.state.selection.anchor, {
type: 'image',
attrs: {
src: fileReader.result
}
})
.focus()
.run();
};
});
}
onDrop: onFileDrop,
onPaste: onFilePaste
})
]
: []),

View file

@ -139,7 +139,7 @@ export const Image = Node.create<ImageOptions>({
if (file) {
img.setAttribute('src', file.url || '');
} else {
img.setAttribute('src', node.attrs.src || '');
img.setAttribute('src', '/no-image.png');
}
} else {
img.setAttribute('src', node.attrs.src || '');
@ -155,7 +155,7 @@ export const Image = Node.create<ImageOptions>({
if (file) {
img.setAttribute('src', file.url || '');
} else {
img.setAttribute('src', node.attrs.src || '');
img.setAttribute('src', '/no-image.png');
}
}
});

View file

@ -34,10 +34,10 @@
import { config, models, settings, showSidebar, socket, user, WEBUI_NAME } from '$lib/stores';
import NotePanel from '$lib/components/notes/NotePanel.svelte';
import MenuLines from '../icons/MenuLines.svelte';
import ChatBubbleOval from '../icons/ChatBubbleOval.svelte';
import Settings from './NoteEditor/Settings.svelte';
import Controls from './NoteEditor/Controls.svelte';
import Chat from './NoteEditor/Chat.svelte';
import AccessControlModal from '$lib/components/workspace/common/AccessControlModal.svelte';
async function loadLocale(locales) {
@ -61,6 +61,8 @@
import MicSolid from '../icons/MicSolid.svelte';
import VoiceRecording from '../chat/MessageInput/VoiceRecording.svelte';
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import MenuLines from '../icons/MenuLines.svelte';
import ChatBubbleOval from '../icons/ChatBubbleOval.svelte';
import Calendar from '../icons/Calendar.svelte';
import Users from '../icons/Users.svelte';
@ -81,6 +83,7 @@
import ArrowRight from '../icons/ArrowRight.svelte';
import Cog6 from '../icons/Cog6.svelte';
import AiMenu from './AIMenu.svelte';
import AdjustmentsHorizontalOutline from '../icons/AdjustmentsHorizontalOutline.svelte';
export let id: null | string = null;
@ -441,35 +444,10 @@ ${content}
}
changeDebounceHandler();
return fileItem;
};
const inputFilesHandler = async (inputFiles) => {
console.log('Input files handler called with:', inputFiles);
inputFiles.forEach(async (file) => {
console.log('Processing file:', {
name: file.name,
type: file.type,
size: file.size,
extension: file.name.split('.').at(-1)
});
if (
($config?.file?.max_size ?? null) !== null &&
file.size > ($config?.file?.max_size ?? 0) * 1024 * 1024
) {
console.log('File exceeds max size limit:', {
fileSize: file.size,
maxSize: ($config?.file?.max_size ?? 0) * 1024 * 1024
});
toast.error(
$i18n.t(`File size should not exceed {{maxSize}} MB.`, {
maxSize: $config?.file?.max_size
})
);
return;
}
if (file['type'].startsWith('image/')) {
const compressImageHandler = async (imageUrl, settings = {}, config = {}) => {
// Quick shortcut so we dont do unnecessary work.
const settingsCompression = settings?.imageCompression ?? false;
@ -506,10 +484,36 @@ ${content}
return imageUrl;
};
const inputFileHandler = async (file) => {
console.log('Processing file:', {
name: file.name,
type: file.type,
size: file.size,
extension: file.name.split('.').at(-1)
});
if (
($config?.file?.max_size ?? null) !== null &&
file.size > ($config?.file?.max_size ?? 0) * 1024 * 1024
) {
console.log('File exceeds max size limit:', {
fileSize: file.size,
maxSize: ($config?.file?.max_size ?? 0) * 1024 * 1024
});
toast.error(
$i18n.t(`File size should not exceed {{maxSize}} MB.`, {
maxSize: $config?.file?.max_size
})
);
return;
}
if (file['type'].startsWith('image/')) {
const uploadImagePromise = new Promise(async (resolve, reject) => {
let reader = new FileReader();
reader.onload = async (event) => {
try {
let imageUrl = event.target.result;
imageUrl = await compressImageHandler(imageUrl, $settings, $config);
const fileId = uuidv4();
@ -520,34 +524,32 @@ ${content}
};
files = [...files, fileItem];
note.data.files = files;
if (imageUrl && editor) {
editor.storage.files = files;
editor
?.chain()
.insertContentAt(editor.state.selection.$anchor.pos, {
type: 'image',
attrs: {
file: fileItem,
src: `data://${fileId}`
// src: imageUrl
}
})
.focus()
.run();
changeDebounceHandler();
resolve(fileItem);
} catch (err) {
reject(err);
}
};
reader.readAsDataURL(
file['type'] === 'image/heic'
? await heic2any({ blob: file, toType: 'image/jpeg' })
: file
);
});
changeDebounceHandler();
return await uploadImagePromise;
} else {
uploadFileHandler(file);
return await uploadFileHandler(file);
}
};
const inputFilesHandler = async (inputFiles) => {
console.log('Input files handler called with:', inputFiles);
inputFiles.forEach(async (file) => {
await inputFileHandler(file);
});
};
@ -866,9 +868,9 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
const dropzoneElement = document.getElementById('note-editor');
dropzoneElement?.addEventListener('dragover', onDragOver);
dropzoneElement?.addEventListener('drop', onDrop);
dropzoneElement?.addEventListener('dragleave', onDragLeave);
// dropzoneElement?.addEventListener('dragover', onDragOver);
// dropzoneElement?.addEventListener('drop', onDrop);
// dropzoneElement?.addEventListener('dragleave', onDragLeave);
});
onDestroy(() => {
@ -878,9 +880,9 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
const dropzoneElement = document.getElementById('note-editor');
if (dropzoneElement) {
dropzoneElement?.removeEventListener('dragover', onDragOver);
dropzoneElement?.removeEventListener('drop', onDrop);
dropzoneElement?.removeEventListener('dragleave', onDragLeave);
// dropzoneElement?.removeEventListener('dragover', onDragOver);
// dropzoneElement?.removeEventListener('drop', onDrop);
// dropzoneElement?.removeEventListener('dragleave', onDragLeave);
}
});
</script>
@ -1044,7 +1046,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
</button>
</Tooltip>
<Tooltip placement="top" content={$i18n.t('Settings')} className="cursor-pointer">
<Tooltip placement="top" content={$i18n.t('Controls')} className="cursor-pointer">
<button
class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg"
on:click={() => {
@ -1058,7 +1060,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
}
}}
>
<Cog6 />
<AdjustmentsHorizontalOutline />
</button>
</Tooltip>
@ -1205,6 +1207,62 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
charCount = editor.storage.characterCount.characters();
}
}}
fileHandler={true}
onFileDrop={(currentEditor, files, pos) => {
files.forEach(async (file) => {
const fileItem = await inputFileHandler(file).catch((error) => {
return null;
});
if (fileItem.type === 'image') {
// If the file is an image, insert it directly
currentEditor
.chain()
.insertContentAt(pos, {
type: 'image',
attrs: {
src: `data://${fileItem.id}`
}
})
.focus()
.run();
}
});
}}
onFilePaste={() => {}}
on:paste={async (e) => {
e = e.detail.event || e;
const clipboardData = e.clipboardData || window.clipboardData;
console.log('Clipboard data:', clipboardData);
if (clipboardData && clipboardData.items) {
console.log('Clipboard data items:', clipboardData.items);
for (const item of clipboardData.items) {
console.log('Clipboard item:', item);
if (item.type.indexOf('image') !== -1) {
const blob = item.getAsFile();
const fileItem = await inputFileHandler(blob);
if (editor) {
editor
?.chain()
.insertContentAt(editor.state.selection.$anchor.pos, {
type: 'image',
attrs: {
src: `data://${fileItem.id}` // Use data URI for the image
}
})
.focus()
.run();
}
} else if (item?.kind === 'file') {
const file = item.getAsFile();
await inputFileHandler(file);
e.preventDefault();
}
}
}
}}
/>
</div>
</div>
@ -1349,7 +1407,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
scrollToBottomHandler={scrollToBottom}
/>
{:else if selectedPanel === 'settings'}
<Settings
<Controls
bind:show={showPanel}
bind:selectedModelId
bind:files

BIN
static/no-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB