diff --git a/src/lib/components/common/RichTextInput.svelte b/src/lib/components/common/RichTextInput.svelte index 55aa5db2c9..01a8aac377 100644 --- a/src/lib/components/common/RichTextInput.svelte +++ b/src/lib/components/common/RichTextInput.svelte @@ -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 }) ] : []), diff --git a/src/lib/components/common/RichTextInput/Image/image.ts b/src/lib/components/common/RichTextInput/Image/image.ts index c0fb995253..54b32c65ba 100644 --- a/src/lib/components/common/RichTextInput/Image/image.ts +++ b/src/lib/components/common/RichTextInput/Image/image.ts @@ -139,7 +139,7 @@ export const Image = Node.create({ 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({ if (file) { img.setAttribute('src', file.url || ''); } else { - img.setAttribute('src', node.attrs.src || ''); + img.setAttribute('src', '/no-image.png'); } } }); diff --git a/src/lib/components/notes/NoteEditor.svelte b/src/lib/components/notes/NoteEditor.svelte index e7b6dd6fcb..45aa257ab2 100644 --- a/src/lib/components/notes/NoteEditor.svelte +++ b/src/lib/components/notes/NoteEditor.svelte @@ -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,113 +444,112 @@ ${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) + const compressImageHandler = async (imageUrl, settings = {}, config = {}) => { + // Quick shortcut so we don’t do unnecessary work. + const settingsCompression = settings?.imageCompression ?? false; + const configWidth = config?.file?.image_compression?.width ?? null; + const configHeight = config?.file?.image_compression?.height ?? null; + + // If neither settings nor config wants compression, return original URL. + if (!settingsCompression && !configWidth && !configHeight) { + return imageUrl; + } + + // Default to null (no compression unless set) + let width = null; + let height = null; + + // If user/settings want compression, pick their preferred size. + if (settingsCompression) { + width = settings?.imageCompressionSize?.width ?? null; + height = settings?.imageCompressionSize?.height ?? null; + } + + // Apply config limits as an upper bound if any + if (configWidth && (width === null || width > configWidth)) { + width = configWidth; + } + if (configHeight && (height === null || height > configHeight)) { + height = configHeight; + } + + // Do the compression if required + if (width || height) { + return await compressImage(imageUrl, width, height); + } + 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 ( - ($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 don’t do unnecessary work. - const settingsCompression = settings?.imageCompression ?? false; - const configWidth = config?.file?.image_compression?.width ?? null; - const configHeight = config?.file?.image_compression?.height ?? null; - - // If neither settings nor config wants compression, return original URL. - if (!settingsCompression && !configWidth && !configHeight) { - return imageUrl; - } - - // Default to null (no compression unless set) - let width = null; - let height = null; - - // If user/settings want compression, pick their preferred size. - if (settingsCompression) { - width = settings?.imageCompressionSize?.width ?? null; - height = settings?.imageCompressionSize?.height ?? null; - } - - // Apply config limits as an upper bound if any - if (configWidth && (width === null || width > configWidth)) { - width = configWidth; - } - if (configHeight && (height === null || height > configHeight)) { - height = configHeight; - } - - // Do the compression if required - if (width || height) { - return await compressImage(imageUrl, width, height); - } - return imageUrl; - }; - + if (file['type'].startsWith('image/')) { + const uploadImagePromise = new Promise(async (resolve, reject) => { let reader = new FileReader(); reader.onload = async (event) => { - let imageUrl = event.target.result; + try { + let imageUrl = event.target.result; + imageUrl = await compressImageHandler(imageUrl, $settings, $config); - imageUrl = await compressImageHandler(imageUrl, $settings, $config); - - const fileId = uuidv4(); - const fileItem = { - id: fileId, - type: 'image', - url: `${imageUrl}` - }; - files = [...files, fileItem]; - note.data.files = files; - - if (imageUrl && editor) { + const fileId = uuidv4(); + const fileItem = { + id: fileId, + type: 'image', + url: `${imageUrl}` + }; + files = [...files, fileItem]; + note.data.files = files; 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(); - } else { - uploadFileHandler(file); - } + return await uploadImagePromise; + } else { + 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); } }); @@ -1044,7 +1046,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings, - + @@ -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(); + } + } + } + }} /> @@ -1349,7 +1407,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings, scrollToBottomHandler={scrollToBottom} /> {:else if selectedPanel === 'settings'} -