mirror of
				https://github.com/open-webui/open-webui
				synced 2025-06-26 18:26:48 +00:00 
			
		
		
		
	feat: notes
This commit is contained in:
		
							parent
							
								
									7d184c3a14
								
							
						
					
					
						commit
						7fee84c06e
					
				@ -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"])
 | 
			
		||||
 | 
			
		||||
@ -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")
 | 
			
		||||
							
								
								
									
										135
									
								
								backend/open_webui/models/notes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								backend/open_webui/models/notes.py
									
									
									
									
									
										Normal file
									
								
							@ -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()
 | 
			
		||||
							
								
								
									
										163
									
								
								backend/open_webui/routers/notes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								backend/open_webui/routers/notes.py
									
									
									
									
									
										Normal file
									
								
							@ -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()
 | 
			
		||||
        )
 | 
			
		||||
							
								
								
									
										168
									
								
								src/lib/apis/notes/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								src/lib/apis/notes/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
};
 | 
			
		||||
@ -192,7 +192,7 @@
 | 
			
		||||
 | 
			
		||||
<svelte:head>
 | 
			
		||||
	<title>
 | 
			
		||||
		{$i18n.t('Functions')} | {$WEBUI_NAME}
 | 
			
		||||
		{$i18n.t('Functions')} • {$WEBUI_NAME}
 | 
			
		||||
	</title>
 | 
			
		||||
</svelte:head>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -195,7 +195,7 @@
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<svelte:head>
 | 
			
		||||
	<title>#{channel?.name ?? 'Channel'} | Open WebUI</title>
 | 
			
		||||
	<title>#{channel?.name ?? 'Channel'} • Open WebUI</title>
 | 
			
		||||
</svelte:head>
 | 
			
		||||
 | 
			
		||||
<div
 | 
			
		||||
 | 
			
		||||
@ -1921,7 +1921,7 @@
 | 
			
		||||
<svelte:head>
 | 
			
		||||
	<title>
 | 
			
		||||
		{$chatTitle
 | 
			
		||||
			? `${$chatTitle.length > 30 ? `${$chatTitle.slice(0, 30)}...` : $chatTitle} | ${$WEBUI_NAME}`
 | 
			
		||||
			? `${$chatTitle.length > 30 ? `${$chatTitle.slice(0, 30)}...` : $chatTitle} • ${$WEBUI_NAME}`
 | 
			
		||||
			: `${$WEBUI_NAME}`}
 | 
			
		||||
	</title>
 | 
			
		||||
</svelte:head>
 | 
			
		||||
 | 
			
		||||
@ -47,8 +47,8 @@
 | 
			
		||||
 | 
			
		||||
<ShareChatModal bind:show={showShareChatModal} chatId={$chatId} />
 | 
			
		||||
 | 
			
		||||
<nav class="sticky top-0 z-30 w-full py-1.5 -mb-8 flex flex-col items-center drag-region">
 | 
			
		||||
	<div class="flex items-center w-full px-1.5">
 | 
			
		||||
<nav class="sticky top-0 z-30 w-full py-1 -mb-8 flex flex-col items-center drag-region">
 | 
			
		||||
	<div class="flex items-center w-full pl-1.5 pr-1">
 | 
			
		||||
		<div
 | 
			
		||||
			class=" bg-linear-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1]"
 | 
			
		||||
		></div>
 | 
			
		||||
 | 
			
		||||
@ -38,6 +38,7 @@
 | 
			
		||||
	import Check from '$lib/components/icons/Check.svelte';
 | 
			
		||||
	import XMark from '$lib/components/icons/XMark.svelte';
 | 
			
		||||
	import Document from '$lib/components/icons/Document.svelte';
 | 
			
		||||
	import Sparkles from '$lib/components/icons/Sparkles.svelte';
 | 
			
		||||
 | 
			
		||||
	export let className = '';
 | 
			
		||||
 | 
			
		||||
@ -65,6 +66,7 @@
 | 
			
		||||
 | 
			
		||||
	let showShareChatModal = false;
 | 
			
		||||
	let confirmEdit = false;
 | 
			
		||||
	let editInputFocused = false;
 | 
			
		||||
 | 
			
		||||
	let chatTitle = title;
 | 
			
		||||
 | 
			
		||||
