Timothy Jaeryang Baek 6 hónapja
szülő
commit
2e85c8e24d

+ 5 - 3
backend/open_webui/models/channels.py

@@ -64,9 +64,9 @@ class ChannelTable:
         self, form_data: ChannelForm, user_id: str
         self, form_data: ChannelForm, user_id: str
     ) -> Optional[ChannelModel]:
     ) -> Optional[ChannelModel]:
         with get_db() as db:
         with get_db() as db:
-            new_channel = Channel(
+            channel = ChannelModel(
                 **{
                 **{
-                    **form_data.model_dump(),
+                    **form_data.dict(),
                     "id": str(uuid.uuid4()),
                     "id": str(uuid.uuid4()),
                     "user_id": user_id,
                     "user_id": user_id,
                     "created_at": int(time.time()),
                     "created_at": int(time.time()),
@@ -74,9 +74,11 @@ class ChannelTable:
                 }
                 }
             )
             )
 
 
+            new_channel = Channel(**channel.model_dump())
+
             db.add(new_channel)
             db.add(new_channel)
             db.commit()
             db.commit()
-            return new_channel
+            return channel
 
 
     def get_channels(self) -> list[ChannelModel]:
     def get_channels(self) -> list[ChannelModel]:
         with get_db() as db:
         with get_db() as db:

+ 71 - 0
src/lib/apis/channels/index.ts

@@ -0,0 +1,71 @@
+import { WEBUI_API_BASE_URL } from '$lib/constants';
+
+type ChannelForm = {
+	name: string;
+	data?: object;
+	meta?: object;
+	access_control?: object;
+}
+
+export const createNewChannel = async (token: string = '', channel: ChannelForm) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/channels/create`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({ ...channel })
+	})
+		.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 getChannels = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/channels/`, {
+		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;
+};

+ 26 - 6
src/lib/components/layout/Sidebar.svelte

@@ -16,7 +16,8 @@
 		pinnedChats,
 		pinnedChats,
 		scrollPaginationEnabled,
 		scrollPaginationEnabled,
 		currentChatPage,
 		currentChatPage,
-		temporaryChatEnabled
+		temporaryChatEnabled,
+		channels
 	} from '$lib/stores';
 	} from '$lib/stores';
 	import { onMount, getContext, tick, onDestroy } from 'svelte';
 	import { onMount, getContext, tick, onDestroy } from 'svelte';
 
 
@@ -49,6 +50,9 @@
 	import Plus from '../icons/Plus.svelte';
 	import Plus from '../icons/Plus.svelte';
 	import Tooltip from '../common/Tooltip.svelte';
 	import Tooltip from '../common/Tooltip.svelte';
 	import Folders from './Sidebar/Folders.svelte';
 	import Folders from './Sidebar/Folders.svelte';
+	import { getChannels, createNewChannel } from '$lib/apis/channels';
+	import CreateChannelModal from './Sidebar/CreateChannelModal.svelte';
+	import ChannelItem from './Sidebar/ChannelItem.svelte';
 
 
 	const BREAKPOINT = 768;
 	const BREAKPOINT = 768;
 
 
@@ -61,14 +65,14 @@
 	let showDropdown = false;
 	let showDropdown = false;
 	let showPinnedChat = true;
 	let showPinnedChat = true;
 
 
+	let showCreateChannel = false;
+
 	// Pagination variables
 	// Pagination variables
 	let chatListLoading = false;
 	let chatListLoading = false;
 	let allChatsLoaded = false;
 	let allChatsLoaded = false;
 
 
 	let folders = {};
 	let folders = {};
 
 
-	const createChannel = async () => {};
-
 	const initFolders = async () => {
 	const initFolders = async () => {
 		const folderList = await getFolders(localStorage.token).catch((error) => {
 		const folderList = await getFolders(localStorage.token).catch((error) => {
 			toast.error(error);
 			toast.error(error);
@@ -145,6 +149,10 @@
 		}
 		}
 	};
 	};
 
 
+	const initChannels = async () => {
+		channels.set(await getChannels(localStorage.token));
+	};
+
 	const initChatList = async () => {
 	const initChatList = async () => {
 		// Reset pagination variables
 		// Reset pagination variables
 		tags.set(await getAllTags(localStorage.token));
 		tags.set(await getAllTags(localStorage.token));
@@ -348,6 +356,7 @@
 			localStorage.sidebar = value;
 			localStorage.sidebar = value;
 		});
 		});
 
 
