From 27d2fbbe33ea7137e85c88c8643b2c84e1851821 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 21 Dec 2024 18:12:44 -0700 Subject: [PATCH 01/34] refac: sidebar styling --- src/lib/components/common/Folder.svelte | 32 +- src/lib/components/layout/Sidebar.svelte | 322 +++++++++--------- .../components/layout/Sidebar/Folders.svelte | 2 +- 3 files changed, 183 insertions(+), 173 deletions(-) diff --git a/src/lib/components/common/Folder.svelte b/src/lib/components/common/Folder.svelte index ab1507545..70d646136 100644 --- a/src/lib/components/common/Folder.svelte +++ b/src/lib/components/common/Folder.svelte @@ -1,4 +1,4 @@ - + +
+ { + if ($mobile) { + showSidebar.set(false); + } + }} + draggable="false" + > +
+ + + + +
+ {name} +
+
+
+ + + +
diff --git a/src/lib/components/layout/Sidebar/CreateChannelModal.svelte b/src/lib/components/layout/Sidebar/CreateChannelModal.svelte new file mode 100644 index 000000000..ca629efa7 --- /dev/null +++ b/src/lib/components/layout/Sidebar/CreateChannelModal.svelte @@ -0,0 +1,138 @@ + + + +
+
+
{$i18n.t('Create Channel')}
+ +
+ +
+
+
{ + submitHandler(); + }} + > +
+
{$i18n.t('Channel Name')}
+ +
+ +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+
diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 0319a5b25..63d3ee29f 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -23,6 +23,8 @@ export const theme = writable('system'); export const chatId = writable(''); export const chatTitle = writable(''); + +export const channels = writable([]); export const chats = writable([]); export const pinnedChats = writable([]); export const tags = writable([]); From e444f769f607746b2b5767b11e0dadcb115051a9 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sun, 22 Dec 2024 04:49:24 -0700 Subject: [PATCH 06/34] refac --- src/lib/components/channel/Channel.svelte | 5 ++ src/lib/components/channel/Messages.svelte | 63 +++++++++++++++++++ .../channel/Messages/Message.svelte | 0 src/lib/components/layout/Sidebar.svelte | 34 +++++----- .../layout/Sidebar/ChannelItem.svelte | 22 ++++--- .../components/layout/Sidebar/Folders.svelte | 1 - src/routes/(app)/channels/[id]/+page.svelte | 7 +++ 7 files changed, 106 insertions(+), 26 deletions(-) create mode 100644 src/lib/components/channel/Channel.svelte create mode 100644 src/lib/components/channel/Messages.svelte create mode 100644 src/lib/components/channel/Messages/Message.svelte create mode 100644 src/routes/(app)/channels/[id]/+page.svelte diff --git a/src/lib/components/channel/Channel.svelte b/src/lib/components/channel/Channel.svelte new file mode 100644 index 000000000..01e35b802 --- /dev/null +++ b/src/lib/components/channel/Channel.svelte @@ -0,0 +1,5 @@ + + +{id} diff --git a/src/lib/components/channel/Messages.svelte b/src/lib/components/channel/Messages.svelte new file mode 100644 index 000000000..8b23c18c5 --- /dev/null +++ b/src/lib/components/channel/Messages.svelte @@ -0,0 +1,63 @@ + + +
+
+ {#key channelId} +
+ {#if messages.at(0)?.parentId !== null} + { + console.log('visible'); + if (!messagesLoading) { + loadMoreMessages(); + } + }} + > +
+ +
Loading...
+
+
+ {/if} + + {#each messages as message, messageIdx (message.id)} + + {/each} +
+
+ {/key} +
+
diff --git a/src/lib/components/channel/Messages/Message.svelte b/src/lib/components/channel/Messages/Message.svelte new file mode 100644 index 000000000..e69de29bb diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index e7a07f2f1..eb8f8f111 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -544,20 +544,24 @@ ? 'opacity-20' : ''}" > - { - showCreateChannel = true; - }} - onAddLabel={$i18n.t('Create Channel')} - > - {#each $channels as channel} - - {/each} - + {#if $user.role === 'admin' || $channels.length > 0} + { + showCreateChannel = true; + } + : null} + onAddLabel={$i18n.t('Create Channel')} + > + {#each $channels as channel} + + {/each} + + {/if} 0}
{ localStorage.setItem('showPinnedChat', e.detail); diff --git a/src/lib/components/layout/Sidebar/ChannelItem.svelte b/src/lib/components/layout/Sidebar/ChannelItem.svelte index 2ca788012..5f0ac85c9 100644 --- a/src/lib/components/layout/Sidebar/ChannelItem.svelte +++ b/src/lib/components/layout/Sidebar/ChannelItem.svelte @@ -5,7 +5,7 @@ const dispatch = createEventDispatcher(); - import { mobile, showSidebar } from '$lib/stores'; + import { mobile, showSidebar, user } from '$lib/stores'; import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte'; export let className = ''; @@ -50,14 +50,16 @@
- - + {/if}
diff --git a/src/lib/components/layout/Sidebar/Folders.svelte b/src/lib/components/layout/Sidebar/Folders.svelte index fab7aee1f..f3d468589 100644 --- a/src/lib/components/layout/Sidebar/Folders.svelte +++ b/src/lib/components/layout/Sidebar/Folders.svelte @@ -19,7 +19,6 @@ {#each folderList as folderId (folderId)} { diff --git a/src/routes/(app)/channels/[id]/+page.svelte b/src/routes/(app)/channels/[id]/+page.svelte new file mode 100644 index 000000000..512fbeaff --- /dev/null +++ b/src/routes/(app)/channels/[id]/+page.svelte @@ -0,0 +1,7 @@ + + + From 2914c29ab3adffdc3389ceb630b2f1570359677e Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sun, 22 Dec 2024 15:11:10 -0700 Subject: [PATCH 07/34] refac: styling --- src/lib/components/layout/Sidebar.svelte | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index eb8f8f111..cfa9e713d 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -544,9 +544,8 @@ ? 'opacity-20' : ''}" > - {#if $user.role === 'admin' || $channels.length > 0} + {#if ($user.role === 'admin' || $channels.length > 0) && !search} Date: Sun, 22 Dec 2024 17:16:14 -0700 Subject: [PATCH 08/34] refac --- src/lib/components/layout/Sidebar.svelte | 253 +++++++++--------- .../components/layout/Sidebar/Folders.svelte | 1 + 2 files changed, 130 insertions(+), 124 deletions(-) diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index cfa9e713d..779933749 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -53,6 +53,7 @@ import { getChannels, createNewChannel } from '$lib/apis/channels'; import CreateChannelModal from './Sidebar/CreateChannelModal.svelte'; import ChannelItem from './Sidebar/ChannelItem.svelte'; + import PencilSquare from '../icons/PencilSquare.svelte'; const BREAKPOINT = 768; @@ -433,36 +434,6 @@ : 'invisible'}" > + + { + selectedChatId = null; + await goto('/'); + const newChatButton = document.getElementById('new-chat-button'); + setTimeout(() => { + newChatButton?.click(); + if ($mobile) { + showSidebar.set(false); + } + }, 0); + }} + > +
+
+ logo +
+
+ {$i18n.t('New Chat')} +
+
+ +
+ +
+
{#if $user?.role === 'admin' || $user?.permissions?.workspace?.models || $user?.permissions?.workspace?.knowledge || $user?.permissions?.workspace?.prompts || $user?.permissions?.workspace?.tools} @@ -544,6 +551,82 @@ ? 'opacity-20' : ''}" > + {#if !search && $pinnedChats.length > 0} +
+ { + localStorage.setItem('showPinnedChat', e.detail); + console.log(e.detail); + }} + on:import={(e) => { + importChatHandler(e.detail, true); + }} + on:drop={async (e) => { + const { type, id, item } = e.detail; + + if (type === 'chat') { + let chat = await getChatById(localStorage.token, id).catch((error) => { + return null; + }); + if (!chat && item) { + chat = await importChat(localStorage.token, item.chat, item?.meta ?? {}); + } + + if (chat) { + console.log(chat); + if (chat.folder_id) { + const res = await updateChatFolderIdById( + localStorage.token, + chat.id, + null + ).catch((error) => { + toast.error(error); + return null; + }); + } + + if (!chat.pinned) { + const res = await toggleChatPinnedStatusById(localStorage.token, chat.id); + } + + initChatList(); + } + } + }} + name={$i18n.t('Pinned')} + > +
+ {#each $pinnedChats as chat, idx} + { + selectedChatId = chat.id; + }} + on:unselect={() => { + selectedChatId = null; + }} + on:change={async () => { + initChatList(); + }} + on:tag={(e) => { + const { type, name } = e.detail; + tagEventHandler(type, name, chat.id); + }} + /> + {/each} +
+
+
+ {/if} + {#if ($user.role === 'admin' || $channels.length > 0) && !search} {/if} + {#if !search && folders} + { + const { folderId, items } = e.detail; + importChatHandler(items, false, folderId); + }} + on:update={async (e) => { + initChatList(); + }} + on:change={async () => { + initChatList(); + }} + /> + {/if} + { importChatHandler(e.detail); }} @@ -621,99 +718,7 @@
{/if} - {#if !search && $pinnedChats.length > 0} -
- { - localStorage.setItem('showPinnedChat', e.detail); - console.log(e.detail); - }} - on:import={(e) => { - importChatHandler(e.detail, true); - }} - on:drop={async (e) => { - const { type, id, item } = e.detail; - - if (type === 'chat') { - let chat = await getChatById(localStorage.token, id).catch((error) => { - return null; - }); - if (!chat && item) { - chat = await importChat(localStorage.token, item.chat, item?.meta ?? {}); - } - - if (chat) { - console.log(chat); - if (chat.folder_id) { - const res = await updateChatFolderIdById( - localStorage.token, - chat.id, - null - ).catch((error) => { - toast.error(error); - return null; - }); - } - - if (!chat.pinned) { - const res = await toggleChatPinnedStatusById(localStorage.token, chat.id); - } - - initChatList(); - } - } - }} - name={$i18n.t('Pinned')} - > -
- {#each $pinnedChats as chat, idx} - { - selectedChatId = chat.id; - }} - on:unselect={() => { - selectedChatId = null; - }} - on:change={async () => { - initChatList(); - }} - on:tag={(e) => { - const { type, name } = e.detail; - tagEventHandler(type, name, chat.id); - }} - /> - {/each} -
-
-
- {/if} -
- {#if !search && folders} - { - const { folderId, items } = e.detail; - importChatHandler(items, false, folderId); - }} - on:update={async (e) => { - initChatList(); - }} - on:change={async () => { - initChatList(); - }} - /> - {/if} -
{#if $chats} {#each $chats as chat, idx} diff --git a/src/lib/components/layout/Sidebar/Folders.svelte b/src/lib/components/layout/Sidebar/Folders.svelte index f3d468589..fb0c955c5 100644 --- a/src/lib/components/layout/Sidebar/Folders.svelte +++ b/src/lib/components/layout/Sidebar/Folders.svelte @@ -19,6 +19,7 @@ {#each folderList as folderId (folderId)} { From f1d21fc59a52bd7ca117f73411fc65231f02f6eb Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sun, 22 Dec 2024 19:40:01 -0700 Subject: [PATCH 09/34] feat: channel socket integration --- backend/open_webui/models/channels.py | 14 +- backend/open_webui/models/messages.py | 4 +- backend/open_webui/routers/channels.py | 28 +- backend/open_webui/socket/main.py | 8 +- src/lib/apis/channels/index.ts | 71 +++++ src/lib/components/channel/Channel.svelte | 93 +++++- .../components/channel/MessageInput.svelte | 266 ++++++++++++++++++ src/lib/components/channel/Messages.svelte | 27 +- .../channel/Messages/Message.svelte | 13 + src/lib/components/chat/MessageInput.svelte | 2 +- src/lib/components/common/Image.svelte | 1 + src/lib/components/layout/Sidebar.svelte | 5 +- src/routes/+layout.svelte | 8 +- 13 files changed, 509 insertions(+), 31 deletions(-) create mode 100644 src/lib/components/channel/MessageInput.svelte diff --git a/backend/open_webui/models/channels.py b/backend/open_webui/models/channels.py index 0e31d5e8e..cc49953e7 100644 --- a/backend/open_webui/models/channels.py +++ b/backend/open_webui/models/channels.py @@ -4,8 +4,7 @@ import uuid from typing import Optional from open_webui.internal.db import Base, get_db -from open_webui.models.tags import TagModel, Tag, Tags - +from open_webui.utils.access_control import has_access from pydantic import BaseModel, ConfigDict from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON @@ -85,6 +84,17 @@ class ChannelTable: channels = db.query(Channel).all() return [ChannelModel.model_validate(channel) for channel in channels] + def get_channels_by_user_id( + self, user_id: str, permission: str = "read" + ) -> list[ChannelModel]: + channels = self.get_channels() + return [ + channel + for channel in channels + if channel.user_id == user_id + or has_access(user_id, permission, channel.access_control) + ] + def get_channel_by_id(self, id: str) -> Optional[ChannelModel]: with get_db() as db: channel = db.query(Channel).filter(Channel.id == id).first() diff --git a/backend/open_webui/models/messages.py b/backend/open_webui/models/messages.py index c9161da96..8e4306cd5 100644 --- a/backend/open_webui/models/messages.py +++ b/backend/open_webui/models/messages.py @@ -95,7 +95,7 @@ class MessageTable: all_messages = ( db.query(Message) .filter_by(channel_id=channel_id) - .order_by(Message.updated_at.desc()) + .order_by(Message.updated_at.asc()) .limit(limit) .offset(skip) .all() @@ -109,7 +109,7 @@ class MessageTable: all_messages = ( db.query(Message) .filter_by(user_id=user_id) - .order_by(Message.updated_at.desc()) + .order_by(Message.updated_at.asc()) .limit(limit) .offset(skip) .all() diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index b4b458f25..c73c601f7 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -2,6 +2,12 @@ import json import logging from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Request, status +from pydantic import BaseModel + + +from open_webui.socket.main import sio from open_webui.models.channels import Channels, ChannelModel, ChannelForm from open_webui.models.messages import Messages, MessageModel, MessageForm @@ -9,12 +15,10 @@ from open_webui.models.messages import Messages, MessageModel, MessageForm 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 fastapi import APIRouter, Depends, HTTPException, Request, status -from pydantic import BaseModel from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.utils.access_control import has_permission +from open_webui.utils.access_control import has_access log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) @@ -53,7 +57,7 @@ async def create_new_channel(form_data: ChannelForm, user=Depends(get_admin_user ############################ -@router.post("/{id}/messages", response_model=list[MessageModel]) +@router.get("/{id}/messages", response_model=list[MessageModel]) async def get_channel_messages(id: str, page: int = 1, user=Depends(get_verified_user)): channel = Channels.get_channel_by_id(id) if not channel: @@ -61,7 +65,7 @@ async def get_channel_messages(id: str, page: int = 1, user=Depends(get_verified status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND ) - if not has_permission(channel.access_control, user): + if not has_access(user.id, type="read", access_control=channel.access_control): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() ) @@ -87,13 +91,25 @@ async def post_new_message( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND ) - if not has_permission(channel.access_control, user): + if not has_access(user.id, type="read", access_control=channel.access_control): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() ) try: message = Messages.insert_new_message(form_data, channel.id, user.id) + + if message: + await sio.emit( + "channel-events", + { + "channel_id": channel.id, + "message_id": message.id, + "data": {"message": message.model_dump()}, + }, + to=f"channel:{channel.id}", + ) + return MessageModel(**message.model_dump()) except Exception as e: log.exception(e) diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py index 965fb9396..23be163e9 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -5,6 +5,7 @@ import sys import time from open_webui.models.users import Users +from open_webui.models.channels import Channels from open_webui.env import ( ENABLE_WEBSOCKET_SUPPORT, WEBSOCKET_MANAGER, @@ -162,7 +163,6 @@ async def connect(sid, environ, auth): @sio.on("user-join") async def user_join(sid, data): - # print("user-join", sid, data) auth = data["auth"] if "auth" in data else None if not auth or "token" not in auth: @@ -182,6 +182,12 @@ async def user_join(sid, data): else: USER_POOL[user.id] = [sid] + # Join all the channels + channels = Channels.get_channels_by_user_id(user.id) + log.debug(f"{channels=}") + for channel in channels: + await sio.enter_room(sid, f"channel:{channel.id}") + # print(f"user {user.name}({user.id}) connected with session ID {sid}") await sio.emit("user-count", {"count": len(USER_POOL.items())}) diff --git a/src/lib/apis/channels/index.ts b/src/lib/apis/channels/index.ts index 8fd6f24f1..84f372fa0 100644 --- a/src/lib/apis/channels/index.ts +++ b/src/lib/apis/channels/index.ts @@ -69,3 +69,74 @@ export const getChannels = async (token: string = '') => { return res; }; + + +export const getChannelMessages = async (token: string = '', channel_id: string, page: number = 1) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/messages?page=${page}`, { + 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; +} + +type MessageForm = { + content: string; + data?: object; + meta?: object; + +} + +export const sendMessage = async (token: string = '', channel_id: string, message: MessageForm) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/post`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ ...message }) + }) + .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; +} \ No newline at end of file diff --git a/src/lib/components/channel/Channel.svelte b/src/lib/components/channel/Channel.svelte index 01e35b802..ca5cbda67 100644 --- a/src/lib/components/channel/Channel.svelte +++ b/src/lib/components/channel/Channel.svelte @@ -1,5 +1,96 @@ -{id} +
+ + +
+ +
+
diff --git a/src/lib/components/channel/MessageInput.svelte b/src/lib/components/channel/MessageInput.svelte new file mode 100644 index 000000000..be987f1cc --- /dev/null +++ b/src/lib/components/channel/MessageInput.svelte @@ -0,0 +1,266 @@ + + +
+
+
+ {#if recording} + { + recording = false; + + await tick(); + document.getElementById('chat-input')?.focus(); + }} + on:confirm={async (e) => { + const { text, filename } = e.detail; + content = `${content}${text} `; + recording = false; + + await tick(); + document.getElementById('chat-input')?.focus(); + }} + /> + {:else} +
{ + submitHandler(); + }} + > +
+
+
+ +
+ + {#if $settings?.richTextInput ?? true} +
+ 0 || + navigator.msMaxTouchPoints > 0 + )} + {placeholder} + largeTextAsFile={$settings?.largeTextAsFile ?? false} + on:keydown={async (e) => { + e = e.detail.event; + const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac + if ( + !$mobile || + !( + 'ontouchstart' in window || + navigator.maxTouchPoints > 0 || + navigator.msMaxTouchPoints > 0 + ) + ) { + // Prevent Enter key from creating a new line + // Uses keyCode '13' for Enter key for chinese/japanese keyboards + if (e.keyCode === 13 && !e.shiftKey) { + e.preventDefault(); + } + + // Submit the content when Enter key is pressed + if (content !== '' && e.keyCode === 13 && !e.shiftKey) { + submitHandler(); + } + } + + if (e.key === 'Escape') { + console.log('Escape'); + } + }} + on:paste={async (e) => { + e = e.detail.event; + console.log(e); + }} + /> +
+ {:else} +