@ -133,10 +135,6 @@
 | 
			
		||||
		dispatch('change');
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const focusEdit = async (node: HTMLInputElement) => {
 | 
			
		||||
		node.focus();
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	let itemElement;
 | 
			
		||||
 | 
			
		||||
	let dragged = false;
 | 
			
		||||
@ -213,6 +211,20 @@
 | 
			
		||||
			chatTitle = '';
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const focusEditInput = async () => {
 | 
			
		||||
		console.log('focusEditInput');
 | 
			
		||||
		await tick();
 | 
			
		||||
 | 
			
		||||
		const input = document.getElementById(`chat-title-input-${id}`);
 | 
			
		||||
		if (input) {
 | 
			
		||||
			input.focus();
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	$: if (confirmEdit) {
 | 
			
		||||
		focusEditInput();
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<ShareChatModal bind:show={showShareChatModal} chatId={id} />
 | 
			
		||||
@ -257,11 +269,23 @@
 | 
			
		||||
					: 'group-hover:bg-gray-100 dark:group-hover:bg-gray-950'}  whitespace-nowrap text-ellipsis"
 | 
			
		||||
		>
 | 
			
		||||
			<input
 | 
			
		||||
				use:focusEdit
 | 
			
		||||
				bind:value={chatTitle}
 | 
			
		||||
				id="chat-title-input-{id}"
 | 
			
		||||
				bind:value={chatTitle}
 | 
			
		||||
				class=" bg-transparent w-full outline-hidden mr-10"
 | 
			
		||||
				on:keydown={chatTitleInputKeydownHandler}
 | 
			
		||||
				on:focus={() => {
 | 
			
		||||
					editInputFocused = true;
 | 
			
		||||
				}}
 | 
			
		||||
				on:blur={() => {
 | 
			
		||||
					if (editInputFocused) {
 | 
			
		||||
						if (chatTitle !== title) {
 | 
			
		||||
							editChatTitle(id, chatTitle);
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						confirmEdit = false;
 | 
			
		||||
						chatTitle = '';
 | 
			
		||||
					}
 | 
			
		||||
				}}
 | 
			
		||||
			/>
 | 
			
		||||
		</div>
 | 
			
		||||
	{:else}
 | 
			
		||||
@ -311,7 +335,7 @@
 | 
			
		||||
				: 'invisible group-hover:visible from-gray-100 dark:from-gray-950'}
 | 
			
		||||
            absolute {className === 'pr-2'
 | 
			
		||||
			? 'right-[8px]'
 | 
			
		||||
			: 'right-0'}  top-[4px] py-1 pr-0.5 mr-1.5 pl-5 bg-linear-to-l from-80%
 | 
			
		||||
			: 'right-1'} top-[4px] py-1 pr-0.5 mr-1.5 pl-5 bg-linear-to-l from-80%
 | 
			
		||||
 | 
			
		||||
              to-transparent"
 | 
			
		||||
		on:mouseenter={(e) => {
 | 
			
		||||
@ -325,28 +349,9 @@
 | 
			
		||||
			<div
 | 
			
		||||
				class="flex self-center items-center space-x-1.5 z-10 translate-y-[0.5px] -translate-x-[0.5px]"
 | 
			
		||||
			>
 | 
			
		||||
				<Tooltip content={$i18n.t('Confirm')}>
 | 
			
		||||
					<button
 | 
			
		||||
						class=" self-center dark:hover:text-white transition"
 | 
			
		||||
						on:click={() => {
 | 
			
		||||
							editChatTitle(id, chatTitle);
 | 
			
		||||
							confirmEdit = false;
 | 
			
		||||
							chatTitle = '';
 | 
			
		||||
						}}
 | 
			
		||||
					>
 | 
			
		||||
						<Check className=" size-3.5" strokeWidth="2.5" />
 | 
			
		||||
					</button>
 | 
			
		||||
				</Tooltip>
 | 
			
		||||
 | 
			
		||||
				<Tooltip content={$i18n.t('Cancel')}>
 | 
			
		||||
					<button
 | 
			
		||||
						class=" self-center dark:hover:text-white transition"
 | 
			
		||||
						on:click={() => {
 | 
			
		||||
							confirmEdit = false;
 | 
			
		||||
							chatTitle = '';
 | 
			
		||||
						}}
 | 
			
		||||
					>
 | 
			
		||||
						<XMark strokeWidth="2.5" />
 | 
			
		||||
				<Tooltip content={$i18n.t('Generate')}>
 | 
			
		||||
					<button class=" self-center dark:hover:text-white transition" on:click={() => {}}>
 | 
			
		||||
						<Sparkles strokeWidth="2" />
 | 
			
		||||
					</button>
 | 
			
		||||
				</Tooltip>
 | 
			
		||||
			</div>
 | 
			
		||||
@ -377,7 +382,7 @@
 | 
			
		||||
				</Tooltip>
 | 
			
		||||
			</div>
 | 
			
		||||
		{:else}
 | 
			
		||||
			<div class="flex self-center space-x-1 z-10">
 | 
			
		||||
			<div class="flex self-center z-10 items-end">
 | 
			
		||||
				<ChatMenu
 | 
			
		||||
					chatId={id}
 | 
			
		||||
					cloneChatHandler={() => {
 | 
			
		||||
@ -414,7 +419,7 @@
 | 
			
		||||
				>
 | 
			
		||||
					<button
 | 
			
		||||
						aria-label="Chat Menu"
 | 
			
		||||
						class=" self-center dark:hover:text-white transition"
 | 
			
		||||
						class=" self-center dark:hover:text-white transition m-0"
 | 
			
		||||
						on:click={() => {
 | 
			
		||||
							dispatch('select');
 | 
			
		||||
						}}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										204
									
								
								src/lib/components/notes/Notes.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								src/lib/components/notes/Notes.svelte
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,204 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { toast } from 'svelte-sonner';
 | 
			
		||||
	import fileSaver from 'file-saver';
 | 
			
		||||
	const { saveAs } = fileSaver;
 | 
			
		||||
 | 
			
		||||
	import { goto } from '$app/navigation';
 | 
			
		||||
	import { onMount, getContext } from 'svelte';
 | 
			
		||||
	import { WEBUI_NAME, config, prompts as _prompts, user } from '$lib/stores';
 | 
			
		||||
 | 
			
		||||
	import { getNotes } from '$lib/apis/notes';
 | 
			
		||||
 | 
			
		||||
	import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
 | 
			
		||||
	import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 | 
			
		||||
	import Search from '../icons/Search.svelte';
 | 
			
		||||
	import Plus from '../icons/Plus.svelte';
 | 
			
		||||
	import ChevronRight from '../icons/ChevronRight.svelte';
 | 
			
		||||
	import Spinner from '../common/Spinner.svelte';
 | 
			
		||||
	import Tooltip from '../common/Tooltip.svelte';
 | 
			
		||||
	import { capitalizeFirstLetter } from '$lib/utils';
 | 
			
		||||
 | 
			
		||||
	const i18n = getContext('i18n');
 | 
			
		||||
	let loaded = false;
 | 
			
		||||
 | 
			
		||||
	let importFiles = '';
 | 
			
		||||
	let query = '';
 | 
			
		||||
 | 
			
		||||
	let notes = [];
 | 
			
		||||
	let selectedNote = null;
 | 
			
		||||
 | 
			
		||||
	let showDeleteConfirm = false;
 | 
			
		||||
 | 
			
		||||
	const init = async () => {
 | 
			
		||||
		notes = await getNotes(localStorage.token);
 | 
			
		||||
 | 
			
		||||
		console.log(notes);
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	onMount(async () => {
 | 
			
		||||
		await init();
 | 
			
		||||
		loaded = true;
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<svelte:head>
 | 
			
		||||
	<title>
 | 
			
		||||
		{$i18n.t('Notes')} • {$WEBUI_NAME}
 | 
			
		||||
	</title>
 | 
			
		||||
</svelte:head>
 | 
			
		||||
 | 
			
		||||
{#if loaded}
 | 
			
		||||
	<DeleteConfirmDialog
 | 
			
		||||
		bind:show={showDeleteConfirm}
 | 
			
		||||
		title={$i18n.t('Delete note?')}
 | 
			
		||||
		on:confirm={() => {}}
 | 
			
		||||
	>
 | 
			
		||||
		<div class=" text-sm text-gray-500">
 | 
			
		||||
			{$i18n.t('This will delete')} <span class="  font-semibold">{selectedNote.title}</span>.
 | 
			
		||||
		</div>
 | 
			
		||||
	</DeleteConfirmDialog>
 | 
			
		||||
 | 
			
		||||
	{#if notes.length > 0}
 | 
			
		||||
		<div class="flex flex-col gap-1 my-1.5">
 | 
			
		||||
			<!-- <div class="flex justify-between items-center">
 | 
			
		||||
			<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
 | 
			
		||||
				{$i18n.t('Notes')}
 | 
			
		||||
				<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
 | 
			
		||||
				<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{notes.length}</span>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div> -->
 | 
			
		||||
 | 
			
		||||
			<div class=" flex w-full space-x-2">
 | 
			
		||||
				<div class="flex flex-1">
 | 
			
		||||
					<div class=" self-center ml-1 mr-3">
 | 
			
		||||
						<Search className="size-3.5" />
 | 
			
		||||
					</div>
 | 
			
		||||
					<input
 | 
			
		||||
						class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
 | 
			
		||||
						bind:value={query}
 | 
			
		||||
						placeholder={$i18n.t('Search Notes')}
 | 
			
		||||
					/>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="mb-5 gap-2 grid lg:grid-cols-2 xl:grid-cols-3">
 | 
			
		||||
			{#each notes as note}
 | 
			
		||||
				<div
 | 
			
		||||
					class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl transition"
 | 
			
		||||
				>
 | 
			
		||||
					<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
 | 
			
		||||
						<a href={`/notes/${note.id}`}>
 | 
			
		||||
							<div class=" flex-1 flex items-center gap-2 self-center">
 | 
			
		||||
								<div class=" font-semibold line-clamp-1 capitalize">{note.title}</div>
 | 
			
		||||
							</div>
 | 
			
		||||
 | 
			
		||||
							<div class=" text-xs px-0.5">
 | 
			
		||||
								<Tooltip
 | 
			
		||||
									content={note?.user?.email ?? $i18n.t('Deleted User')}
 | 
			
		||||
									className="flex shrink-0"
 | 
			
		||||
									placement="top-start"
 | 
			
		||||
								>
 | 
			
		||||
									<div class="shrink-0 text-gray-500">
 | 
			
		||||
										{$i18n.t('By {{name}}', {
 | 
			
		||||
											name: capitalizeFirstLetter(
 | 
			
		||||
												note?.user?.name ?? note?.user?.email ?? $i18n.t('Deleted User')
 | 
			
		||||
											)
 | 
			
		||||
										})}
 | 
			
		||||
									</div>
 | 
			
		||||
								</Tooltip>
 | 
			
		||||
							</div>
 | 
			
		||||
						</a>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			{/each}
 | 
			
		||||
		</div>
 | 
			
		||||
	{:else}
 | 
			
		||||
		<div class="w-full h-full flex flex-col items-center justify-center">
 | 
			
		||||
			<div class="pb-20 text-center">
 | 
			
		||||
				<div class=" text-xl font-medium text-gray-400 dark:text-gray-600">
 | 
			
		||||
					{$i18n.t('No Notes')}
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<div class="mt-1 text-sm text-gray-300 dark:text-gray-700">
 | 
			
		||||
					{$i18n.t('Create your first note by clicking on the plus button below.')}
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	{/if}
 | 
			
		||||
 | 
			
		||||
	<div class="absolute bottom-0 left-0 right-0 p-5 max-w-full flex justify-end">
 | 
			
		||||
		<div class="flex gap-0.5 justify-end w-full">
 | 
			
		||||
			<Tooltip content={$i18n.t('Create Note')}>
 | 
			
		||||
				<button
 | 
			
		||||
					class="cursor-pointer p-2.5 flex rounded-full bg-gray-50 dark:bg-gray-850 hover:bg-gray-100 dark:hover:bg-gray-800 transition shadow-xl"
 | 
			
		||||
					type="button"
 | 
			
		||||
					on:click={async () => {}}
 | 
			
		||||
				>
 | 
			
		||||
					<Plus className="size-4.5" strokeWidth="2.5" />
 | 
			
		||||
				</button>
 | 
			
		||||
			</Tooltip>
 | 
			
		||||
 | 
			
		||||
			<!-- <button
 | 
			
		||||
				class="cursor-pointer p-2.5 flex rounded-full hover:bg-gray-100 dark:hover:bg-gray-850 transition shadow-xl"
 | 
			
		||||
			>
 | 
			
		||||
				<SparklesSolid className="size-4" />
 | 
			
		||||
			</button> -->
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<!-- {#if $user?.role === 'admin'}
 | 
			
		||||
		<div class=" flex justify-end w-full mb-3">
 | 
			
		||||
			<div class="flex space-x-2">
 | 
			
		||||
				<input
 | 
			
		||||
					id="notes-import-input"
 | 
			
		||||
					bind:files={importFiles}
 | 
			
		||||
					type="file"
 | 
			
		||||
					accept=".md"
 | 
			
		||||
					hidden
 | 
			
		||||
					on:change={() => {
 | 
			
		||||
						console.log(importFiles);
 | 
			
		||||
 | 
			
		||||
						const reader = new FileReader();
 | 
			
		||||
						reader.onload = async (event) => {
 | 
			
		||||
							console.log(event.target.result);
 | 
			
		||||
						};
 | 
			
		||||
 | 
			
		||||
						reader.readAsText(importFiles[0]);
 | 
			
		||||
					}}
 | 
			
		||||
				/>
 | 
			
		||||
 | 
			
		||||
				<button
 | 
			
		||||
					class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
 | 
			
		||||
					on:click={() => {
 | 
			
		||||
						const notesImportInputElement = document.getElementById('notes-import-input');
 | 
			
		||||
						if (notesImportInputElement) {
 | 
			
		||||
							notesImportInputElement.click();
 | 
			
		||||
						}
 | 
			
		||||
					}}
 | 
			
		||||
				>
 | 
			
		||||
					<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Notes')}</div>
 | 
			
		||||
 | 
			
		||||
					<div class=" self-center">
 | 
			
		||||
						<svg
 | 
			
		||||
							xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
							viewBox="0 0 16 16"
 | 
			
		||||
							fill="currentColor"
 | 
			
		||||
							class="w-4 h-4"
 | 
			
		||||
						>
 | 
			
		||||
							<path
 | 
			
		||||
								fill-rule="evenodd"
 | 
			
		||||
								d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
 | 
			
		||||
								clip-rule="evenodd"
 | 
			
		||||
							/>
 | 
			
		||||
						</svg>
 | 
			
		||||
					</div>
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	{/if} -->
 | 
			
		||||
{:else}
 | 
			
		||||
	<div class="w-full h-full flex justify-center items-center">
 | 
			
		||||
		<Spinner />
 | 
			
		||||
	</div>
 | 
			
		||||
{/if}
 | 
			
		||||
@ -72,7 +72,7 @@
 | 
			
		||||
 | 
			
		||||
<svelte:head>
 | 
			
		||||
	<title>
 | 
			
		||||
		{$i18n.t('Knowledge')} | {$WEBUI_NAME}
 | 
			
		||||
		{$i18n.t('Knowledge')} • {$WEBUI_NAME}
 | 
			
		||||
	</title>
 | 
			
		||||
</svelte:head>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -196,7 +196,7 @@
 | 
			
		||||
 | 
			
		||||
<svelte:head>
 | 
			
		||||
	<title>
 | 
			
		||||
		{$i18n.t('Models')} | {$WEBUI_NAME}
 | 
			
		||||
		{$i18n.t('Models')} • {$WEBUI_NAME}
 | 
			
		||||
	</title>
 | 
			
		||||
</svelte:head>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -88,7 +88,7 @@
 | 
			
		||||
 | 
			
		||||
<svelte:head>
 | 
			
		||||
	<title>
 | 
			
		||||
		{$i18n.t('Prompts')} | {$WEBUI_NAME}
 | 
			
		||||
		{$i18n.t('Prompts')} • {$WEBUI_NAME}
 | 
			
		||||
	</title>
 | 
			
		||||
</svelte:head>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -164,7 +164,7 @@
 | 
			
		||||
 | 
			
		||||
<svelte:head>
 | 
			
		||||
	<title>
 | 
			
		||||
		{$i18n.t('Tools')} | {$WEBUI_NAME}
 | 
			
		||||
		{$i18n.t('Tools')} • {$WEBUI_NAME}
 | 
			
		||||
	</title>
 | 
			
		||||
</svelte:head>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,9 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { onMount, getContext } from 'svelte';
 | 
			
		||||
	import { WEBUI_NAME, showSidebar, functions, config } from '$lib/stores';
 | 
			
		||||
	import { WEBUI_NAME, showSidebar, functions, config, user, showArchivedChats } from '$lib/stores';
 | 
			
		||||
	import MenuLines from '$lib/components/icons/MenuLines.svelte';
 | 
			
		||||
	import { page } from '$app/stores';
 | 
			
		||||
	import UserMenu from '$lib/components/layout/Sidebar/UserMenu.svelte';
 | 
			
		||||
 | 
			
		||||
	const i18n = getContext('i18n');
 | 
			
		||||
 | 
			
		||||
@ -25,9 +26,9 @@
 | 
			
		||||
		? 'md:max-w-[calc(100%-260px)]'
 | 
			
		||||
		: ''} max-w-full"
 | 
			
		||||
>
 | 
			
		||||
	<nav class="   px-2.5 pt-1 backdrop-blur-xl w-full drag-region">
 | 
			
		||||
	<nav class="   px-2 pt-1 backdrop-blur-xl w-full drag-region">
 | 
			
		||||
		<div class=" flex items-center">
 | 
			
		||||
			<div class="{$showSidebar ? 'md:hidden' : ''} flex flex-none items-center self-end">
 | 
			
		||||
			<div class="{$showSidebar ? 'md:hidden' : ''} flex flex-none items-center">
 | 
			
		||||
				<button
 | 
			
		||||
					id="sidebar-toggle-button"
 | 
			
		||||
					class="cursor-pointer p-1.5 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
 | 
			
		||||
@ -41,10 +42,50 @@
 | 
			
		||||
					</div>
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div class="ml-2 py-0.5 self-center flex items-center justify-between w-full">
 | 
			
		||||
				<div class="">
 | 
			
		||||
					<div
 | 
			
		||||
						class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent py-1 touch-auto pointer-events-auto"
 | 
			
		||||
					>
 | 
			
		||||
						<a class="min-w-fit rounded-full transition" href="/notes">
 | 
			
		||||
							{$i18n.t('Notes')}
 | 
			
		||||
						</a>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<div class=" self-center flex items-center gap-1">
 | 
			
		||||
					{#if $user !== undefined && $user !== null}
 | 
			
		||||
						<UserMenu
 | 
			
		||||
							className="max-w-[200px]"
 | 
			
		||||
							role={$user?.role}
 | 
			
		||||
							on:show={(e) => {
 | 
			
		||||
								if (e.detail === 'archived-chat') {
 | 
			
		||||
									showArchivedChats.set(true);
 | 
			
		||||
								}
 | 
			
		||||
							}}
 | 
			
		||||
						>
 | 
			
		||||
							<button
 | 
			
		||||
								class="select-none flex rounded-xl p-1.5 w-full hover:bg-gray-50 dark:hover:bg-gray-850 transition"
 | 
			
		||||
								aria-label="User Menu"
 | 
			
		||||
							>
 | 
			
		||||
								<div class=" self-center">
 | 
			
		||||
									<img
 | 
			
		||||
										src={$user?.profile_image_url}
 | 
			
		||||
										class="size-6 object-cover rounded-full"
 | 
			
		||||
										alt="User profile"
 | 
			
		||||
										draggable="false"
 | 
			
		||||
									/>
 | 
			
		||||
								</div>
 | 
			
		||||
							</button>
 | 
			
		||||
						</UserMenu>
 | 
			
		||||
					{/if}
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</nav>
 | 
			
		||||
 | 
			
		||||
	<div class=" flex-1 max-h-full overflow-y-auto">
 | 
			
		||||
	<div class=" pb-1 px-[18px] flex-1 max-h-full overflow-y-auto">
 | 
			
		||||
		<slot />
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,5 @@
 | 
			
		||||
<script>
 | 
			
		||||
	import Notes from '$lib/components/notes/Notes.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Notes />
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user