mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 12:25:20 +00:00
refac/enh: rich text input
This commit is contained in:
parent
032fa52190
commit
c1c589d609
4 changed files with 211 additions and 151 deletions
|
|
@ -120,6 +120,53 @@
|
||||||
export let image = false;
|
export let image = false;
|
||||||
export let fileHandler = 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 id = '';
|
||||||
export let value = '';
|
export let value = '';
|
||||||
export let html = '';
|
export let html = '';
|
||||||
|
|
@ -847,57 +894,12 @@
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
CharacterCount.configure({}),
|
CharacterCount.configure({}),
|
||||||
|
|
||||||
...(image ? [Image] : []),
|
...(image ? [Image] : []),
|
||||||
...(fileHandler
|
...(fileHandler
|
||||||
? [
|
? [
|
||||||
FileHandler.configure({
|
FileHandler.configure({
|
||||||
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
|
onDrop: onFileDrop,
|
||||||
onDrop: (currentEditor, files, pos) => {
|
onPaste: onFilePaste
|
||||||
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();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ export const Image = Node.create<ImageOptions>({
|
||||||
if (file) {
|
if (file) {
|
||||||
img.setAttribute('src', file.url || '');
|
img.setAttribute('src', file.url || '');
|
||||||
} else {
|
} else {
|
||||||
img.setAttribute('src', node.attrs.src || '');
|
img.setAttribute('src', '/no-image.png');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
img.setAttribute('src', node.attrs.src || '');
|
img.setAttribute('src', node.attrs.src || '');
|
||||||
|
|
@ -155,7 +155,7 @@ export const Image = Node.create<ImageOptions>({
|
||||||
if (file) {
|
if (file) {
|
||||||
img.setAttribute('src', file.url || '');
|
img.setAttribute('src', file.url || '');
|
||||||
} else {
|
} else {
|
||||||
img.setAttribute('src', node.attrs.src || '');
|
img.setAttribute('src', '/no-image.png');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,10 @@
|
||||||
import { config, models, settings, showSidebar, socket, user, WEBUI_NAME } from '$lib/stores';
|
import { config, models, settings, showSidebar, socket, user, WEBUI_NAME } from '$lib/stores';
|
||||||
|
|
||||||
import NotePanel from '$lib/components/notes/NotePanel.svelte';
|
import NotePanel from '$lib/components/notes/NotePanel.svelte';
|
||||||
import MenuLines from '../icons/MenuLines.svelte';
|
|
||||||
import ChatBubbleOval from '../icons/ChatBubbleOval.svelte';
|
import Controls from './NoteEditor/Controls.svelte';
|
||||||
import Settings from './NoteEditor/Settings.svelte';
|
|
||||||
import Chat from './NoteEditor/Chat.svelte';
|
import Chat from './NoteEditor/Chat.svelte';
|
||||||
|
|
||||||
import AccessControlModal from '$lib/components/workspace/common/AccessControlModal.svelte';
|
import AccessControlModal from '$lib/components/workspace/common/AccessControlModal.svelte';
|
||||||
|
|
||||||
async function loadLocale(locales) {
|
async function loadLocale(locales) {
|
||||||
|
|
@ -61,6 +61,8 @@
|
||||||
import MicSolid from '../icons/MicSolid.svelte';
|
import MicSolid from '../icons/MicSolid.svelte';
|
||||||
import VoiceRecording from '../chat/MessageInput/VoiceRecording.svelte';
|
import VoiceRecording from '../chat/MessageInput/VoiceRecording.svelte';
|
||||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.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 Calendar from '../icons/Calendar.svelte';
|
||||||
import Users from '../icons/Users.svelte';
|
import Users from '../icons/Users.svelte';
|
||||||
|
|
@ -81,6 +83,7 @@
|
||||||
import ArrowRight from '../icons/ArrowRight.svelte';
|
import ArrowRight from '../icons/ArrowRight.svelte';
|
||||||
import Cog6 from '../icons/Cog6.svelte';
|
import Cog6 from '../icons/Cog6.svelte';
|
||||||
import AiMenu from './AIMenu.svelte';
|
import AiMenu from './AIMenu.svelte';
|
||||||
|
import AdjustmentsHorizontalOutline from '../icons/AdjustmentsHorizontalOutline.svelte';
|
||||||
|
|
||||||
export let id: null | string = null;
|
export let id: null | string = null;
|
||||||
|
|
||||||
|
|
@ -441,35 +444,10 @@ ${content}
|
||||||
}
|
}
|
||||||
|
|
||||||
changeDebounceHandler();
|
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 = {}) => {
|
const compressImageHandler = async (imageUrl, settings = {}, config = {}) => {
|
||||||
// Quick shortcut so we don’t do unnecessary work.
|
// Quick shortcut so we don’t do unnecessary work.
|
||||||
const settingsCompression = settings?.imageCompression ?? false;
|
const settingsCompression = settings?.imageCompression ?? false;
|
||||||
|
|
@ -506,10 +484,36 @@ ${content}
|
||||||
return imageUrl;
|
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();
|
let reader = new FileReader();
|
||||||
reader.onload = async (event) => {
|
reader.onload = async (event) => {
|
||||||
|
try {
|
||||||
let imageUrl = event.target.result;
|
let imageUrl = event.target.result;
|
||||||
|
|
||||||
imageUrl = await compressImageHandler(imageUrl, $settings, $config);
|
imageUrl = await compressImageHandler(imageUrl, $settings, $config);
|
||||||
|
|
||||||
const fileId = uuidv4();
|
const fileId = uuidv4();
|
||||||
|
|
@ -520,34 +524,32 @@ ${content}
|
||||||
};
|
};
|
||||||
files = [...files, fileItem];
|
files = [...files, fileItem];
|
||||||
note.data.files = files;
|
note.data.files = files;
|
||||||
|
|
||||||
if (imageUrl && editor) {
|
|
||||||
editor.storage.files = files;
|
editor.storage.files = files;
|
||||||
editor
|
|
||||||
?.chain()
|
|
||||||
.insertContentAt(editor.state.selection.$anchor.pos, {
|
|
||||||
type: 'image',
|
|
||||||
attrs: {
|
|
||||||
file: fileItem,
|
|
||||||
src: `data://${fileId}`
|
|
||||||
|
|
||||||
// src: imageUrl
|
changeDebounceHandler();
|
||||||
}
|
resolve(fileItem);
|
||||||
})
|
} catch (err) {
|
||||||
.focus()
|
reject(err);
|
||||||
.run();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsDataURL(
|
reader.readAsDataURL(
|
||||||
file['type'] === 'image/heic'
|
file['type'] === 'image/heic'
|
||||||
? await heic2any({ blob: file, toType: 'image/jpeg' })
|
? await heic2any({ blob: file, toType: 'image/jpeg' })
|
||||||
: file
|
: file
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
changeDebounceHandler();
|
return await uploadImagePromise;
|
||||||
} else {
|
} 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');
|
const dropzoneElement = document.getElementById('note-editor');
|
||||||
|
|
||||||
dropzoneElement?.addEventListener('dragover', onDragOver);
|
// dropzoneElement?.addEventListener('dragover', onDragOver);
|
||||||
dropzoneElement?.addEventListener('drop', onDrop);
|
// dropzoneElement?.addEventListener('drop', onDrop);
|
||||||
dropzoneElement?.addEventListener('dragleave', onDragLeave);
|
// dropzoneElement?.addEventListener('dragleave', onDragLeave);
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
|
|
@ -878,9 +880,9 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
const dropzoneElement = document.getElementById('note-editor');
|
const dropzoneElement = document.getElementById('note-editor');
|
||||||
|
|
||||||
if (dropzoneElement) {
|
if (dropzoneElement) {
|
||||||
dropzoneElement?.removeEventListener('dragover', onDragOver);
|
// dropzoneElement?.removeEventListener('dragover', onDragOver);
|
||||||
dropzoneElement?.removeEventListener('drop', onDrop);
|
// dropzoneElement?.removeEventListener('drop', onDrop);
|
||||||
dropzoneElement?.removeEventListener('dragleave', onDragLeave);
|
// dropzoneElement?.removeEventListener('dragleave', onDragLeave);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -1044,7 +1046,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip placement="top" content={$i18n.t('Settings')} className="cursor-pointer">
|
<Tooltip placement="top" content={$i18n.t('Controls')} className="cursor-pointer">
|
||||||
<button
|
<button
|
||||||
class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg"
|
class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|
@ -1058,7 +1060,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Cog6 />
|
<AdjustmentsHorizontalOutline />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|
@ -1205,6 +1207,62 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
charCount = editor.storage.characterCount.characters();
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1349,7 +1407,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
scrollToBottomHandler={scrollToBottom}
|
scrollToBottomHandler={scrollToBottom}
|
||||||
/>
|
/>
|
||||||
{:else if selectedPanel === 'settings'}
|
{:else if selectedPanel === 'settings'}
|
||||||
<Settings
|
<Controls
|
||||||
bind:show={showPanel}
|
bind:show={showPanel}
|
||||||
bind:selectedModelId
|
bind:selectedModelId
|
||||||
bind:files
|
bind:files
|
||||||
|
|
|
||||||
BIN
static/no-image.png
Normal file
BIN
static/no-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Loading…
Reference in a new issue