From 7fee84c06e665b895fc1193f3d5f06c2edbff400 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 3 May 2025 18:16:32 +0400 Subject: [PATCH 1/6] feat: notes --- backend/open_webui/main.py | 3 + .../versions/9f0c9cd09105_add_note_table.py | 33 +++ backend/open_webui/models/notes.py | 135 ++++++++++++ backend/open_webui/routers/notes.py | 163 ++++++++++++++ src/lib/apis/notes/index.ts | 168 +++++++++++++++ src/lib/components/admin/Functions.svelte | 2 +- src/lib/components/channel/Channel.svelte | 2 +- src/lib/components/chat/Chat.svelte | 2 +- src/lib/components/chat/Navbar.svelte | 4 +- .../components/layout/Sidebar/ChatItem.svelte | 67 +++--- src/lib/components/notes/Notes.svelte | 204 ++++++++++++++++++ src/lib/components/workspace/Knowledge.svelte | 2 +- src/lib/components/workspace/Models.svelte | 2 +- src/lib/components/workspace/Prompts.svelte | 2 +- src/lib/components/workspace/Tools.svelte | 2 +- src/routes/(app)/notes/+layout.svelte | 49 ++++- src/routes/(app)/notes/+page.svelte | 5 + 17 files changed, 801 insertions(+), 44 deletions(-) create mode 100644 backend/open_webui/migrations/versions/9f0c9cd09105_add_note_table.py create mode 100644 backend/open_webui/models/notes.py create mode 100644 backend/open_webui/routers/notes.py create mode 100644 src/lib/apis/notes/index.ts create mode 100644 src/lib/components/notes/Notes.svelte diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index ef38904c0..f19f81022 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -64,6 +64,7 @@ from open_webui.routers import ( auths, channels, chats, + notes, folders, configs, groups, @@ -996,6 +997,8 @@ app.include_router(users.router, prefix="/api/v1/users", tags=["users"]) app.include_router(channels.router, prefix="/api/v1/channels", tags=["channels"]) app.include_router(chats.router, prefix="/api/v1/chats", tags=["chats"]) +app.include_router(notes.router, prefix="/api/v1/notes", tags=["notes"]) + app.include_router(models.router, prefix="/api/v1/models", tags=["models"]) app.include_router(knowledge.router, prefix="/api/v1/knowledge", tags=["knowledge"]) diff --git a/backend/open_webui/migrations/versions/9f0c9cd09105_add_note_table.py b/backend/open_webui/migrations/versions/9f0c9cd09105_add_note_table.py new file mode 100644 index 000000000..8e983a2cf --- /dev/null +++ b/backend/open_webui/migrations/versions/9f0c9cd09105_add_note_table.py @@ -0,0 +1,33 @@ +"""Add note table + +Revision ID: 9f0c9cd09105 +Revises: 3781e22d8b01 +Create Date: 2025-05-03 03:00:00.000000 + +""" + +from alembic import op +import sqlalchemy as sa + +revision = "9f0c9cd09105" +down_revision = "3781e22d8b01" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "note", + sa.Column("id", sa.Text(), nullable=False, primary_key=True, unique=True), + sa.Column("user_id", sa.Text(), nullable=True), + sa.Column("title", sa.Text(), nullable=True), + sa.Column("data", sa.JSON(), nullable=True), + sa.Column("meta", sa.JSON(), nullable=True), + sa.Column("access_control", sa.JSON(), nullable=True), + sa.Column("created_at", sa.BigInteger(), nullable=True), + sa.Column("updated_at", sa.BigInteger(), nullable=True), + ) + + +def downgrade(): + op.drop_table("note") diff --git a/backend/open_webui/models/notes.py b/backend/open_webui/models/notes.py new file mode 100644 index 000000000..114ccdc57 --- /dev/null +++ b/backend/open_webui/models/notes.py @@ -0,0 +1,135 @@ +import json +import time +import uuid +from typing import Optional + +from open_webui.internal.db import Base, get_db +from open_webui.utils.access_control import has_access +from open_webui.models.users import Users, UserResponse + + +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON +from sqlalchemy import or_, func, select, and_, text +from sqlalchemy.sql import exists + +#################### +# Note DB Schema +#################### + + +class Note(Base): + __tablename__ = "note" + + id = Column(Text, primary_key=True) + user_id = Column(Text) + + title = Column(Text) + data = Column(JSON, nullable=True) + meta = Column(JSON, nullable=True) + + access_control = Column(JSON, nullable=True) + + created_at = Column(BigInteger) + updated_at = Column(BigInteger) + + +class NoteModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + user_id: str + + title: str + data: Optional[dict] = None + meta: Optional[dict] = None + + access_control: Optional[dict] = None + + created_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + + +#################### +# Forms +#################### + + +class NoteForm(BaseModel): + title: str + data: Optional[dict] = None + meta: Optional[dict] = None + access_control: Optional[dict] = None + + +class NoteUserResponse(NoteModel): + user: Optional[UserResponse] = None + + +class NoteTable: + def insert_new_note( + self, + form_data: NoteForm, + user_id: str, + ) -> Optional[NoteModel]: + with get_db() as db: + note = NoteModel( + **{ + "id": str(uuid.uuid4()), + "user_id": user_id, + **form_data.model_dump(), + "created_at": int(time.time_ns()), + "updated_at": int(time.time_ns()), + } + ) + + new_note = Note(**note.model_dump()) + + db.add(new_note) + db.commit() + return note + + def get_notes(self) -> list[NoteModel]: + with get_db() as db: + notes = db.query(Note).order_by(Note.updated_at.desc()).all() + return [NoteModel.model_validate(note) for note in notes] + + def get_notes_by_user_id( + self, user_id: str, permission: str = "write" + ) -> list[NoteModel]: + notes = self.get_notes() + return [ + note + for note in notes + if note.user_id == user_id + or has_access(user_id, permission, note.access_control) + ] + + def get_note_by_id(self, id: str) -> Optional[NoteModel]: + with get_db() as db: + note = db.query(Note).filter(Note.id == id).first() + return NoteModel.model_validate(note) if note else None + + def update_note_by_id(self, id: str, form_data: NoteForm) -> Optional[NoteModel]: + with get_db() as db: + note = db.query(Note).filter(Note.id == id).first() + if not note: + return None + + note.title = form_data.title + note.data = form_data.data + note.meta = form_data.meta + note.access_control = form_data.access_control + note.updated_at = int(time.time_ns()) + + db.commit() + return NoteModel.model_validate(note) if note else None + + def delete_note_by_id(self, id: str): + with get_db() as db: + db.query(Note).filter(Note.id == id).delete() + db.commit() + return True + + +Notes = NoteTable() diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py new file mode 100644 index 000000000..ed373e1b5 --- /dev/null +++ b/backend/open_webui/routers/notes.py @@ -0,0 +1,163 @@ +import json +import logging +from typing import Optional + + +from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks +from pydantic import BaseModel + +from open_webui.models.users import Users, UserNameResponse +from open_webui.models.notes import Notes, NoteModel, NoteForm, NoteUserResponse + +from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import SRC_LOG_LEVELS + + +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.access_control import has_access + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + +router = APIRouter() + +############################ +# GetNotes +############################ + + +@router.get("/", response_model=list[NoteUserResponse]) +async def get_notes(user=Depends(get_verified_user)): + notes = [ + NoteUserResponse( + **{ + **note.model_dump(), + "user": UserNameResponse( + **Users.get_user_by_id(note.user_id).model_dump() + ), + } + ) + for note in Notes.get_notes_by_user_id(user.id, "write") + ] + + return notes + + +@router.get("/list", response_model=list[NoteUserResponse]) +async def get_note_list(user=Depends(get_verified_user)): + notes = [ + NoteUserResponse( + **{ + **note.model_dump(), + "user": UserNameResponse( + **Users.get_user_by_id(note.user_id).model_dump() + ), + } + ) + for note in Notes.get_notes_by_user_id(user.id, "read") + ] + + return notes + + +############################ +# CreateNewNote +############################ + + +@router.post("/create", response_model=Optional[NoteModel]) +async def create_new_note(form_data: NoteForm, user=Depends(get_admin_user)): + try: + note = Notes.insert_new_note(form_data, user.id) + return note + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() + ) + + +############################ +# GetNoteById +############################ + + +@router.get("/{id}", response_model=Optional[NoteModel]) +async def get_note_by_id(id: str, user=Depends(get_verified_user)): + note = Notes.get_note_by_id(id) + if not note: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND + ) + + if user.role != "admin" and not has_access( + user.id, type="read", access_control=note.access_control + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() + ) + + return note + + +############################ +# UpdateNoteById +############################ + + +@router.post("/{id}/update", response_model=Optional[NoteModel]) +async def update_note_by_id( + id: str, form_data: NoteForm, user=Depends(get_verified_user) +): + note = Notes.get_note_by_id(id) + if not note: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND + ) + + if user.role != "admin" and not has_access( + user.id, type="write", access_control=note.access_control + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() + ) + + try: + note = Notes.update_note_by_id(id, form_data) + return note + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() + ) + + +############################ +# DeleteNoteById +############################ + + +@router.delete("/{id}/delete", response_model=bool) +async def delete_note_by_id(id: str, user=Depends(get_verified_user)): + note = Notes.get_note_by_id(id) + if not note: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND + ) + + if user.role != "admin" and not has_access( + user.id, type="write", access_control=note.access_control + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() + ) + + try: + note = Notes.delete_note_by_id(id) + return True + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() + ) diff --git a/src/lib/apis/notes/index.ts b/src/lib/apis/notes/index.ts new file mode 100644 index 000000000..445697fd3 --- /dev/null +++ b/src/lib/apis/notes/index.ts @@ -0,0 +1,168 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +type NoteItem = { + title: string; + content: string; + access_control?: null | object; +}; + +export const createNewNote = async (token: string, note: NoteItem) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...note + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getNotes = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getNoteById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/${id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateNoteById = async (token: string, id: string, note: NoteItem) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/${id}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...note + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteNoteById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/${id}/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/components/admin/Functions.svelte b/src/lib/components/admin/Functions.svelte index 87f4958d4..fbeb61596 100644 --- a/src/lib/components/admin/Functions.svelte +++ b/src/lib/components/admin/Functions.svelte @@ -192,7 +192,7 @@ - {$i18n.t('Functions')} | {$WEBUI_NAME} + {$i18n.t('Functions')} • {$WEBUI_NAME} diff --git a/src/lib/components/channel/Channel.svelte b/src/lib/components/channel/Channel.svelte index a0de69570..1b1ca25de 100644 --- a/src/lib/components/channel/Channel.svelte +++ b/src/lib/components/channel/Channel.svelte @@ -195,7 +195,7 @@ - #{channel?.name ?? 'Channel'} | Open WebUI + #{channel?.name ?? 'Channel'} • Open WebUI
{$chatTitle - ? `${$chatTitle.length > 30 ? `${$chatTitle.slice(0, 30)}...` : $chatTitle} | ${$WEBUI_NAME}` + ? `${$chatTitle.length > 30 ? `${$chatTitle.slice(0, 30)}...` : $chatTitle} • ${$WEBUI_NAME}` : `${$WEBUI_NAME}`} diff --git a/src/lib/components/chat/Navbar.svelte b/src/lib/components/chat/Navbar.svelte index 4fbc8029e..4687dd983 100644 --- a/src/lib/components/chat/Navbar.svelte +++ b/src/lib/components/chat/Navbar.svelte @@ -47,8 +47,8 @@ -
- {#if notes.length > 0} -
- -
-
-
- -
- -
-
-
+ {#each notes[timeRange] as note, idx (note.id)} +
+
+
+ +
+
{note.title}
+
-
+ {/each} {:else}
@@ -133,7 +164,9 @@ diff --git a/src/routes/(app)/notes/+layout.svelte b/src/routes/(app)/notes/+layout.svelte index 512cb2415..23a3ab72c 100644 --- a/src/routes/(app)/notes/+layout.svelte +++ b/src/routes/(app)/notes/+layout.svelte @@ -85,7 +85,7 @@
-
+
diff --git a/src/routes/(app)/notes/[id]/+page.svelte b/src/routes/(app)/notes/[id]/+page.svelte new file mode 100644 index 000000000..262d8555a --- /dev/null +++ b/src/routes/(app)/notes/[id]/+page.svelte @@ -0,0 +1,5 @@ + + +{$page.params.id} From a74297ed47fe1b31fa637d336cbf164a9aa76c04 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 3 May 2025 18:56:27 +0400 Subject: [PATCH 3/6] refac: styling --- src/lib/components/notes/Notes.svelte | 8 ++++---- src/routes/(app)/notes/+layout.svelte | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/components/notes/Notes.svelte b/src/lib/components/notes/Notes.svelte index 629e8b7b3..53da2e80a 100644 --- a/src/lib/components/notes/Notes.svelte +++ b/src/lib/components/notes/Notes.svelte @@ -101,8 +101,8 @@ {$i18n.t(timeRange)}
- {#each notes[timeRange] as note, idx (note.id)} -
+
+ {#each notes[timeRange] as note, idx (note.id)}
@@ -141,8 +141,8 @@
-
- {/each} + {/each} +
{/each} {:else}
diff --git a/src/routes/(app)/notes/+layout.svelte b/src/routes/(app)/notes/+layout.svelte index 23a3ab72c..e2ebec71a 100644 --- a/src/routes/(app)/notes/+layout.svelte +++ b/src/routes/(app)/notes/+layout.svelte @@ -46,9 +46,9 @@
From 52d32e8bf222593918b6c707f13897ba23889917 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 3 May 2025 21:02:21 +0400 Subject: [PATCH 4/6] refac: styling --- src/lib/components/notes/Notes.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/notes/Notes.svelte b/src/lib/components/notes/Notes.svelte index 53da2e80a..6ebb8be63 100644 --- a/src/lib/components/notes/Notes.svelte +++ b/src/lib/components/notes/Notes.svelte @@ -162,7 +162,7 @@
+ + {/if} + + +
+
diff --git a/src/lib/components/notes/Notes.svelte b/src/lib/components/notes/Notes.svelte index 6ebb8be63..2ff3503fb 100644 --- a/src/lib/components/notes/Notes.svelte +++ b/src/lib/components/notes/Notes.svelte @@ -95,68 +95,70 @@
- {#if Object.keys(notes).length > 0} - {#each Object.keys(notes) as timeRange} -
- {$i18n.t(timeRange)} -
+
+ {#if Object.keys(notes).length > 0} + {#each Object.keys(notes) as timeRange} +
+ {$i18n.t(timeRange)} +
-
- {#each notes[timeRange] as note, idx (note.id)} -
-
- -
-
{note.title}
-
- -
- {#if note.data?.content} - {note.data?.content} - {:else} - {$i18n.t('No content')} - {/if} -
- -
- - {/each} - {:else} -
-
-
- {$i18n.t('No Notes')} -
-
- {$i18n.t('Create your first note by clicking on the plus button below.')} +
+ {#if note.data?.content} + {note.data?.content} + {:else} + {$i18n.t('No content')} + {/if} +
+ +
+
+ {dayjs(note.updated_at / 1000000).fromNow()} +
+ +
+ {$i18n.t('By {{name}}', { + name: capitalizeFirstLetter( + note?.user?.name ?? note?.user?.email ?? $i18n.t('Deleted User') + ) + })} +
+
+
+ +
+
+ {/each} +
+ {/each} + {:else} +
+
+
+ {$i18n.t('No Notes')} +
+ +
+ {$i18n.t('Create your first note by clicking on the plus button below.')} +
-
- {/if} + {/if} +
diff --git a/src/routes/(app)/notes/+layout.svelte b/src/routes/(app)/notes/+layout.svelte index e2ebec71a..8b8792c94 100644 --- a/src/routes/(app)/notes/+layout.svelte +++ b/src/routes/(app)/notes/+layout.svelte @@ -85,7 +85,7 @@
-
+
diff --git a/src/routes/(app)/notes/[id]/+page.svelte b/src/routes/(app)/notes/[id]/+page.svelte index 262d8555a..737c0e00f 100644 --- a/src/routes/(app)/notes/[id]/+page.svelte +++ b/src/routes/(app)/notes/[id]/+page.svelte @@ -1,5 +1,6 @@ -{$page.params.id} + From 4acd278624f0005d5dd83a8b7b466ebbb34b72d6 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 3 May 2025 22:53:23 +0400 Subject: [PATCH 6/6] refac --- .../components/channel/MessageInput.svelte | 6 +- src/lib/components/chat/MessageInput.svelte | 6 +- .../chat/MessageInput/VoiceRecording.svelte | 149 +++++++++------- .../components/common/RichTextInput.svelte | 164 +++++++++-------- src/lib/components/icons/Calendar.svelte | 19 ++ src/lib/components/icons/CalendarSolid.svelte | 11 ++ src/lib/components/icons/Users.svelte | 19 ++ src/lib/components/notes/NoteEditor.svelte | 167 ++++++++++++------ src/lib/components/notes/Notes.svelte | 39 ++-- src/lib/components/playground/Notes.svelte | 6 +- .../KnowledgeBase/AddTextContentModal.svelte | 6 +- 11 files changed, 370 insertions(+), 222 deletions(-) create mode 100644 src/lib/components/icons/Calendar.svelte create mode 100644 src/lib/components/icons/CalendarSolid.svelte create mode 100644 src/lib/components/icons/Users.svelte diff --git a/src/lib/components/channel/MessageInput.svelte b/src/lib/components/channel/MessageInput.svelte index 9f495a8de..d60851fc8 100644 --- a/src/lib/components/channel/MessageInput.svelte +++ b/src/lib/components/channel/MessageInput.svelte @@ -357,14 +357,14 @@ {#if recording} { + onCancel={async () => { recording = false; await tick(); document.getElementById(`chat-input-${id}`)?.focus(); }} - on:confirm={async (e) => { - const { text, filename } = e.detail; + onConfirm={async (data) => { + const { text, filename } = data; content = `${content}${text} `; recording = false; diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index d31861459..bcf28f61c 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -479,14 +479,14 @@ {#if recording} { + onCancel={async () => { recording = false; await tick(); document.getElementById('chat-input')?.focus(); }} - on:confirm={async (e) => { - const { text, filename } = e.detail; + onConfirm={async (data) => { + const { text, filename } = data; prompt = `${prompt}${text} `; recording = false; diff --git a/src/lib/components/chat/MessageInput/VoiceRecording.svelte b/src/lib/components/chat/MessageInput/VoiceRecording.svelte index 70472055f..726e26bbb 100644 --- a/src/lib/components/chat/MessageInput/VoiceRecording.svelte +++ b/src/lib/components/chat/MessageInput/VoiceRecording.svelte @@ -1,6 +1,6 @@
diff --git a/src/lib/components/icons/Calendar.svelte b/src/lib/components/icons/Calendar.svelte new file mode 100644 index 000000000..4406015fd --- /dev/null +++ b/src/lib/components/icons/Calendar.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/src/lib/components/icons/CalendarSolid.svelte b/src/lib/components/icons/CalendarSolid.svelte new file mode 100644 index 000000000..3f75fc91b --- /dev/null +++ b/src/lib/components/icons/CalendarSolid.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/src/lib/components/icons/Users.svelte b/src/lib/components/icons/Users.svelte new file mode 100644 index 000000000..9ef032dd7 --- /dev/null +++ b/src/lib/components/icons/Users.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/src/lib/components/notes/NoteEditor.svelte b/src/lib/components/notes/NoteEditor.svelte index c12511e04..3d2ebf837 100644 --- a/src/lib/components/notes/NoteEditor.svelte +++ b/src/lib/components/notes/NoteEditor.svelte @@ -3,26 +3,58 @@ const i18n = getContext('i18n'); import { toast } from 'svelte-sonner'; - import { getNoteById } from '$lib/apis/notes'; + + import { showSidebar } from '$lib/stores'; + import { goto } from '$app/navigation'; + + import dayjs from '$lib/dayjs'; + import calendar from 'dayjs/plugin/calendar'; + import duration from 'dayjs/plugin/duration'; + import relativeTime from 'dayjs/plugin/relativeTime'; + + dayjs.extend(calendar); + dayjs.extend(duration); + dayjs.extend(relativeTime); + + async function loadLocale(locales) { + for (const locale of locales) { + try { + dayjs.locale(locale); + break; // Stop after successfully loading the first available locale + } catch (error) { + console.error(`Could not load locale '${locale}':`, error); + } + } + } + + // Assuming $i18n.languages is an array of language codes + $: loadLocale($i18n.languages); + + import { getNoteById, updateNoteById } from '$lib/apis/notes'; import RichTextInput from '../common/RichTextInput.svelte'; import Spinner from '../common/Spinner.svelte'; - import Sparkles from '../icons/Sparkles.svelte'; - import SparklesSolid from '../icons/SparklesSolid.svelte'; import Mic from '../icons/Mic.svelte'; import VoiceRecording from '../chat/MessageInput/VoiceRecording.svelte'; import Tooltip from '../common/Tooltip.svelte'; - import { showSidebar } from '$lib/stores'; + + import Calendar from '../icons/Calendar.svelte'; + import Users from '../icons/Users.svelte'; export let id: null | string = null; - let title = ''; - let data = { - content: '', - files: [] + let note = { + title: '', + data: { + content: { + json: null, + html: '', + md: '' + } + }, + meta: null, + access_control: null }; - let meta = null; - let accessControl = null; let voiceInput = false; let loading = false; @@ -35,15 +67,38 @@ }); if (res) { - title = res.title; - data = res.data; - meta = res.meta; - accessControl = res.access_control; + note = res; + } else { + toast.error($i18n.t('Note not found')); + goto('/notes'); + return; } loading = false; }; + let debounceTimeout: NodeJS.Timeout | null = null; + + const changeDebounceHandler = () => { + console.log('debounce'); + if (debounceTimeout) { + clearTimeout(debounceTimeout); + } + + debounceTimeout = setTimeout(async () => { + const res = await updateNoteById(localStorage.token, id, { + ...note, + title: note.title === '' ? $i18n.t('Untitled') : note.title + }).catch((e) => { + toast.error(`${e}`); + }); + }, 200); + }; + + $: if (note) { + changeDebounceHandler(); + } + $: if (id) { init(); } @@ -56,35 +111,55 @@
- {/if} + {:else} +
+
+
+ +
+
-
-
-
- + + + +
+ +
+ { + note.data.html = content.html; + note.data.md = content.md; + }} />
- -
- -
-
+ {/if}
-
+
@@ -93,24 +168,12 @@ { + transcribe={false} + onCancel={() => { voiceInput = false; }} - on:confirm={(e) => { - const { text, filename } = e.detail; - - // url is hostname + /cache/audio/transcription/ + filename - const url = `${window.location.origin}/cache/audio/transcription/${filename}`; - - // Open in new tab - - if (content.trim() !== '') { - content = `${content}\n\n${text}\n\nRecording: ${url}\n\n`; - } else { - content = `${content}${text}\n\nRecording: ${url}\n\n`; - } - - voiceInput = false; + onConfirm={(data) => { + console.log(data); }} />
diff --git a/src/lib/components/notes/Notes.svelte b/src/lib/components/notes/Notes.svelte index 2ff3503fb..fd8ff6b1b 100644 --- a/src/lib/components/notes/Notes.svelte +++ b/src/lib/components/notes/Notes.svelte @@ -58,7 +58,11 @@ const res = await createNewNote(localStorage.token, { title: $i18n.t('New Note'), data: { - content: '' + content: { + json: null, + html: '', + md: '' + } }, meta: null, access_control: null @@ -95,30 +99,37 @@
-
+
{#if Object.keys(notes).length > 0} {#each Object.keys(notes) as timeRange}
{$i18n.t(timeRange)}
-