Bläddra i källkod

enh: channel navbar

Timothy Jaeryang Baek 4 månader sedan
förälder
incheckning
74cacf8bf5

+ 1 - 0
backend/open_webui/models/channels.py

@@ -70,6 +70,7 @@ class ChannelTable:
             channel = ChannelModel(
                 **{
                     **form_data.model_dump(),
+                    "name": form_data.name.lower(),
                     "id": str(uuid.uuid4()),
                     "user_id": user_id,
                     "created_at": int(time.time_ns()),

+ 38 - 30
src/lib/components/channel/Channel.svelte

@@ -2,12 +2,13 @@
 	import { toast } from 'svelte-sonner';
 	import { onDestroy, onMount, tick } from 'svelte';
 
-	import { socket } from '$lib/stores';
+	import { showSidebar, socket } from '$lib/stores';
 	import { getChannelById, getChannelMessages, sendMessage } from '$lib/apis/channels';
 
 	import Messages from './Messages.svelte';
 	import MessageInput from './MessageInput.svelte';
 	import { goto } from '$app/navigation';
+	import Navbar from './Navbar.svelte';
 
 	export let id = '';
 
@@ -95,35 +96,42 @@
 	});
 </script>
 
-<div class="h-full md:max-w-[calc(100%-260px)] w-full max-w-full flex flex-col">
-	<div
-		class=" pb-2.5 max-w-full z-10 scrollbar-hidden w-full h-full pt-6 flex-1 flex flex-col-reverse overflow-auto"
-		id="messages-container"
-		bind:this={messagesContainerElement}
-		on:scroll={(e) => {
-			scrollEnd = Math.abs(messagesContainerElement.scrollTop) <= 50;
-		}}
-	>
-		{#key id}
-			<Messages
-				{channel}
-				{messages}
-				{top}
-				onLoad={async () => {
-					page += 1;
-
-					const newMessages = await getChannelMessages(localStorage.token, id, page);
-
-					if (newMessages.length === 0) {
-						top = true;
-						return;
-					}
-
-					messages = [...messages, ...newMessages];
-				}}
-			/>
-		{/key}
-	</div>
+<div
+	class="h-screen max-h-[100dvh] {$showSidebar
+		? 'md:max-w-[calc(100%-260px)]'
+		: ''} w-full max-w-full flex flex-col"
+>
+	{#if channel}
+		<Navbar {channel} />
+		<div
+			class=" pb-2.5 max-w-full z-10 scrollbar-hidden w-full h-full pt-6 flex-1 flex flex-col-reverse overflow-auto"
+			id="messages-container"
+			bind:this={messagesContainerElement}
+			on:scroll={(e) => {
+				scrollEnd = Math.abs(messagesContainerElement.scrollTop) <= 50;
+			}}
+		>
+			{#key id}
+				<Messages
+					{channel}
+					{messages}
+					{top}
+					onLoad={async () => {
+						page += 1;
+
+						const newMessages = await getChannelMessages(localStorage.token, id, page);
+
+						if (newMessages.length === 0) {
+							top = true;
+							return;
+						}
+
+						messages = [...messages, ...newMessages];
+					}}
+				/>
+			{/key}
+		</div>
+	{/if}
 
 	<div class=" pb-[1rem]">
 		<MessageInput onSubmit={submitHandler} {scrollToBottom} {scrollEnd} />

+ 84 - 0
src/lib/components/channel/Navbar.svelte

@@ -0,0 +1,84 @@
+<script lang="ts">
+	import { getContext } from 'svelte';
+	import { toast } from 'svelte-sonner';
+
+	import { showArchivedChats, showSidebar, user } from '$lib/stores';
+
+	import { slide } from 'svelte/transition';
+	import { page } from '$app/stores';
+
+	import UserMenu from '$lib/components/layout/Sidebar/UserMenu.svelte';
+	import MenuLines from '../icons/MenuLines.svelte';
+	import PencilSquare from '../icons/PencilSquare.svelte';
+
+	const i18n = getContext('i18n');
+
+	export let channel;
+</script>
+
+<div class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center">
+	<div
+		class=" bg-gradient-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] blur"
+	></div>
+
+	<div class=" flex max-w-full w-full mx-auto px-1 pt-0.5 bg-transparent">
+		<div class="flex items-center w-full max-w-full">
+			<div
+				class="{$showSidebar
+					? 'md:hidden'
+					: ''} mr-1 self-start flex flex-none items-center text-gray-600 dark:text-gray-400"
+			>
+				<button
+					id="sidebar-toggle-button"
+					class="cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
+					on:click={() => {
+						showSidebar.set(!$showSidebar);
+					}}
+					aria-label="Toggle Sidebar"
+				>
+					<div class=" m-auto self-center">
+						<MenuLines />
+					</div>
+				</button>
+			</div>
+
+			<div
+				class="flex-1 overflow-hidden max-w-full py-0.5
+			{$showSidebar ? 'ml-1' : ''}
+			"
+			>
+				<div class="line-clamp-1 capitalize font-medium font-primary text-lg">
+					{channel.name}
+				</div>
+			</div>
+
+			<div class="self-start flex flex-none items-center text-gray-600 dark:text-gray-400">
+				{#if $user !== undefined}
+					<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>
+</div>

+ 2 - 2
src/lib/components/chat/Chat.svelte

@@ -70,15 +70,15 @@
 		generateMoACompletion,
 		stopTask
 	} from '$lib/apis';
+	import { getTools } from '$lib/apis/tools';
 
 	import Banner from '../common/Banner.svelte';
 	import MessageInput from '$lib/components/chat/MessageInput.svelte';
 	import Messages from '$lib/components/chat/Messages.svelte';
-	import Navbar from '$lib/components/layout/Navbar.svelte';
+	import Navbar from '$lib/components/chat/Navbar.svelte';
 	import ChatControls from './ChatControls.svelte';
 	import EventConfirmDialog from '../common/ConfirmDialog.svelte';
 	import Placeholder from './Placeholder.svelte';
-	import { getTools } from '$lib/apis/tools';
 	import NotificationToast from '../NotificationToast.svelte';
 
 	export let chatIdProp = '';

+ 194 - 0
src/lib/components/chat/Navbar.svelte

@@ -0,0 +1,194 @@
+<script lang="ts">
+	import { getContext } from 'svelte';
+	import { toast } from 'svelte-sonner';
+
+	import {
+		WEBUI_NAME,
+		chatId,
+		mobile,
+		settings,
+		showArchivedChats,
+		showControls,
+		showSidebar,
+		temporaryChatEnabled,
+		user
+	} from '$lib/stores';
+
+	import { slide } from 'svelte/transition';
+	import { page } from '$app/stores';
+
+	import ShareChatModal from '../chat/ShareChatModal.svelte';
+	import ModelSelector from '../chat/ModelSelector.svelte';
+	import Tooltip from '../common/Tooltip.svelte';
+	import Menu from '$lib/components/layout/Navbar/Menu.svelte';
+	import UserMenu from '$lib/components/layout/Sidebar/UserMenu.svelte';
+	import MenuLines from '../icons/MenuLines.svelte';
+	import AdjustmentsHorizontal from '../icons/AdjustmentsHorizontal.svelte';
+
+	import PencilSquare from '../icons/PencilSquare.svelte';
+
+	const i18n = getContext('i18n');
+
+	export let initNewChat: Function;
+	export let title: string = $WEBUI_NAME;
+	export let shareEnabled: boolean = false;
+
+	export let chat;
+	export let selectedModels;
+	export let showModelSelector = true;
+
+	let showShareChatModal = false;
+	let showDownloadChatModal = false;
+</script>
+
+<ShareChatModal bind:show={showShareChatModal} chatId={$chatId} />
+
+<div class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center">
+	<div
+		class=" bg-gradient-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] blur"
+	></div>
+
+	<div class=" flex max-w-full w-full mx-auto px-1 pt-0.5 bg-transparent">
+		<div class="flex items-center w-full max-w-full">
+			<div
+				class="{$showSidebar
+					? 'md:hidden'
+					: ''} mr-1 self-start flex flex-none items-center text-gray-600 dark:text-gray-400"
+			>
+				<button
+					id="sidebar-toggle-button"
+					class="cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
+					on:click={() => {
+						showSidebar.set(!$showSidebar);
+					}}
+					aria-label="Toggle Sidebar"
+				>
+					<div class=" m-auto self-center">
+						<MenuLines />
+					</div>
+				</button>
+			</div>
+
+			<div
+				class="flex-1 overflow-hidden max-w-full py-0.5
+			{$showSidebar ? 'ml-1' : ''}
+			"
+			>
+				{#if showModelSelector}
+					<ModelSelector bind:selectedModels showSetDefault={!shareEnabled} />
+				{/if}
+			</div>
+
+			<div class="self-start flex flex-none items-center text-gray-600 dark:text-gray-400">
+				<!-- <div class="md:hidden flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> -->
+				{#if shareEnabled && chat && (chat.id || $temporaryChatEnabled)}
+					<Menu
+						{chat}
+						{shareEnabled}
+						shareHandler={() => {
+							showShareChatModal = !showShareChatModal;
+						}}
+						downloadHandler={() => {
+							showDownloadChatModal = !showDownloadChatModal;
+						}}
+					>
+						<button
+							class="flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
+							id="chat-context-menu-button"
+						>
+							<div class=" m-auto self-center">
+								<svg
+									xmlns="http://www.w3.org/2000/svg"
+									fill="none"
+									viewBox="0 0 24 24"
+									stroke-width="1.5"
+									stroke="currentColor"
+									class="size-5"
+								>
+									<path
+										stroke-linecap="round"
+										stroke-linejoin="round"
+										d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"
+									/>
+								</svg>
+							</div>
+						</button>
+					</Menu>
+				{:else if $mobile}
+					<Tooltip content={$i18n.t('Controls')}>
+						<button
+							class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
+							on:click={async () => {
+								await showControls.set(!$showControls);
+							}}
+							aria-label="Controls"
+						>
+							<div class=" m-auto self-center">
+								<AdjustmentsHorizontal className=" size-5" strokeWidth="0.5" />
+							</div>
+						</button>
+					</Tooltip>
+				{/if}
+
+				{#if !$mobile}
+					<Tooltip content={$i18n.t('Controls')}>
+						<button
+							class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
+							on:click={async () => {
+								await showControls.set(!$showControls);
+							}}
+							aria-label="Controls"
+						>
+							<div class=" m-auto self-center">
+								<AdjustmentsHorizontal className=" size-5" strokeWidth="0.5" />
+							</div>
+						</button>
+					</Tooltip>
+				{/if}
+
+				<Tooltip content={$i18n.t('New Chat')}>
+					<button
+						id="new-chat-button"
+						class=" flex {$showSidebar
+							? 'md:hidden'
+							: ''} cursor-pointer px-2 py-2 rounded-xl text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-850 transition"
+						on:click={() => {
+							initNewChat();
+						}}
+						aria-label="New Chat"
+					>
+						<div class=" m-auto self-center">
+							<PencilSquare className=" size-5" strokeWidth="2" />
+						</div>
+					</button>
+				</Tooltip>
+
+				{#if $user !== undefined}
+					<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>
+</div>

+ 1 - 1
src/lib/components/layout/Sidebar/CreateChannelModal.svelte

@@ -17,7 +17,7 @@
 	let loading = false;
 
 	$: if (name) {
-		name = name.replace(/\s/g, '-');
+		name = name.replace(/\s/g, '-').toLocaleLowerCase();
 	}
 
 	const submitHandler = async () => {