mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 20:35:19 +00:00
enh/refac: note image upload
This commit is contained in:
parent
9d633b062b
commit
d4ece7384c
9 changed files with 547 additions and 45 deletions
|
|
@ -6,6 +6,9 @@ from typing import Optional
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks
|
from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from open_webui.socket.main import sio
|
||||||
|
|
||||||
|
|
||||||
from open_webui.models.users import Users, UserResponse
|
from open_webui.models.users import Users, UserResponse
|
||||||
from open_webui.models.notes import Notes, NoteModel, NoteForm, NoteUserResponse
|
from open_webui.models.notes import Notes, NoteModel, NoteForm, NoteUserResponse
|
||||||
|
|
||||||
|
|
@ -170,6 +173,12 @@ async def update_note_by_id(
|
||||||
|
|
||||||
try:
|
try:
|
||||||
note = Notes.update_note_by_id(id, form_data)
|
note = Notes.update_note_by_id(id, form_data)
|
||||||
|
await sio.emit(
|
||||||
|
"note-events",
|
||||||
|
note.model_dump(),
|
||||||
|
to=f"note:{note.id}",
|
||||||
|
)
|
||||||
|
|
||||||
return note
|
return note
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,37 @@ async def join_channel(sid, data):
|
||||||
await sio.enter_room(sid, f"channel:{channel.id}")
|
await sio.enter_room(sid, f"channel:{channel.id}")
|
||||||
|
|
||||||
|
|
||||||
|
@sio.on("join-note")
|
||||||
|
async def join_note(sid, data):
|
||||||
|
auth = data["auth"] if "auth" in data else None
|
||||||
|
if not auth or "token" not in auth:
|
||||||
|
return
|
||||||
|
|
||||||
|
token_data = decode_token(auth["token"])
|
||||||
|
if token_data is None or "id" not in token_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
user = Users.get_user_by_id(token_data["id"])
|
||||||
|
if not user:
|
||||||
|
return
|
||||||
|
|
||||||
|
note = Notes.get_note_by_id(data["note_id"])
|
||||||
|
if not note:
|
||||||
|
log.error(f"Note {data['note_id']} not found for user {user.id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if (
|
||||||
|
user.role != "admin"
|
||||||
|
and user.id != note.user_id
|
||||||
|
and not has_access(user.id, type="read", access_control=note.access_control)
|
||||||
|
):
|
||||||
|
log.error(f"User {user.id} does not have access to note {data['note_id']}")
|
||||||
|
return
|
||||||
|
|
||||||
|
log.debug(f"Joining note {note.id} for user {user.id}")
|
||||||
|
await sio.enter_room(sid, f"note:{note.id}")
|
||||||
|
|
||||||
|
|
||||||
@sio.on("channel-events")
|
@sio.on("channel-events")
|
||||||
async def channel_events(sid, data):
|
async def channel_events(sid, data):
|
||||||
room = f"channel:{data['channel_id']}"
|
room = f"channel:{data['channel_id']}"
|
||||||
|
|
|
||||||
88
package-lock.json
generated
88
package-lock.json
generated
|
|
@ -22,6 +22,7 @@
|
||||||
"@tiptap/core": "^3.0.7",
|
"@tiptap/core": "^3.0.7",
|
||||||
"@tiptap/extension-bubble-menu": "^2.26.1",
|
"@tiptap/extension-bubble-menu": "^2.26.1",
|
||||||
"@tiptap/extension-code-block-lowlight": "^3.0.7",
|
"@tiptap/extension-code-block-lowlight": "^3.0.7",
|
||||||
|
"@tiptap/extension-drag-handle": "^3.0.7",
|
||||||
"@tiptap/extension-file-handler": "^3.0.7",
|
"@tiptap/extension-file-handler": "^3.0.7",
|
||||||
"@tiptap/extension-floating-menu": "^2.26.1",
|
"@tiptap/extension-floating-menu": "^2.26.1",
|
||||||
"@tiptap/extension-highlight": "^3.0.7",
|
"@tiptap/extension-highlight": "^3.0.7",
|
||||||
|
|
@ -30,6 +31,7 @@
|
||||||
"@tiptap/extension-list": "^3.0.7",
|
"@tiptap/extension-list": "^3.0.7",
|
||||||
"@tiptap/extension-table": "^3.0.7",
|
"@tiptap/extension-table": "^3.0.7",
|
||||||
"@tiptap/extension-typography": "^3.0.7",
|
"@tiptap/extension-typography": "^3.0.7",
|
||||||
|
"@tiptap/extension-youtube": "^3.0.7",
|
||||||
"@tiptap/extensions": "^3.0.7",
|
"@tiptap/extensions": "^3.0.7",
|
||||||
"@tiptap/pm": "^3.0.7",
|
"@tiptap/pm": "^3.0.7",
|
||||||
"@tiptap/starter-kit": "^3.0.7",
|
"@tiptap/starter-kit": "^3.0.7",
|
||||||
|
|
@ -3218,6 +3220,23 @@
|
||||||
"lowlight": "^2 || ^3"
|
"lowlight": "^2 || ^3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/extension-collaboration": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-collaboration/-/extension-collaboration-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-so59vQCAS1vy6k86byk96fYvAPM5w8u8/Yp3jKF1LPi9LH4wzS4hGnOP/dEbedxPU48an9WB1lSOczSKPECJaQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.0.7",
|
||||||
|
"@tiptap/pm": "^3.0.7",
|
||||||
|
"@tiptap/y-tiptap": "^3.0.0-beta.3",
|
||||||
|
"yjs": "^13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tiptap/extension-document": {
|
"node_modules/@tiptap/extension-document": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.0.7.tgz",
|
||||||
|
|
@ -3231,6 +3250,26 @@
|
||||||
"@tiptap/core": "^3.0.7"
|
"@tiptap/core": "^3.0.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/extension-drag-handle": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-drag-handle/-/extension-drag-handle-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-rm8+0kPz5C5JTp4f1QY61Qd5d7zlJAxLeJtOvgC9RCnrNG1F7LCsmOkvy5fsU6Qk2YCCYOiSSMC4S4HKPrUJhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.6.13"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.0.7",
|
||||||
|
"@tiptap/extension-collaboration": "^3.0.7",
|
||||||
|
"@tiptap/extension-node-range": "^3.0.7",
|
||||||
|
"@tiptap/pm": "^3.0.7",
|
||||||
|
"@tiptap/y-tiptap": "^3.0.0-beta.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tiptap/extension-dropcursor": {
|
"node_modules/@tiptap/extension-dropcursor": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.0.7.tgz",
|
||||||
|
|
@ -3425,6 +3464,21 @@
|
||||||
"@tiptap/extension-list": "^3.0.7"
|
"@tiptap/extension-list": "^3.0.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/extension-node-range": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-node-range/-/extension-node-range-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-cHViNqtOUD9CLJxEj28rcj8tb8RYQZ7kwmtSvIye84Y3MJIzigRm4IUBNNOYnZfq5YAZIR97WKcJeFz3EU1VPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.0.7",
|
||||||
|
"@tiptap/pm": "^3.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tiptap/extension-ordered-list": {
|
"node_modules/@tiptap/extension-ordered-list": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.0.7.tgz",
|
||||||
|
|
@ -3531,6 +3585,19 @@
|
||||||
"@tiptap/core": "^3.0.7"
|
"@tiptap/core": "^3.0.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/extension-youtube": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-youtube/-/extension-youtube-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-BD4rc7Xoi3O+puXSEArHAbBVu4dhj+9TuuVYzEFgNHI+FN/py9J5AiNf4TXGKBSlMUOYPpODaEROwyGmqAmpuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tiptap/extensions": {
|
"node_modules/@tiptap/extensions": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.0.7.tgz",
|
||||||
|
|
@ -3611,6 +3678,27 @@
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/y-tiptap": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/y-tiptap/-/y-tiptap-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-HIeJZCj+KYJde2x6fONzo4o6kd7gW7eonwhQsv2p2VQnUgwNXMVhN+D6Z3AH/2i541Sq33y1PO4U/1ThCPjqbA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"lib0": "^0.2.100"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0",
|
||||||
|
"npm": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"prosemirror-model": "^1.7.1",
|
||||||
|
"prosemirror-state": "^1.2.3",
|
||||||
|
"prosemirror-view": "^1.9.10",
|
||||||
|
"y-protocols": "^1.0.1",
|
||||||
|
"yjs": "^13.5.38"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/cookie": {
|
"node_modules/@types/cookie": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@
|
||||||
"@tiptap/core": "^3.0.7",
|
"@tiptap/core": "^3.0.7",
|
||||||
"@tiptap/extension-bubble-menu": "^2.26.1",
|
"@tiptap/extension-bubble-menu": "^2.26.1",
|
||||||
"@tiptap/extension-code-block-lowlight": "^3.0.7",
|
"@tiptap/extension-code-block-lowlight": "^3.0.7",
|
||||||
|
"@tiptap/extension-drag-handle": "^3.0.7",
|
||||||
"@tiptap/extension-file-handler": "^3.0.7",
|
"@tiptap/extension-file-handler": "^3.0.7",
|
||||||
"@tiptap/extension-floating-menu": "^2.26.1",
|
"@tiptap/extension-floating-menu": "^2.26.1",
|
||||||
"@tiptap/extension-highlight": "^3.0.7",
|
"@tiptap/extension-highlight": "^3.0.7",
|
||||||
|
|
@ -74,6 +75,7 @@
|
||||||
"@tiptap/extension-list": "^3.0.7",
|
"@tiptap/extension-list": "^3.0.7",
|
||||||
"@tiptap/extension-table": "^3.0.7",
|
"@tiptap/extension-table": "^3.0.7",
|
||||||
"@tiptap/extension-typography": "^3.0.7",
|
"@tiptap/extension-typography": "^3.0.7",
|
||||||
|
"@tiptap/extension-youtube": "^3.0.7",
|
||||||
"@tiptap/extensions": "^3.0.7",
|
"@tiptap/extensions": "^3.0.7",
|
||||||
"@tiptap/pm": "^3.0.7",
|
"@tiptap/pm": "^3.0.7",
|
||||||
"@tiptap/starter-kit": "^3.0.7",
|
"@tiptap/starter-kit": "^3.0.7",
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,10 @@
|
||||||
import { ListKit } from '@tiptap/extension-list';
|
import { ListKit } from '@tiptap/extension-list';
|
||||||
import { Placeholder, CharacterCount } from '@tiptap/extensions';
|
import { Placeholder, CharacterCount } from '@tiptap/extensions';
|
||||||
|
|
||||||
|
import Image from './RichTextInput/Image/index.js';
|
||||||
|
// import TiptapImage from '@tiptap/extension-image';
|
||||||
|
|
||||||
|
import FileHandler from '@tiptap/extension-file-handler';
|
||||||
import Typography from '@tiptap/extension-typography';
|
import Typography from '@tiptap/extension-typography';
|
||||||
import Highlight from '@tiptap/extension-highlight';
|
import Highlight from '@tiptap/extension-highlight';
|
||||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
||||||
|
|
@ -106,11 +110,15 @@
|
||||||
|
|
||||||
export let socket = null;
|
export let socket = null;
|
||||||
export let user = null;
|
export let user = null;
|
||||||
|
export let files = [];
|
||||||
|
|
||||||
export let documentId = '';
|
export let documentId = '';
|
||||||
|
|
||||||
export let className = 'input-prose';
|
export let className = 'input-prose';
|
||||||
export let placeholder = 'Type here...';
|
export let placeholder = 'Type here...';
|
||||||
export let link = false;
|
export let link = false;
|
||||||
|
export let image = false;
|
||||||
|
export let fileHandler = false;
|
||||||
|
|
||||||
export let id = '';
|
export let id = '';
|
||||||
export let value = '';
|
export let value = '';
|
||||||
|
|
@ -819,7 +827,9 @@
|
||||||
editor = new Editor({
|
editor = new Editor({
|
||||||
element: element,
|
element: element,
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit,
|
StarterKit.configure({
|
||||||
|
link: link
|
||||||
|
}),
|
||||||
Placeholder.configure({ placeholder }),
|
Placeholder.configure({ placeholder }),
|
||||||
|
|
||||||
CodeBlockLowlight.configure({
|
CodeBlockLowlight.configure({
|
||||||
|
|
@ -838,6 +848,60 @@
|
||||||
}),
|
}),
|
||||||
CharacterCount.configure({}),
|
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();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
|
||||||
...(autocomplete
|
...(autocomplete
|
||||||
? [
|
? [
|
||||||
AIAutocompletion.configure({
|
AIAutocompletion.configure({
|
||||||
|
|
@ -1093,6 +1157,11 @@
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onBeforeCreate: ({ editor }) => {
|
||||||
|
if (files) {
|
||||||
|
editor.storage.files = files;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
197
src/lib/components/common/RichTextInput/Image/image.ts
Normal file
197
src/lib/components/common/RichTextInput/Image/image.ts
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
import { mergeAttributes, Node, nodeInputRule } from '@tiptap/core';
|
||||||
|
|
||||||
|
export interface ImageOptions {
|
||||||
|
/**
|
||||||
|
* Controls if the image node should be inline or not.
|
||||||
|
* @default false
|
||||||
|
* @example true
|
||||||
|
*/
|
||||||
|
inline: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls if base64 images are allowed. Enable this if you want to allow
|
||||||
|
* base64 image urls in the `src` attribute.
|
||||||
|
* @default false
|
||||||
|
* @example true
|
||||||
|
*/
|
||||||
|
allowBase64: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTML attributes to add to the image element.
|
||||||
|
* @default {}
|
||||||
|
* @example { class: 'foo' }
|
||||||
|
*/
|
||||||
|
HTMLAttributes: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetImageOptions {
|
||||||
|
src: string;
|
||||||
|
alt?: string;
|
||||||
|
title?: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
image: {
|
||||||
|
/**
|
||||||
|
* Add an image
|
||||||
|
* @param options The image attributes
|
||||||
|
* @example
|
||||||
|
* editor
|
||||||
|
* .commands
|
||||||
|
* .setImage({ src: 'https://tiptap.dev/logo.png', alt: 'tiptap', title: 'tiptap logo' })
|
||||||
|
*/
|
||||||
|
setImage: (options: SetImageOptions) => ReturnType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches an image to a  on input.
|
||||||
|
*/
|
||||||
|
export const inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This extension allows you to insert images.
|
||||||
|
* @see https://www.tiptap.dev/api/nodes/image
|
||||||
|
*/
|
||||||
|
export const Image = Node.create<ImageOptions>({
|
||||||
|
name: 'image',
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
inline: false,
|
||||||
|
allowBase64: false,
|
||||||
|
HTMLAttributes: {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
inline() {
|
||||||
|
return this.options.inline;
|
||||||
|
},
|
||||||
|
|
||||||
|
group() {
|
||||||
|
return this.options.inline ? 'inline' : 'block';
|
||||||
|
},
|
||||||
|
|
||||||
|
draggable: true,
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
file: {
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
src: {
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
alt: {
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: this.options.allowBase64 ? 'img[src]' : 'img[src]:not([src^="data:"])'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
if (HTMLAttributes.file) {
|
||||||
|
delete HTMLAttributes.file;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ({ node, editor }) => {
|
||||||
|
const domImg = document.createElement('img');
|
||||||
|
domImg.setAttribute('src', node.attrs.src || '');
|
||||||
|
domImg.setAttribute('alt', node.attrs.alt || '');
|
||||||
|
domImg.setAttribute('title', node.attrs.title || '');
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const img = document.createElement('img');
|
||||||
|
|
||||||
|
const fileId = node.attrs.src.replace('data://', '');
|
||||||
|
img.setAttribute('id', `image:${fileId}`);
|
||||||
|
|
||||||
|
img.classList.add('rounded-md', 'max-h-72', 'w-fit', 'object-contain');
|
||||||
|
|
||||||
|
const editorFiles = editor.storage?.files || [];
|
||||||
|
|
||||||
|
if (editorFiles && node.attrs.src.startsWith('data://')) {
|
||||||
|
const file = editorFiles.find((f) => f.id === fileId);
|
||||||
|
if (file) {
|
||||||
|
img.setAttribute('src', file.url || '');
|
||||||
|
} else {
|
||||||
|
img.setAttribute('src', node.attrs.src || '');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
img.setAttribute('src', node.attrs.src || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
img.setAttribute('alt', node.attrs.alt || '');
|
||||||
|
img.setAttribute('title', node.attrs.title || '');
|
||||||
|
|
||||||
|
img.addEventListener('data', (e) => {
|
||||||
|
const files = e?.files || [];
|
||||||
|
if (files && node.attrs.src.startsWith('data://')) {
|
||||||
|
const file = editorFiles.find((f) => f.id === fileId);
|
||||||
|
if (file) {
|
||||||
|
img.setAttribute('src', file.url || '');
|
||||||
|
} else {
|
||||||
|
img.setAttribute('src', node.attrs.src || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
container.append(img);
|
||||||
|
return {
|
||||||
|
dom: img,
|
||||||
|
contentDOM: domImg
|
||||||
|
};
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setImage:
|
||||||
|
(options) =>
|
||||||
|
({ commands }) => {
|
||||||
|
return commands.insertContent({
|
||||||
|
type: this.name,
|
||||||
|
attrs: options
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addInputRules() {
|
||||||
|
return [
|
||||||
|
nodeInputRule({
|
||||||
|
find: inputRegex,
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: (match) => {
|
||||||
|
const [, , alt, src, title] = match;
|
||||||
|
|
||||||
|
return { src, alt, title };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
5
src/lib/components/common/RichTextInput/Image/index.ts
Normal file
5
src/lib/components/common/RichTextInput/Image/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { Image } from './image.js';
|
||||||
|
|
||||||
|
export * from './image.js';
|
||||||
|
|
||||||
|
export default Image;
|
||||||
|
|
@ -432,6 +432,14 @@ ${content}
|
||||||
note.data.files = null;
|
note.data.files = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editor.storage.files = files;
|
||||||
|
// open the settings panel if it is not open
|
||||||
|
selectedPanel = 'settings';
|
||||||
|
|
||||||
|
if (!showPanel) {
|
||||||
|
showPanel = true;
|
||||||
|
}
|
||||||
|
|
||||||
changeDebounceHandler();
|
changeDebounceHandler();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -504,20 +512,39 @@ ${content}
|
||||||
|
|
||||||
imageUrl = await compressImageHandler(imageUrl, $settings, $config);
|
imageUrl = await compressImageHandler(imageUrl, $settings, $config);
|
||||||
|
|
||||||
files = [
|
const fileId = uuidv4();
|
||||||
...files,
|
const fileItem = {
|
||||||
{
|
id: fileId,
|
||||||
type: 'image',
|
type: 'image',
|
||||||
url: `${imageUrl}`
|
url: `${imageUrl}`
|
||||||
}
|
};
|
||||||
];
|
files = [...files, fileItem];
|
||||||
note.data.files = files;
|
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();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
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();
|
||||||
} else {
|
} else {
|
||||||
uploadFileHandler(file);
|
uploadFileHandler(file);
|
||||||
}
|
}
|
||||||
|
|
@ -773,8 +800,47 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
inputElement?.insertContent(content);
|
inputElement?.insertContent(content);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const noteEventHandler = async (_note) => {
|
||||||
|
console.log('noteEventHandler', _note);
|
||||||
|
if (_note.id !== id) return;
|
||||||
|
|
||||||
|
if (_note.access_control && _note.access_control !== note.access_control) {
|
||||||
|
note.access_control = _note.access_control;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_note.data && _note.data.files) {
|
||||||
|
files = _note.data.files;
|
||||||
|
note.data.files = files;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_note.title && _note.title) {
|
||||||
|
note.title = _note.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.storage.files = files;
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.type === 'image') {
|
||||||
|
const e = new CustomEvent('data', { files: files });
|
||||||
|
|
||||||
|
const img = document.getElementById(`image:${file.id}`);
|
||||||
|
if (img) {
|
||||||
|
img.dispatchEvent(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await tick();
|
await tick();
|
||||||
|
$socket?.emit('join-note', {
|
||||||
|
note_id: id,
|
||||||
|
auth: {
|
||||||
|
token: localStorage.token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$socket?.on('note-events', noteEventHandler);
|
||||||
|
|
||||||
if ($settings?.models) {
|
if ($settings?.models) {
|
||||||
selectedModelId = $settings?.models[0];
|
selectedModelId = $settings?.models[0];
|
||||||
|
|
@ -807,6 +873,8 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
console.log('destroy');
|
console.log('destroy');
|
||||||
|
$socket?.off('note-events', noteEventHandler);
|
||||||
|
|
||||||
const dropzoneElement = document.getElementById('note-editor');
|
const dropzoneElement = document.getElementById('note-editor');
|
||||||
|
|
||||||
if (dropzoneElement) {
|
if (dropzoneElement) {
|
||||||
|
|
@ -1111,52 +1179,21 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||||
></div>
|
></div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if files && files.length > 0}
|
|
||||||
<div class="mb-2.5 w-full flex gap-1 flex-wrap z-40">
|
|
||||||
{#each files as file, fileIdx}
|
|
||||||
<div class="w-fit">
|
|
||||||
{#if file.type === 'image'}
|
|
||||||
<Image
|
|
||||||
src={file.url}
|
|
||||||
imageClassName=" max-h-96 rounded-lg"
|
|
||||||
dismissible={true}
|
|
||||||
onDismiss={() => {
|
|
||||||
files = files.filter((item, idx) => idx !== fileIdx);
|
|
||||||
note.data.files = files.length > 0 ? files : null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<FileItem
|
|
||||||
item={file}
|
|
||||||
dismissible={true}
|
|
||||||
url={file.url}
|
|
||||||
name={file.name}
|
|
||||||
type={file.type}
|
|
||||||
size={file?.size}
|
|
||||||
loading={file.status === 'uploading'}
|
|
||||||
on:dismiss={() => {
|
|
||||||
files = files.filter((item) => item?.id !== file.id);
|
|
||||||
note.data.files = files.length > 0 ? files : null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<RichTextInput
|
<RichTextInput
|
||||||
bind:this={inputElement}
|
bind:this={inputElement}
|
||||||
bind:editor
|
bind:editor
|
||||||
|
id={`note-${note.id}`}
|
||||||
className="input-prose-sm px-0.5"
|
className="input-prose-sm px-0.5"
|
||||||
json={true}
|
json={true}
|
||||||
bind:value={note.data.content.json}
|
bind:value={note.data.content.json}
|
||||||
html={note.data?.content?.html}
|
html={note.data?.content?.html}
|
||||||
documentId={`note:${note.id}`}
|
documentId={`note:${note.id}`}
|
||||||
|
{files}
|
||||||
collaboration={true}
|
collaboration={true}
|
||||||
socket={$socket}
|
socket={$socket}
|
||||||
user={$user}
|
user={$user}
|
||||||
link={true}
|
link={true}
|
||||||
|
image={true}
|
||||||
placeholder={$i18n.t('Write something...')}
|
placeholder={$i18n.t('Write something...')}
|
||||||
editable={versionIdx === null && !editing}
|
editable={versionIdx === null && !editing}
|
||||||
onChange={(content) => {
|
onChange={(content) => {
|
||||||
|
|
@ -1312,7 +1349,14 @@ 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 bind:show={showPanel} bind:selectedModelId />
|
<Settings
|
||||||
|
bind:show={showPanel}
|
||||||
|
bind:selectedModelId
|
||||||
|
bind:files
|
||||||
|
onUpdate={() => {
|
||||||
|
changeDebounceHandler();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</NotePanel>
|
</NotePanel>
|
||||||
</PaneGroup>
|
</PaneGroup>
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,17 @@
|
||||||
|
|
||||||
import XMark from '$lib/components/icons/XMark.svelte';
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
import { models } from '$lib/stores';
|
import { models } from '$lib/stores';
|
||||||
|
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
||||||
|
import FileItem from '$lib/components/common/FileItem.svelte';
|
||||||
|
import Image from '$lib/components/common/Image.svelte';
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let selectedModelId = '';
|
export let selectedModelId = '';
|
||||||
|
export let files = [];
|
||||||
|
|
||||||
|
export let onUpdate = (files: any[]) => {
|
||||||
|
// Default no-op function
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center mb-1.5 pt-1.5">
|
<div class="flex items-center mb-1.5 pt-1.5">
|
||||||
|
|
@ -23,13 +31,62 @@
|
||||||
|
|
||||||
<div class=" font-medium text-base flex items-center gap-1">
|
<div class=" font-medium text-base flex items-center gap-1">
|
||||||
<div>
|
<div>
|
||||||
{$i18n.t('Settings')}
|
{$i18n.t('Controls')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
<div>
|
<div class="pb-10">
|
||||||
|
{#if files.length > 0}
|
||||||
|
<div class=" text-xs font-medium pb-1">Files</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
{#each files.filter((file) => file.type !== 'image') as file, fileIdx}
|
||||||
|
<FileItem
|
||||||
|
className="w-full"
|
||||||
|
item={file}
|
||||||
|
small={true}
|
||||||
|
edit={true}
|
||||||
|
dismissible={true}
|
||||||
|
url={file.url}
|
||||||
|
name={file.name}
|
||||||
|
type={file.type}
|
||||||
|
size={file?.size}
|
||||||
|
loading={file.status === 'uploading'}
|
||||||
|
on:dismiss={() => {
|
||||||
|
// Remove the file from the files array
|
||||||
|
files = files.filter((item) => item.id !== file.id);
|
||||||
|
files = files;
|
||||||
|
|
||||||
|
onUpdate(files);
|
||||||
|
}}
|
||||||
|
on:click={() => {
|
||||||
|
console.log(file);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<div class="flex items-center flex-wrap gap-2 mt-1.5">
|
||||||
|
{#each files.filter((file) => file.type === 'image') as file, fileIdx}
|
||||||
|
<Image
|
||||||
|
src={file.url}
|
||||||
|
imageClassName=" size-14 rounded-xl object-cover"
|
||||||
|
dismissible={true}
|
||||||
|
onDismiss={() => {
|
||||||
|
files = files.filter((item) => item.id !== file.id);
|
||||||
|
files = files;
|
||||||
|
|
||||||
|
onUpdate(files);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-2 border-gray-50 dark:border-gray-700/10" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class=" text-xs font-medium mb-1">Model</div>
|
<div class=" text-xs font-medium mb-1">Model</div>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue