Browse Source

feat: note chat

Timothy Jaeryang Baek 3 months ago
parent
commit
9bd001a14b

+ 1 - 1
src/lib/components/channel/Channel.svelte

@@ -246,7 +246,7 @@
 				{/if}
 			</div>
 
-			<div class=" pb-[1rem]">
+			<div class=" pb-[1rem] px-2.5">
 				<MessageInput
 					id="root"
 					{typingUsers}

+ 105 - 67
src/lib/components/channel/MessageInput.svelte

@@ -31,17 +31,22 @@
 	let content = '';
 	let files = [];
 
-	let chatInputElement;
+	export let chatInputElement;
 	let filesInputElement;
 	let inputFiles;
 
 	export let typingUsers = [];
+	export let inputLoading = false;
+
+	export let onSubmit: Function = (e) => {};
+	export let onChange: Function = (e) => {};
+	export let onStop: Function = (e) => {};
 
-	export let onSubmit: Function;
-	export let onChange: Function;
 	export let scrollEnd = true;
 	export let scrollToBottom: Function = () => {};
 
+	export let acceptFiles = true;
+
 	const screenCaptureHandler = async () => {
 		try {
 			// Request screen media
@@ -260,7 +265,7 @@
 	const onDrop = async (e) => {
 		e.preventDefault();
 
-		if (e.dataTransfer?.files) {
+		if (e.dataTransfer?.files && acceptFiles) {
 			const inputFiles = Array.from(e.dataTransfer?.files);
 			if (inputFiles && inputFiles.length > 0) {
 				console.log(inputFiles);
@@ -330,27 +335,30 @@
 
 <FilesOverlay show={draggedOver} />
 
-<input
-	bind:this={filesInputElement}
-	bind:files={inputFiles}
-	type="file"
-	hidden
-	multiple
-	on:change={async () => {
-		if (inputFiles && inputFiles.length > 0) {
-			inputFilesHandler(Array.from(inputFiles));
-		} else {
-			toast.error($i18n.t(`File not found.`));
-		}
+{#if acceptFiles}
+	<input
+		bind:this={filesInputElement}
+		bind:files={inputFiles}
+		type="file"
+		hidden
+		multiple
+		on:change={async () => {
+			if (inputFiles && inputFiles.length > 0) {
+				inputFilesHandler(Array.from(inputFiles));
+			} else {
+				toast.error($i18n.t(`File not found.`));
+			}
+
+			filesInputElement.value = '';
+		}}
+	/>
+{/if}
 
-		filesInputElement.value = '';
-	}}
-/>
 <div class="bg-transparent">
 	<div
 		class="{($settings?.widescreenMode ?? null)
 			? 'max-w-full'
-			: 'max-w-6xl'} px-2.5 mx-auto inset-x-0 relative"
+			: 'max-w-6xl'}  mx-auto inset-x-0 relative"
 	>
 		<div class="absolute top-0 left-0 right-0 mx-auto inset-x-0 bg-transparent flex justify-center">
 			<div class="flex flex-col px-3 w-full">
@@ -492,7 +500,7 @@
 
 						<div class="px-2.5">
 							<div
-								class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
+								class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none h-fit max-h-80 overflow-auto"
 							>
 								<RichTextInput
 									bind:this={chatInputElement}
@@ -547,29 +555,33 @@
 
 						<div class=" flex justify-between mb-2.5 mt-1.5 mx-0.5">
 							<div class="ml-1 self-end flex space-x-1">
-								<InputMenu
-									{screenCaptureHandler}
-									uploadFilesHandler={() => {
-										filesInputElement.click();
-									}}
-								>
-									<button
-										class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-hidden focus:outline-hidden"
-										type="button"
-										aria-label="More"
-									>
-										<svg
-											xmlns="http://www.w3.org/2000/svg"
-											viewBox="0 0 20 20"
-											fill="currentColor"
-											class="size-5"
+								<slot name="menu">
+									{#if acceptFiles}
+										<InputMenu
+											{screenCaptureHandler}
+											uploadFilesHandler={() => {
+												filesInputElement.click();
+											}}
 										>
-											<path
-												d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
-											/>
-										</svg>
-									</button>
-								</InputMenu>
+											<button
+												class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-hidden focus:outline-hidden"
+												type="button"
+												aria-label="More"
+											>
+												<svg
+													xmlns="http://www.w3.org/2000/svg"
+													viewBox="0 0 20 20"
+													fill="currentColor"
+													class="size-5"
+												>
+													<path
+														d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
+													/>
+												</svg>
+											</button>
+										</InputMenu>
+									{/if}
+								</slot>
 							</div>
 
 							<div class="self-end flex space-x-1 mr-1">
@@ -620,31 +632,57 @@
 								{/if}
 
 								<div class=" flex items-center">
-									<div class=" flex items-center">
-										<Tooltip content={$i18n.t('Send message')}>
-											<button
-												id="send-message-button"
-												class="{content !== '' || files.length !== 0
-													? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
-													: 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
-												type="submit"
-												disabled={content === '' && files.length === 0}
-											>
-												<svg
-													xmlns="http://www.w3.org/2000/svg"
-													viewBox="0 0 16 16"
-													fill="currentColor"
-													class="size-5"
+									{#if inputLoading && onStop}
+										<div class=" flex items-center">
+											<Tooltip content={$i18n.t('Stop')}>
+												<button
+													class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
+													on:click={() => {
+														onStop();
+													}}
 												>
-													<path
-														fill-rule="evenodd"
-														d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
-														clip-rule="evenodd"
-													/>
-												</svg>
-											</button>
-										</Tooltip>
-									</div>
+													<svg
+														xmlns="http://www.w3.org/2000/svg"
+														viewBox="0 0 24 24"
+														fill="currentColor"
+														class="size-5"
+													>
+														<path
+															fill-rule="evenodd"
+															d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm6-2.438c0-.724.588-1.312 1.313-1.312h4.874c.725 0 1.313.588 1.313 1.313v4.874c0 .725-.588 1.313-1.313 1.313H9.564a1.312 1.312 0 01-1.313-1.313V9.564z"
+															clip-rule="evenodd"
+														/>
+													</svg>
+												</button>
+											</Tooltip>
+										</div>
+									{:else}
+										<div class=" flex items-center">
+											<Tooltip content={$i18n.t('Send message')}>
+												<button
+													id="send-message-button"
+													class="{content !== '' || files.length !== 0
+														? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
+														: 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
+													type="submit"
+													disabled={content === '' && files.length === 0}
+												>
+													<svg
+														xmlns="http://www.w3.org/2000/svg"
+														viewBox="0 0 16 16"
+														fill="currentColor"
+														class="size-5"
+													>
+														<path
+															fill-rule="evenodd"
+															d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
+															clip-rule="evenodd"
+														/>
+													</svg>
+												</button>
+											</Tooltip>
+										</div>
+									{/if}
 								</div>
 							</div>
 						</div>

+ 1 - 1
src/lib/components/channel/Thread.svelte

@@ -196,7 +196,7 @@
 				}}
 			/>
 
-			<div class=" pb-[1rem]">
+			<div class=" pb-[1rem] px-2.5">
 				<MessageInput id={threadId} {typingUsers} {onChange} onSubmit={submitHandler} />
 			</div>
 		</div>

+ 50 - 42
src/lib/components/notes/NoteEditor.svelte

@@ -99,6 +99,8 @@
 	let displayMediaRecord = false;
 
 	let showPanel = false;
+	let selectedPanel = 'chat';
+
 	let showDeleteConfirm = false;
 
 	let dragged = false;
@@ -677,6 +679,9 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
 	import { PaneGroup, Pane, PaneResizer } from 'paneforge';
 	import XMark from '../icons/XMark.svelte';
 	import MenuLines from '../icons/MenuLines.svelte';
+	import ChatBubbleOval from '../icons/ChatBubbleOval.svelte';
+	import Settings from './NoteEditor/Settings.svelte';
+	import Chat from './NoteEditor/Chat.svelte';
 </script>
 
 <FilesOverlay show={dragged} />
@@ -709,7 +714,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
 						<div class="w-full flex items-center">
 							<div
 								class="{$showSidebar
-									? 'md:hidden'
+									? 'md:hidden pl-0.5'
 									: ''} flex flex-none items-center pr-1 -translate-x-1"
 							>
 								<button
@@ -734,7 +739,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
 								required
 							/>
 
-							<div class="flex items-center gap-2 translate-x-1">
+							<div class="flex items-center gap-0.5 translate-x-1">
 								{#if note.data?.versions?.length > 0}
 									<div>
 										<div class="flex items-center gap-0.5 self-center min-w-fit" dir="ltr">
@@ -780,17 +785,46 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
 										showDeleteConfirm = true;
 									}}
 								>
-									<EllipsisHorizontal className="size-5" />
+									<div class="p-1 bg-transparent hover:bg-white/5 transition rounded-lg">
+										<EllipsisHorizontal className="size-5" />
+									</div>
 								</NoteMenu>
 
-								<button
-									class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg"
-									on:click={() => {
-										showPanel = !showPanel;
-									}}
-								>
-									<Cog6 />
-								</button>
+								<Tooltip placement="top" content={$i18n.t('Chat')} className="cursor-pointer">
+									<button
+										class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg"
+										on:click={() => {
+											if (showPanel && selectedPanel === 'chat') {
+												showPanel = false;
+											} else {
+												if (!showPanel) {
+													showPanel = true;
+												}
+												selectedPanel = 'chat';
+											}
+										}}
+									>
+										<ChatBubbleOval />
+									</button>
+								</Tooltip>
+
+								<Tooltip placement="top" content={$i18n.t('Settings')} className="cursor-pointer">
+									<button
+										class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg"
+										on:click={() => {
+											if (showPanel && selectedPanel === 'settings') {
+												showPanel = false;
+											} else {
+												if (!showPanel) {
+													showPanel = true;
+												}
+												selectedPanel = 'settings';
+											}
+										}}
+									>
+										<Cog6 />
+									</button>
+								</Tooltip>
 							</div>
 						</div>
 					</div>
@@ -998,36 +1032,10 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
 		</div>
 	</Pane>
 	<NotePanel bind:show={showPanel}>
-		<div class="flex items-center mb-2">
-			<div class=" -translate-x-1.5">
-				<button
-					class="p-1.5 bg-transparent transition rounded-lg"
-					on:click={() => {
-						showPanel = !showPanel;
-					}}
-				>
-					<XMark className="size-5" strokeWidth="2.5" />
-				</button>
-			</div>
-
-			<div class=" font-medium text-base">Settings</div>
-		</div>
-
-		<div class="mt-1">
-			<div>
-				<div class=" text-xs font-medium mb-1">Model</div>
-
-				<div class="w-full">
-					<select class="w-full bg-transparent text-sm outline-hidden" bind:value={selectedModelId}>
-						<option value="" class="bg-gray-50 dark:bg-gray-700" disabled>
-							{$i18n.t('Select a model')}
-						</option>
-						{#each $models.filter((model) => !(model?.info?.meta?.hidden ?? false)) as model}
-							<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
-						{/each}
-					</select>
-				</div>
-			</div>
-		</div>
+		{#if selectedPanel === 'chat'}
+			<Chat bind:show={showPanel} bind:selectedModelId />
+		{:else if selectedPanel === 'settings'}
+			<Settings bind:show={showPanel} bind:selectedModelId />
+		{/if}
 	</NotePanel>
 </PaneGroup>

+ 257 - 0
src/lib/components/notes/NoteEditor/Chat.svelte

@@ -0,0 +1,257 @@
+<script lang="ts">
+	export let show = false;
+	export let selectedModelId = '';
+
+	import { toast } from 'svelte-sonner';
+
+	import { goto } from '$app/navigation';
+	import { onMount, tick, getContext } from 'svelte';
+
+	import {
+		OLLAMA_API_BASE_URL,
+		OPENAI_API_BASE_URL,
+		WEBUI_API_BASE_URL,
+		WEBUI_BASE_URL
+	} from '$lib/constants';
+	import { WEBUI_NAME, config, user, models, settings } from '$lib/stores';
+
+	import { chatCompletion, generateOpenAIChatCompletion } from '$lib/apis/openai';
+
+	import { splitStream } from '$lib/utils';
+
+	import Messages from '$lib/components/notes/NoteEditor/Chat/Messages.svelte';
+	import MessageInput from '$lib/components/channel/MessageInput.svelte';
+	import XMark from '$lib/components/icons/XMark.svelte';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+
+	const i18n = getContext('i18n');
+
+	let loaded = false;
+
+	let loading = false;
+	let stopResponseFlag = false;
+
+	let systemTextareaElement: HTMLTextAreaElement;
+	let messagesContainerElement: HTMLDivElement;
+
+	let system = '';
+	let content = '';
+
+	let messages = [];
+	let chatInputElement = null;
+
+	const scrollToBottom = () => {
+		const element = messagesContainerElement;
+
+		if (element) {
+			element.scrollTop = element?.scrollHeight;
+		}
+	};
+
+	const stopHandler = () => {
+		stopResponseFlag = true;
+		console.log('stopResponse');
+	};
+
+	const chatCompletionHandler = async () => {
+		if (selectedModelId === '') {
+			toast.error($i18n.t('Please select a model.'));
+			return;
+		}
+
+		const model = $models.find((model) => model.id === selectedModelId);
+		if (!model) {
+			selectedModelId = '';
+			return;
+		}
+
+		const [res, controller] = await chatCompletion(
+			localStorage.token,
+			{
+				model: model.id,
+				stream: true,
+				messages: [
+					system
+						? {
+								role: 'system',
+								content: system
+							}
+						: undefined,
+					...messages
+				].filter((message) => message)
+			},
+			`${WEBUI_BASE_URL}/api`
+		);
+
+		let responseMessage;
+		if (messages.at(-1)?.role === 'assistant') {
+			responseMessage = messages.at(-1);
+		} else {
+			responseMessage = {
+				role: 'assistant',
+				content: ''
+			};
+			messages.push(responseMessage);
+			messages = messages;
+		}
+
+		await tick();
+		const textareaElement = document.getElementById(`assistant-${messages.length - 1}-textarea`);
+
+		if (res && res.ok) {
+			const reader = res.body
+				.pipeThrough(new TextDecoderStream())
+				.pipeThrough(splitStream('\n'))
+				.getReader();
+
+			while (true) {
+				const { value, done } = await reader.read();
+				if (done || stopResponseFlag) {
+					if (stopResponseFlag) {
+						controller.abort('User: Stop Response');
+					}
+					break;
+				}
+
+				try {
+					let lines = value.split('\n');
+
+					for (const line of lines) {
+						if (line !== '') {
+							console.log(line);
+							if (line === 'data: [DONE]') {
+								// responseMessage.done = true;
+								messages = messages;
+							} else {
+								let data = JSON.parse(line.replace(/^data: /, ''));
+								console.log(data);
+
+								if (responseMessage.content == '' && data.choices[0].delta.content == '\n') {
+									continue;
+								} else {
+									textareaElement.style.height = textareaElement.scrollHeight + 'px';
+
+									responseMessage.content += data.choices[0].delta.content ?? '';
+									messages = messages;
+
+									textareaElement.style.height = textareaElement.scrollHeight + 'px';
+
+									await tick();
+								}
+							}
+						}
+					}
+				} catch (error) {
+					console.log(error);
+				}
+
+				scrollToBottom();
+			}
+		}
+	};
+
+	const submitHandler = async (e) => {
+		const { content, data } = e;
+		if (selectedModelId && content) {
+			messages.push({
+				role: 'user',
+				content: content
+			});
+			messages = messages;
+
+			await tick();
+			scrollToBottom();
+
+			loading = true;
+			await chatCompletionHandler();
+
+			loading = false;
+			stopResponseFlag = false;
+		}
+	};
+
+	onMount(async () => {
+		if ($user?.role !== 'admin') {
+			await goto('/');
+		}
+
+		if ($settings?.models) {
+			selectedModelId = $settings?.models[0];
+		} else if ($config?.default_models) {
+			selectedModelId = $config?.default_models.split(',')[0];
+		} else {
+			selectedModelId = '';
+		}
+		loaded = true;
+	});
+</script>
+
+<div class="flex items-center mb-2">
+	<div class=" -translate-x-1.5">
+		<button
+			class="p-1.5 bg-transparent transition rounded-lg"
+			on:click={() => {
+				show = !show;
+			}}
+		>
+			<XMark className="size-5" strokeWidth="2.5" />
+		</button>
+	</div>
+
+	<div class=" font-medium text-base flex items-center gap-1">
+		<div>
+			{$i18n.t('Chat')}
+		</div>
+
+		<div>
+			<Tooltip
+				content={$i18n.t('This is an experimental feature, it may not work as expected.')}
+				position="top"
+				className="inline-block"
+			>
+				<span class="text-gray-500 text-sm">({$i18n.t('Experimental')})</span>
+			</Tooltip>
+		</div>
+	</div>
+</div>
+
+<div class="flex flex-col items-center mb-2 flex-1">
+	<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
+		<div class="mx-auto w-full md:px-0 h-full relative">
+			<div class=" flex flex-col h-full">
+				<div
+					class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
+					id="messages-container"
+					bind:this={messagesContainerElement}
+				>
+					<div class=" h-full w-full flex flex-col">
+						<div class="flex-1 p-1">
+							<Messages bind:messages />
+						</div>
+					</div>
+				</div>
+
+				<div class=" pb-2">
+					<MessageInput
+						bind:chatInputElement
+						acceptFiles={false}
+						inputLoading={loading}
+						onSubmit={submitHandler}
+						onStop={stopHandler}
+					>
+						<div slot="menu">
+							<select
+								class=" bg-transparent rounded-lg py-1 px-2 -mx-0.5 text-sm outline-hidden w-50"
+								bind:value={selectedModelId}
+							>
+								{#each $models as model}
+									<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
+								{/each}
+							</select>
+						</div>
+					</MessageInput>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>

+ 52 - 0
src/lib/components/notes/NoteEditor/Chat/Message.svelte

@@ -0,0 +1,52 @@
+<script lang="ts">
+	import { onMount, getContext } from 'svelte';
+
+	const i18n = getContext('i18n');
+
+	export let message;
+	export let idx;
+
+	export let onDelete;
+
+	let textAreaElement: HTMLTextAreaElement;
+
+	onMount(() => {
+		textAreaElement.style.height = '';
+		textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
+	});
+</script>
+
+<div class="flex flex-col gap-1 group">
+	<div class="flex items-start pt-1">
+		<div
+			class="px-2 py-1 text-sm font-semibold uppercase min-w-[6rem] text-left rounded-lg transition"
+		>
+			{$i18n.t(message.role)}
+		</div>
+	</div>
+
+	<div class="flex-1">
+		<!-- $i18n.t('a user') -->
+		<!-- $i18n.t('an assistant') -->
+		<textarea
+			id="{message.role}-{idx}-textarea"
+			bind:this={textAreaElement}
+			class="w-full bg-transparent outline-hidden rounded-lg px-2 text-sm resize-none overflow-hidden"
+			placeholder={$i18n.t(`Enter {{role}} message here`, {
+				role: message.role === 'user' ? $i18n.t('a user') : $i18n.t('an assistant')
+			})}
+			rows="1"
+			on:input={(e) => {
+				textAreaElement.style.height = '';
+				textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
+			}}
+			on:focus={(e) => {
+				textAreaElement.style.height = '';
+				textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
+
+				// e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
+			}}
+			bind:value={message.content}
+		/>
+	</div>
+</div>

+ 20 - 0
src/lib/components/notes/NoteEditor/Chat/Messages.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import { onMount, getContext } from 'svelte';
+	import Message from './Message.svelte';
+
+	const i18n = getContext('i18n');
+
+	export let messages = [];
+</script>
+
+<div class="space-y-3">
+	{#each messages as message, idx}
+		<Message
+			{message}
+			{idx}
+			onDelete={() => {
+				messages = messages.filter((message, messageIdx) => messageIdx !== idx);
+			}}
+		/>
+	{/each}
+</div>

+ 42 - 0
src/lib/components/notes/NoteEditor/Settings.svelte

@@ -0,0 +1,42 @@
+<script lang="ts">
+	import { getContext } from 'svelte';
+	const i18n = getContext('i18n');
+
+	import XMark from '$lib/components/icons/XMark.svelte';
+	import { models } from '$lib/stores';
+
+	export let show = false;
+	export let selectedModelId = '';
+</script>
+
+<div class="flex items-center mb-2">
+	<div class=" -translate-x-1.5">
+		<button
+			class="p-1.5 bg-transparent transition rounded-lg"
+			on:click={() => {
+				show = !show;
+			}}
+		>
+			<XMark className="size-5" strokeWidth="2.5" />
+		</button>
+	</div>
+
+	<div class=" font-medium text-base">Settings</div>
+</div>
+
+<div class="mt-1">
+	<div>
+		<div class=" text-xs font-medium mb-1">Model</div>
+
+		<div class="w-full">
+			<select class="w-full bg-transparent text-sm outline-hidden" bind:value={selectedModelId}>
+				<option value="" class="bg-gray-50 dark:bg-gray-700" disabled>
+					{$i18n.t('Select a model')}
+				</option>
+				{#each $models.filter((model) => !(model?.info?.meta?.hidden ?? false)) as model}
+					<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
+				{/each}
+			</select>
+		</div>
+	</div>
+</div>

+ 3 - 3
src/lib/components/notes/NotePanel.svelte

@@ -61,8 +61,8 @@
 
 	<Pane
 		bind:pane
-		defaultSize={30}
-		minSize={30}
+		defaultSize={35}
+		minSize={35}
 		onCollapse={() => {
 			show = false;
 		}}
@@ -72,7 +72,7 @@
 		{#if show}
 			<div class="flex max-h-full min-h-full">
 				<div
-					class="w-full pl-1.5 pr-2.5 pt-2 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-100 dark:border-gray-850 z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
+					class="w-full pl-1.5 pr-2.5 pt-2 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-100 dark:border-gray-850 z-40 pointer-events-auto overflow-y-auto scrollbar-hidden flex flex-col"
 				>
 					<slot />
 				</div>

+ 14 - 51
src/lib/components/playground/Chat.svelte

@@ -211,42 +211,6 @@
 
 <div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
 	<div class="mx-auto w-full md:px-0 h-full relative">
-		<Sidebar bind:show={showSettings} className=" bg-white dark:bg-gray-900" width="300px">
-			<div class="flex flex-col px-5 py-3 text-sm">
-				<div class="flex justify-between items-center mb-2">
-					<div class=" font-medium text-base">Settings</div>
-
-					<div class=" translate-x-1.5">
-						<button
-							class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg"
-							on:click={() => {
-								showSettings = !showSettings;
-							}}
-						>
-							<ArrowRight className="size-3" strokeWidth="2.5" />
-						</button>
-					</div>
-				</div>
-
-				<div class="mt-1">
-					<div>
-						<div class=" text-xs font-medium mb-1">Model</div>
-
-						<div class="w-full">
-							<select
-								class="w-full bg-transparent border border-gray-100 dark:border-gray-850 rounded-lg py-1 px-2 -mx-0.5 text-sm outline-hidden"
-								bind:value={selectedModelId}
-							>
-								{#each $models as model}
-									<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
-								{/each}
-							</select>
-						</div>
-					</div>
-				</div>
-			</div>
-		</Sidebar>
-
 		<div class=" flex flex-col h-full px-3.5">
 			<div class="flex w-full items-start gap-1.5">
 				<Collapsible
@@ -292,17 +256,6 @@
 						</div>
 					</div>
 				</Collapsible>
-
-				<div class="translate-y-1">
-					<button
-						class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg"
-						on:click={() => {
-							showSettings = !showSettings;
-						}}
-					>
-						<Cog6 />
-					</button>
-				</div>
 			</div>
 
 			<div
@@ -318,9 +271,6 @@
 			</div>
 
 			<div class="pb-3">
-				<div class="text-xs font-medium text-gray-500 px-2 py-1">
-					{selectedModelId}
-				</div>
 				<div class="border border-gray-100 dark:border-gray-850 w-full px-3 py-2.5 rounded-xl">
 					<div class="py-0.5">
 						<!-- $i18n.t('a user') -->
@@ -359,7 +309,20 @@
 							</button>
 						</div>
 
-						<div>
+						<div class="flex items-center gap-2">
+							<div class="">
+								<select
+									class=" bg-transparent border border-gray-100 dark:border-gray-850 rounded-lg py-1 px-2 -mx-0.5 text-sm outline-hidden w-40"
+									bind:value={selectedModelId}
+								>
+									{#each $models as model}
+										<option value={model.id} class="bg-gray-50 dark:bg-gray-700"
+											>{model.name}</option
+										>
+									{/each}
+								</select>
+							</div>
+
 							{#if !loading}
 								<button
 									disabled={message === ''}