Browse Source

refac: styling

Timothy Jaeryang Baek 4 tháng trước cách đây
mục cha
commit
5e91c2c1fe

+ 264 - 0
src/lib/components/chat/ModelSelector/ModelItem.svelte

@@ -0,0 +1,264 @@
+<script lang="ts">
+	import { marked } from 'marked';
+
+	import { getContext, tick } from 'svelte';
+	import dayjs from '$lib/dayjs';
+
+	import { mobile, pinnedModels, user } from '$lib/stores';
+
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import { copyToClipboard, sanitizeResponseContent } from '$lib/utils';
+	import ArrowUpTray from '$lib/components/icons/ArrowUpTray.svelte';
+	import Check from '$lib/components/icons/Check.svelte';
+	import ModelItemMenu from './ModelItemMenu.svelte';
+	import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
+	import { toast } from 'svelte-sonner';
+
+	const i18n = getContext('i18n');
+
+	export let selectedModelIdx: number = -1;
+	export let item: any = {};
+	export let index: number = -1;
+	export let value: string = '';
+
+	export let unloadModelHandler: (modelValue: string) => void = () => {};
+	export let onClick: () => void = () => {};
+
+	const copyLinkHandler = async (model) => {
+		const baseUrl = window.location.origin;
+		const res = await copyToClipboard(`${baseUrl}/?model=${encodeURIComponent(model.id)}`);
+
+		if (res) {
+			toast.success($i18n.t('Copied link to clipboard'));
+		} else {
+			toast.error($i18n.t('Failed to copy link'));
+		}
+	};
+
+	let showMenu = false;
+</script>
+
+<button
+	aria-label="model-item"
+	class="flex group/item w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-highlighted:bg-muted {index ===
+	selectedModelIdx
+		? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent'
+		: ''}"
+	data-arrow-selected={index === selectedModelIdx}
+	data-value={item.value}
+	on:click={() => {
+		onClick();
+	}}
+>
+	<div class="flex flex-col">
+		{#if $mobile && (item?.model?.tags ?? []).length > 0}
+			<div class="flex gap-0.5 self-start h-full mb-1.5 -translate-x-1">
+				{#each item.model?.tags.sort((a, b) => a.name.localeCompare(b.name)) as tag}
+					<div
+						class=" text-xs font-bold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
+					>
+						{tag.name}
+					</div>
+				{/each}
+			</div>
+		{/if}
+		<div class="flex items-center gap-2">
+			<div class="flex items-center min-w-fit">
+				<div class="line-clamp-1">
+					<div class="flex items-center min-w-fit">
+						<Tooltip
+							content={$user?.role === 'admin' ? (item?.value ?? '') : ''}
+							placement="top-start"
+						>
+							<img
+								src={item.model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
+								alt="Model"
+								class="rounded-full size-5 flex items-center mr-2"
+							/>
+
+							<div class="flex items-center line-clamp-1">
+								<div class="line-clamp-1">
+									{item.label}
+								</div>
+							</div>
+						</Tooltip>
+					</div>
+				</div>
+			</div>
+
+			{#if item.model.owned_by === 'ollama'}
+				{#if (item.model.ollama?.details?.parameter_size ?? '') !== ''}
+					<div class="flex items-center translate-y-[0.5px]">
+						<Tooltip
+							content={`${
+								item.model.ollama?.details?.quantization_level
+									? item.model.ollama?.details?.quantization_level + ' '
+									: ''
+							}${
+								item.model.ollama?.size
+									? `(${(item.model.ollama?.size / 1024 ** 3).toFixed(1)}GB)`
+									: ''
+							}`}
+							className="self-end"
+						>
+							<span class=" text-xs font-medium text-gray-600 dark:text-gray-400 line-clamp-1"
+								>{item.model.ollama?.details?.parameter_size ?? ''}</span
+							>
+						</Tooltip>
+					</div>
+				{/if}
+				{#if item.model.ollama?.expires_at && new Date(item.model.ollama?.expires_at * 1000) > new Date()}
+					<div class="flex items-center translate-y-[0.5px] px-0.5">
+						<Tooltip
+							content={`${$i18n.t('Unloads {{FROM_NOW}}', {
+								FROM_NOW: dayjs(item.model.ollama?.expires_at * 1000).fromNow()
+							})}`}
+							className="self-end"
+						>
+							<div class=" flex items-center">
+								<span class="relative flex size-2">
+									<span
+										class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
+									/>
+									<span class="relative inline-flex rounded-full size-2 bg-green-500" />
+								</span>
+							</div>
+						</Tooltip>
+					</div>
+				{/if}
+			{/if}
+
+			<!-- {JSON.stringify(item.info)} -->
+
+			{#if item.model?.direct}
+				<Tooltip content={`${$i18n.t('Direct')}`}>
+					<div class="translate-y-[1px]">
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							viewBox="0 0 16 16"
+							fill="currentColor"
+							class="size-3"
+						>
+							<path
+								fill-rule="evenodd"
+								d="M2 2.75A.75.75 0 0 1 2.75 2C8.963 2 14 7.037 14 13.25a.75.75 0 0 1-1.5 0c0-5.385-4.365-9.75-9.75-9.75A.75.75 0 0 1 2 2.75Zm0 4.5a.75.75 0 0 1 .75-.75 6.75 6.75 0 0 1 6.75 6.75.75.75 0 0 1-1.5 0C8 10.35 5.65 8 2.75 8A.75.75 0 0 1 2 7.25ZM3.5 11a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
+								clip-rule="evenodd"
+							/>
+						</svg>
+					</div>
+				</Tooltip>
+			{:else if item.model.connection_type === 'external'}
+				<Tooltip content={`${$i18n.t('External')}`}>
+					<div class="translate-y-[1px]">
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							viewBox="0 0 16 16"
+							fill="currentColor"
+							class="size-3"
+						>
+							<path
+								fill-rule="evenodd"
+								d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z"
+								clip-rule="evenodd"
+							/>
+							<path
+								fill-rule="evenodd"
+								d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z"
+								clip-rule="evenodd"
+							/>
+						</svg>
+					</div>
+				</Tooltip>
+			{/if}
+
+			{#if item.model?.info?.meta?.description}
+				<Tooltip
+					content={`${marked.parse(
+						sanitizeResponseContent(item.model?.info?.meta?.description).replaceAll('\n', '<br>')
+					)}`}
+				>
+					<div class=" translate-y-[1px]">
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							fill="none"
+							viewBox="0 0 24 24"
+							stroke-width="1.5"
+							stroke="currentColor"
+							class="w-4 h-4"
+						>
+							<path
+								stroke-linecap="round"
+								stroke-linejoin="round"
+								d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
+							/>
+						</svg>
+					</div>
+				</Tooltip>
+			{/if}
+
+			{#if !$mobile && (item?.model?.tags ?? []).length > 0}
+				<div
+					class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px] overflow-x-auto scrollbar-none"
+				>
+					{#each item.model?.tags.sort((a, b) => a.name.localeCompare(b.name)) as tag}
+						<Tooltip content={tag.name} className="flex-shrink-0">
+							<div
+								class=" text-xs font-bold px-1 rounded-sm uppercase bg-gray-500/20 text-gray-700 dark:text-gray-200"
+							>
+								{tag.name}
+							</div>
+						</Tooltip>
+					{/each}
+				</div>
+			{/if}
+		</div>
+	</div>
+
+	<div class="ml-auto pl-2 pr-1 flex items-center gap-1.5">
+		{#if $user?.role === 'admin' && item.model.owned_by === 'ollama' && item.model.ollama?.expires_at && new Date(item.model.ollama?.expires_at * 1000) > new Date()}
+			<Tooltip
+				content={`${$i18n.t('Eject')}`}
+				className="flex-shrink-0 group-hover/item:opacity-100 opacity-0 "
+			>
+				<button
+					class="flex"
+					on:click={(e) => {
+						e.preventDefault();
+						e.stopPropagation();
+						unloadModelHandler(item.value);
+					}}
+				>
+					<ArrowUpTray className="size-3" />
+				</button>
+			</Tooltip>
+		{/if}
+
+		<ModelItemMenu
+			bind:show={showMenu}
+			model={item.model}
+			toggleSidebarHandler={() => {
+				pinnedModels.set([...new Set([...$pinnedModels, item.model.id])]);
+			}}
+			copyLinkHandler={() => {
+				copyLinkHandler(item.model);
+			}}
+		>
+			<button
+				class="flex items-center"
+				on:click={(e) => {
+					e.preventDefault();
+					e.stopPropagation();
+					showMenu = !showMenu;
+				}}
+			>
+				<EllipsisHorizontal />
+			</button>
+		</ModelItemMenu>
+
+		{#if value === item.value}
+			<div>
+				<Check className="size-3" />
+			</div>
+		{/if}
+	</div>
+</button>

+ 79 - 0
src/lib/components/chat/ModelSelector/ModelItemMenu.svelte

@@ -0,0 +1,79 @@
+<script lang="ts">
+	import { DropdownMenu } from 'bits-ui';
+	import { flyAndScale } from '$lib/utils/transitions';
+
+	import { getContext } from 'svelte';
+
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import Link from '$lib/components/icons/Link.svelte';
+	import Eye from '$lib/components/icons/Eye.svelte';
+	import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
+	import { pinnedModels } from '$lib/stores';
+
+	const i18n = getContext('i18n');
+
+	export let show = false;
+	export let model;
+
+	export let toggleSidebarHandler: Function = () => {};
+	export let copyLinkHandler: Function = () => {};
+
+	export let onClose: Function = () => {};
+</script>
+
+<DropdownMenu.Root
+	bind:open={show}
+	closeFocus={false}
+	onOpenChange={(state) => {
+		if (state === false) {
+			onClose();
+		}
+	}}
+	typeahead={false}
+>
+	<DropdownMenu.Trigger>
+		<Tooltip content={$i18n.t('More')} className=" group-hover/item:opacity-100  opacity-0">
+			<slot />
+		</Tooltip>
+	</DropdownMenu.Trigger>
+
+	<DropdownMenu.Content
+		class="w-full max-w-[180px] text-sm rounded-xl px-1 py-1.5 z-[9999999] bg-white dark:bg-gray-850 dark:text-white shadow-lg"
+		sideOffset={-2}
+		side="bottom"
+		align="start"
+		transition={flyAndScale}
+	>
+		<DropdownMenu.Item
+			class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2"
+			on:click={() => {
+				toggleSidebarHandler();
+			}}
+		>
+			{#if ($pinnedModels ?? []).includes(model?.id)}
+				<EyeSlash />
+			{:else}
+				<Eye />
+			{/if}
+
+			<div class="flex items-center">
+				{#if ($pinnedModels ?? []).includes(model?.id)}
+					{$i18n.t('Hide from Sidebar')}
+				{:else}
+					{$i18n.t('Keep in Sidebar')}
+				{/if}
+			</div>
+		</DropdownMenu.Item>
+
+		<DropdownMenu.Item
+			class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2"
+			on:click={() => {
+				copyLinkHandler();
+			}}
+		>
+			<Link />
+
+			<div class="flex items-center">{$i18n.t('Copy Link')}</div>
+		</DropdownMenu.Item>
+	</DropdownMenu.Content>
+</DropdownMenu.Root>

+ 18 - 208
src/lib/components/chat/ModelSelector/Selector.svelte

@@ -3,12 +3,13 @@
 	import { marked } from 'marked';
 	import Fuse from 'fuse.js';
 
+	import dayjs from '$lib/dayjs';
+	import relativeTime from 'dayjs/plugin/relativeTime';
+	dayjs.extend(relativeTime);
+
 	import { flyAndScale } from '$lib/utils/transitions';
 	import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
-
-	import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
-	import Check from '$lib/components/icons/Check.svelte';
-	import Search from '$lib/components/icons/Search.svelte';
+	import { goto } from '$app/navigation';
 
 	import { deleteModel, getOllamaVersion, pullModel, unloadModel } from '$lib/apis/ollama';
 
@@ -25,14 +26,14 @@
 	import { capitalizeFirstLetter, sanitizeResponseContent, splitStream } from '$lib/utils';
 	import { getModels } from '$lib/apis';
 
+	import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
+	import Check from '$lib/components/icons/Check.svelte';
+	import Search from '$lib/components/icons/Search.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
 	import ChatBubbleOval from '$lib/components/icons/ChatBubbleOval.svelte';
-	import { goto } from '$app/navigation';
-	import dayjs from '$lib/dayjs';
-	import relativeTime from 'dayjs/plugin/relativeTime';
-	import ArrowUpTray from '$lib/components/icons/ArrowUpTray.svelte';
-	dayjs.extend(relativeTime);
+
+	import ModelItem from './ModelItem.svelte';
 
 	const i18n = getContext('i18n');
 	const dispatch = createEventDispatcher();
@@ -494,210 +495,19 @@
 				{/if}
 
 				{#each filteredItems as item, index}
-					<button
-						aria-label="model-item"
-						class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-highlighted:bg-muted {index ===
-						selectedModelIdx
-							? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent'
-							: ''}"
-						data-arrow-selected={index === selectedModelIdx}
-						data-value={item.value}
-						on:click={() => {
+					<ModelItem
+						{selectedModelIdx}
+						{item}
+						{index}
+						{value}
+						{unloadModelHandler}
+						onClick={() => {
 							value = item.value;
 							selectedModelIdx = index;
 
 							show = false;
 						}}
-					>
-						<div class="flex flex-col">
-							{#if $mobile && (item?.model?.tags ?? []).length > 0}
-								<div class="flex gap-0.5 self-start h-full mb-1.5 -translate-x-1">
-									{#each item.model?.tags.sort((a, b) => a.name.localeCompare(b.name)) as tag}
-										<div
-											class=" text-xs font-bold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
-										>
-											{tag.name}
-										</div>
-									{/each}
-								</div>
-							{/if}
-							<div class="flex items-center gap-2">
-								<div class="flex items-center min-w-fit">
-									<div class="line-clamp-1">
-										<div class="flex items-center min-w-fit">
-											<Tooltip
-												content={$user?.role === 'admin' ? (item?.value ?? '') : ''}
-												placement="top-start"
-											>
-												<img
-													src={item.model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
-													alt="Model"
-													class="rounded-full size-5 flex items-center mr-2"
-												/>
-
-												<div class="flex items-center line-clamp-1">
-													<div class="line-clamp-1">
-														{item.label}
-													</div>
-												</div>
-											</Tooltip>
-										</div>
-									</div>
-								</div>
-
-								{#if item.model.owned_by === 'ollama'}
-									{#if (item.model.ollama?.details?.parameter_size ?? '') !== ''}
-										<div class="flex items-center translate-y-[0.5px]">
-											<Tooltip
-												content={`${
-													item.model.ollama?.details?.quantization_level
-														? item.model.ollama?.details?.quantization_level + ' '
-														: ''
-												}${
-													item.model.ollama?.size
-														? `(${(item.model.ollama?.size / 1024 ** 3).toFixed(1)}GB)`
-														: ''
-												}`}
-												className="self-end"
-											>
-												<span
-													class=" text-xs font-medium text-gray-600 dark:text-gray-400 line-clamp-1"
-													>{item.model.ollama?.details?.parameter_size ?? ''}</span
-												>
-											</Tooltip>
-										</div>
-									{/if}
-									{#if item.model.ollama?.expires_at && new Date(item.model.ollama?.expires_at * 1000) > new Date()}
-										<div class="flex items-center translate-y-[0.5px] px-0.5">
-											<Tooltip
-												content={`${$i18n.t('Unloads {{FROM_NOW}}', {
-													FROM_NOW: dayjs(item.model.ollama?.expires_at * 1000).fromNow()
-												})}`}
-												className="self-end"
-											>
-												<div class=" flex items-center">
-													<span class="relative flex size-2">
-														<span
-															class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
-														/>
-														<span class="relative inline-flex rounded-full size-2 bg-green-500" />
-													</span>
-												</div>
-											</Tooltip>
-										</div>
-									{/if}
-								{/if}
-
-								<!-- {JSON.stringify(item.info)} -->
-
-								{#if item.model?.direct}
-									<Tooltip content={`${$i18n.t('Direct')}`}>
-										<div class="translate-y-[1px]">
-											<svg
-												xmlns="http://www.w3.org/2000/svg"
-												viewBox="0 0 16 16"
-												fill="currentColor"
-												class="size-3"
-											>
-												<path
-													fill-rule="evenodd"
-													d="M2 2.75A.75.75 0 0 1 2.75 2C8.963 2 14 7.037 14 13.25a.75.75 0 0 1-1.5 0c0-5.385-4.365-9.75-9.75-9.75A.75.75 0 0 1 2 2.75Zm0 4.5a.75.75 0 0 1 .75-.75 6.75 6.75 0 0 1 6.75 6.75.75.75 0 0 1-1.5 0C8 10.35 5.65 8 2.75 8A.75.75 0 0 1 2 7.25ZM3.5 11a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
-													clip-rule="evenodd"
-												/>
-											</svg>
-										</div>
-									</Tooltip>
-								{:else if item.model.connection_type === 'external'}
-									<Tooltip content={`${$i18n.t('External')}`}>
-										<div class="translate-y-[1px]">
-											<svg
-												xmlns="http://www.w3.org/2000/svg"
-												viewBox="0 0 16 16"
-												fill="currentColor"
-												class="size-3"
-											>
-												<path
-													fill-rule="evenodd"
-													d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z"
-													clip-rule="evenodd"
-												/>
-												<path
-													fill-rule="evenodd"
-													d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z"
-													clip-rule="evenodd"
-												/>
-											</svg>
-										</div>
-									</Tooltip>
-								{/if}
-
-								{#if item.model?.info?.meta?.description}
-									<Tooltip
-										content={`${marked.parse(
-											sanitizeResponseContent(item.model?.info?.meta?.description).replaceAll(
-												'\n',
-												'<br>'
-											)
-										)}`}
-									>
-										<div class=" translate-y-[1px]">
-											<svg
-												xmlns="http://www.w3.org/2000/svg"
-												fill="none"
-												viewBox="0 0 24 24"
-												stroke-width="1.5"
-												stroke="currentColor"
-												class="w-4 h-4"
-											>
-												<path
-													stroke-linecap="round"
-													stroke-linejoin="round"
-													d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
-												/>
-											</svg>
-										</div>
-									</Tooltip>
-								{/if}
-
-								{#if !$mobile && (item?.model?.tags ?? []).length > 0}
-									<div
-										class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px] overflow-x-auto scrollbar-none"
-									>
-										{#each item.model?.tags.sort((a, b) => a.name.localeCompare(b.name)) as tag}
-											<Tooltip content={tag.name} className="flex-shrink-0">
-												<div
-													class=" text-xs font-bold px-1 rounded-sm uppercase bg-gray-500/20 text-gray-700 dark:text-gray-200"
-												>
-													{tag.name}
-												</div>
-											</Tooltip>
-										{/each}
-									</div>
-								{/if}
-							</div>
-						</div>
-
-						<div class="ml-auto pl-2 pr-1 flex gap-1.5 items-center">
-							{#if $user?.role === 'admin' && item.model.owned_by === 'ollama' && item.model.ollama?.expires_at && new Date(item.model.ollama?.expires_at * 1000) > new Date()}
-								<Tooltip content={`${$i18n.t('Eject')}`} className="flex-shrink-0">
-									<button
-										class="flex"
-										on:click={() => {
-											unloadModelHandler(item.value);
-										}}
-									>
-										<ArrowUpTray className="size-3" />
-									</button>
-								</Tooltip>
-							{/if}
-
-							{#if value === item.value}
-								<div>
-									<Check className="size-3" />
-								</div>
-							{/if}
-						</div>
-					</button>
+					/>
 				{:else}
 					<div class="">
 						<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">

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

@@ -21,7 +21,9 @@
 		channels,
 		socket,
 		config,
-		isApp
+		isApp,
+		pinnedModels,
+		models
 	} from '$lib/stores';
 	import { onMount, getContext, tick, onDestroy } from 'svelte';
 
@@ -644,6 +646,51 @@
 			</div>
 		{/if}
 
+		{#if ($pinnedModels ?? []).length > 0}
+			<div class="py-2">
+				{#each $pinnedModels as modelId (modelId)}
+					<div class="px-1.5 flex justify-center text-gray-800 dark:text-gray-200">
+						<a
+							class="grow flex items-center space-x-3 rounded-lg px-2 py-[7px] hover:bg-gray-100 dark:hover:bg-gray-900 transition"
+							href="/?model={modelId}"
+							on:click={() => {
+								selectedChatId = null;
+								chatId.set('');
+
+								if ($mobile) {
+									showSidebar.set(false);
+								}
+							}}
+							draggable="false"
+						>
+							<div class="self-center">
+								<svg
+									xmlns="http://www.w3.org/2000/svg"
+									fill="none"
+									viewBox="0 0 24 24"
+									stroke-width="2"
+									stroke="currentColor"
+									class="size-[1.1rem]"
+								>
+									<path
+										stroke-linecap="round"
+										stroke-linejoin="round"
+										d="M13.5 16.875h3.375m0 0h3.375m-3.375 0V13.5m0 3.375v3.375M6 10.5h2.25a2.25 2.25 0 0 0 2.25-2.25V6a2.25 2.25 0 0 0-2.25-2.25H6A2.25 2.25 0 0 0 3.75 6v2.25A2.25 2.25 0 0 0 6 10.5Zm0 9.75h2.25A2.25 2.25 0 0 0 10.5 18v-2.25a2.25 2.25 0 0 0-2.25-2.25H6a2.25 2.25 0 0 0-2.25 2.25V18A2.25 2.25 0 0 0 6 20.25Zm9.75-9.75H18a2.25 2.25 0 0 0 2.25-2.25V6A2.25 2.25 0 0 0 18 3.75h-2.25A2.25 2.25 0 0 0 13.5 6v2.25a2.25 2.25 0 0 0 2.25 2.25Z"
+									/>
+								</svg>
+							</div>
+
+							<div class="flex self-center translate-y-[0.5px]">
+								<div class=" self-center font-medium text-sm font-primary">
+									{$models.find((model) => model.id === modelId)?.name ?? modelId}
+								</div>
+							</div>
+						</a>
+					</div>
+				{/each}
+			</div>
+		{/if}
+
 		<div
 			class="relative flex flex-col flex-1 overflow-y-auto overflow-x-hidden {$temporaryChatEnabled
 				? 'opacity-20'

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

@@ -52,6 +52,7 @@ export const pinnedChats = writable([]);
 export const tags = writable([]);
 
 export const models: Writable<Model[]> = writable([]);
+export const pinnedModels = writable([]);
 
 export const prompts: Writable<null | Prompt[]> = writable(null);
 export const knowledge: Writable<null | Document[]> = writable(null);