+		await initChannels();
 		await initChatList();
 		await initChatList();
 
 
 		window.addEventListener('keydown', onKeyDown);
 		window.addEventListener('keydown', onKeyDown);
@@ -391,6 +400,13 @@
 	}}
 	}}
 />
 />
 
 
+<CreateChannelModal
+	bind:show={showCreateChannel}
+	onChange={async () => {
+		await initChannels();
+	}}
+/>
+
 <!-- svelte-ignore a11y-no-static-element-interactions -->
 <!-- svelte-ignore a11y-no-static-element-interactions -->
 
 
 {#if $showSidebar}
 {#if $showSidebar}
@@ -533,10 +549,14 @@
 				className="px-2 mt-0.5"
 				className="px-2 mt-0.5"
 				name={$i18n.t('Channels')}
 				name={$i18n.t('Channels')}
 				dragAndDrop={false}
 				dragAndDrop={false}
-				onAdd={createChannel}
-				onAddLabel={$i18n.t('New Channel')}
+				onAdd={() => {
+					showCreateChannel = true;
+				}}
+				onAddLabel={$i18n.t('Create Channel')}
 			>
 			>
-				channels
+				{#each $channels as channel}
+					<ChannelItem id={channel.id} name={channel.name} />
+				{/each}
 			</Folder>
 			</Folder>
 
 
 			<Folder
 			<Folder

+ 63 - 0
src/lib/components/layout/Sidebar/ChannelItem.svelte

@@ -0,0 +1,63 @@
+<script lang="ts">
+	import { toast } from 'svelte-sonner';
+	import { onMount, getContext, createEventDispatcher, tick, onDestroy } from 'svelte';
+	const i18n = getContext('i18n');
+
+	const dispatch = createEventDispatcher();
+
+	import { mobile, showSidebar } from '$lib/stores';
+	import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
+
+	export let className = '';
+
+	export let id;
+	export let name;
+
+	let itemElement;
+</script>
+
+<div
+	bind:this={itemElement}
+	class=" w-full {className} rounded-lg flex relative group hover:bg-gray-100 dark:hover:bg-gray-900 px-2.5 py-1"
+>
+	<a
+		class=" w-full flex justify-between"
+		href="/channels/{id}"
+		on:click={() => {
+			if ($mobile) {
+				showSidebar.set(false);
+			}
+		}}
+		draggable="false"
+	>
+		<div class="flex items-center gap-1">
+			<svg
+				xmlns="http://www.w3.org/2000/svg"
+				viewBox="0 0 16 16"
+				fill="currentColor"
+				class="size-5"
+			>
+				<path
+					fill-rule="evenodd"
+					d="M7.487 2.89a.75.75 0 1 0-1.474-.28l-.455 2.388H3.61a.75.75 0 0 0 0 1.5h1.663l-.571 2.998H2.75a.75.75 0 0 0 0 1.5h1.666l-.403 2.114a.75.75 0 0 0 1.474.28l.456-2.394h2.973l-.403 2.114a.75.75 0 0 0 1.474.28l.456-2.394h1.947a.75.75 0 0 0 0-1.5h-1.661l.57-2.998h1.95a.75.75 0 0 0 0-1.5h-1.664l.402-2.108a.75.75 0 0 0-1.474-.28l-.455 2.388H7.085l.402-2.108ZM6.8 6.498l-.571 2.998h2.973l.57-2.998H6.8Z"
+					clip-rule="evenodd"
+				/>
+			</svg>
+
+			<div class=" text-left self-center overflow-hidden w-full line-clamp-1">
+				{name}
+			</div>
+		</div>
+	</a>
+
+	<button
+		class="absolute z-10 right-2.5 invisible group-hover:visible self-center flex items-center dark:text-gray-300"
+		on:pointerup={(e) => {
+			e.stopPropagation();
+		}}
+	>
+		<button class="p-0.5 dark:hover:bg-gray-850 rounded-lg touch-auto" on:click={(e) => {}}>
+			<EllipsisHorizontal className="size-4" strokeWidth="2.5" />
+		</button>
+	</button>
+</div>

+ 138 - 0
src/lib/components/layout/Sidebar/CreateChannelModal.svelte

@@ -0,0 +1,138 @@
+<script lang="ts">
+	import { getContext, createEventDispatcher } from 'svelte';
+
+	import { createNewChannel } from '$lib/apis/channels';
+
+	import Modal from '$lib/components/common/Modal.svelte';
+	import AccessControl from '$lib/components/workspace/common/AccessControl.svelte';
+	import { toast } from 'svelte-sonner';
+	const i18n = getContext('i18n');
+
+	export let show = false;
+	export let onChange: Function = () => {};
+
+	let name = '';
+	let accessControl = null;
+
+	let loading = false;
+
+	$: if (name) {
+		name = name.replace(/\s/g, '-');
+	}
+
+	const submitHandler = async () => {
+		loading = true;
+		const res = await createNewChannel(localStorage.token, {
+			name: name.replace(/\s/g, '-'),
+			access_control: accessControl
+		}).catch((error) => {
+			toast.error(error);
+			return null;
+		});
+
+		onChange();
+		show = false;
+
+		loading = false;
+	};
+</script>
+
+<Modal size="sm" bind:show>
+	<div>
+		<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-1">
+			<div class=" text-lg font-medium self-center">{$i18n.t('Create Channel')}</div>
+			<button
+				class="self-center"
+				on:click={() => {
+					show = false;
+				}}
+			>
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 20 20"
+					fill="currentColor"
+					class="w-5 h-5"
+				>
+					<path
+						fill-rule="evenodd"
+						d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+			</button>
+		</div>
+
+		<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
+			<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
+				<form
+					class="flex flex-col w-full"
+					on:submit|preventDefault={() => {
+						submitHandler();
+					}}
+				>
+					<div class="flex flex-col w-full mt-2">
+						<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Channel Name')}</div>
+
+						<div class="flex-1">
+							<input
+								class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+								type="text"
+								bind:value={name}
+								placeholder={$i18n.t('new-channel')}
+								autocomplete="off"
+							/>
+						</div>
+					</div>
+
+					<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
+
+					<div class="my-2 -mx-2">
+						<div class="px-3 py-2 bg-gray-50 dark:bg-gray-950 rounded-lg">
+							<AccessControl bind:accessControl />
+						</div>
+					</div>
+
+					<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
+						<button
+							class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-950 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center {loading
+								? ' cursor-not-allowed'
+								: ''}"
+							type="submit"
+							disabled={loading}
+						>
+							{$i18n.t('Create')}
+
+							{#if loading}
+								<div class="ml-2 self-center">
+									<svg
+										class=" w-4 h-4"
+										viewBox="0 0 24 24"
+										fill="currentColor"
+										xmlns="http://www.w3.org/2000/svg"
+										><style>
+											.spinner_ajPY {
+												transform-origin: center;
+												animation: spinner_AtaB 0.75s infinite linear;
+											}
+											@keyframes spinner_AtaB {
+												100% {
+													transform: rotate(360deg);
+												}
+											}
+										</style><path
+											d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
+											opacity=".25"
+										/><path
+											d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
+											class="spinner_ajPY"
+										/></svg
+									>
+								</div>
+							{/if}
+						</button>
+					</div>
+				</form>
+			</div>
+		</div>
+	</div>
+</Modal>

+ 2 - 0
src/lib/stores/index.ts

@@ -23,6 +23,8 @@ export const theme = writable('system');
 export const chatId = writable('');
 export const chatId = writable('');
 export const chatTitle = writable('');
 export const chatTitle = writable('');
 
 
+
+export const channels = writable([]);
 export const chats = writable([]);
 export const chats = writable([]);
 export const pinnedChats = writable([]);
 export const pinnedChats = writable([]);
 export const tags = writable([]);
 export const tags = writable([]);