Sfoglia il codice sorgente

feat: background image

Timothy J. Baek 10 mesi fa
parent
commit
5fa355e1ae

+ 19 - 3
src/lib/components/chat/Chat.svelte

@@ -273,6 +273,7 @@
 				id: m.id,
 				role: m.role,
 				content: m.content,
+				info: m.info ? m.info : undefined,
 				timestamp: m.timestamp
 			})),
 			chat_id: $chatId
@@ -1322,6 +1323,19 @@
 			? 'md:max-w-[calc(100%-260px)]'
 			: ''} w-full max-w-full flex flex-col"
 	>
+		{#if $settings?.backgroundImageUrl ?? null}
+			<div
+				class="absolute {$showSidebar
+					? 'md:max-w-[calc(100%-260px)] md:translate-x-[260px]'
+					: ''} top-0 left-0 w-full h-full bg-cover bg-center bg-no-repeat"
+				style="background-image: url({$settings.backgroundImageUrl})  "
+			/>
+
+			<div
+				class="absolute top-0 left-0 w-full h-full bg-gradient-to-t from-white to-white/90 dark:from-gray-900 dark:to-[#171717]/90 z-0 -translate-y-[78px]"
+			/>
+		{/if}
+
 		<Navbar
 			{title}
 			bind:selectedModels
@@ -1333,7 +1347,9 @@
 
 		{#if $banners.length > 0 && messages.length === 0 && !$chatId && selectedModels.length <= 1}
 			<div
-				class="absolute top-[4.25rem] w-full {$showSidebar ? 'md:max-w-[calc(100%-260px)]' : ''}"
+				class="absolute top-[4.25rem] w-full {$showSidebar
+					? 'md:max-w-[calc(100%-260px)]'
+					: ''} z-0"
 			>
 				<div class=" flex flex-col gap-1 w-full">
 					{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
@@ -1358,9 +1374,9 @@
 			</div>
 		{/if}
 
-		<div class="flex flex-col flex-auto">
+		<div class="flex flex-col flex-auto z-10">
 			<div
-				class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full"
+				class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10"
 				id="messages-container"
 				bind:this={messagesContainerElement}
 				on:scroll={(e) => {

+ 148 - 84
src/lib/components/chat/Settings/Interface.svelte

@@ -13,6 +13,10 @@
 
 	export let saveSettings: Function;
 
+	let backgroundImageUrl = null;
+	let inputFiles = null;
+	let filesInputElement;
+
 	// Addons
 	let titleAutoGenerate = true;
 	let responseAutoCopy = false;
@@ -132,6 +136,8 @@
 		userLocation = $settings.userLocation ?? false;
 
 		defaultModelId = ($settings?.models ?? ['']).at(0);
+
+		backgroundImageUrl = $settings.backgroundImageUrl ?? null;
 	});
 </script>
 
@@ -142,13 +148,63 @@
 		dispatch('save');
 	}}
 >
-	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]">
+	<input
+		bind:this={filesInputElement}
+		bind:files={inputFiles}
+		type="file"
+		hidden
+		accept="image/*"
+		on:change={() => {
+			let reader = new FileReader();
+			reader.onload = (event) => {
+				let originalImageUrl = `${event.target.result}`;
+
+				backgroundImageUrl = originalImageUrl;
+				saveSettings({ backgroundImageUrl });
+			};
+
+			if (
+				inputFiles &&
+				inputFiles.length > 0 &&
+				['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(inputFiles[0]['type'])
+			) {
+				reader.readAsDataURL(inputFiles[0]);
+			} else {
+				console.log(`Unsupported File Type '${inputFiles[0]['type']}'.`);
+				inputFiles = null;
+			}
+		}}
+	/>
+
+	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem] scrollbar-hidden">
+		<div class=" space-y-1 mb-3">
+			<div class="mb-2">
+				<div class="flex justify-between items-center text-xs">
+					<div class=" text-sm font-medium">{$i18n.t('Default Model')}</div>
+				</div>
+			</div>
+
+			<div class="flex-1 mr-2">
+				<select
+					class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+					bind:value={defaultModelId}
+					placeholder="Select a model"
+				>
+					<option value="" disabled selected>{$i18n.t('Select a model')}</option>
+					{#each $models.filter((model) => model.id) as model}
+						<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
+					{/each}
+				</select>
+			</div>
+		</div>
+		<hr class=" dark:border-gray-850" />
+
 		<div>
-			<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Add-ons')}</div>
+			<div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div>
 
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">{$i18n.t('Chat Bubble UI')}</div>
+					<div class=" self-center text-xs">{$i18n.t('Chat Bubble UI')}</div>
 
 					<button
 						class="p-1 px-3 text-xs flex rounded transition"
@@ -166,9 +222,33 @@
 				</div>
 			</div>
 
+			{#if !$settings.chatBubble}
+				<div>
+					<div class=" py-0.5 flex w-full justify-between">
+						<div class=" self-center text-xs">
+							{$i18n.t('Display the username instead of You in the Chat')}
+						</div>
+
+						<button
+							class="p-1 px-3 text-xs flex rounded transition"
+							on:click={() => {
+								toggleShowUsername();
+							}}
+							type="button"
+						>
+							{#if showUsername === true}
+								<span class="ml-2 self-center">{$i18n.t('On')}</span>
+							{:else}
+								<span class="ml-2 self-center">{$i18n.t('Off')}</span>
+							{/if}
+						</button>
+					</div>
+				</div>
+			{/if}
+
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">{$i18n.t('Widescreen Mode')}</div>
+					<div class=" self-center text-xs">{$i18n.t('Widescreen Mode')}</div>
 
 					<button
 						class="p-1 px-3 text-xs flex rounded transition"
@@ -188,16 +268,36 @@
 
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">{$i18n.t('Title Auto-Generation')}</div>
+					<div class=" self-center text-xs">{$i18n.t('Chat direction')}</div>
+
+					<button
+						class="p-1 px-3 text-xs flex rounded transition"
+						on:click={toggleChangeChatDirection}
+						type="button"
+					>
+						{#if chatDirection === 'LTR'}
+							<span class="ml-2 self-center">{$i18n.t('LTR')}</span>
+						{:else}
+							<span class="ml-2 self-center">{$i18n.t('RTL')}</span>
+						{/if}
+					</button>
+				</div>
+			</div>
+
+			<div>
+				<div class=" py-0.5 flex w-full justify-between">
+					<div class=" self-center text-xs">
+						{$i18n.t('Fluidly stream large external response chunks')}
+					</div>
 
 					<button
 						class="p-1 px-3 text-xs flex rounded transition"
 						on:click={() => {
-							toggleTitleAutoGenerate();
+							toggleSplitLargeChunks();
 						}}
 						type="button"
 					>
-						{#if titleAutoGenerate === true}
+						{#if splitLargeChunks === true}
 							<span class="ml-2 self-center">{$i18n.t('On')}</span>
 						{:else}
 							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -208,38 +308,45 @@
 
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">
-						{$i18n.t('Response AutoCopy to Clipboard')}
+					<div class=" self-center text-xs">
+						{$i18n.t('Chat Background Image')}
 					</div>
 
 					<button
 						class="p-1 px-3 text-xs flex rounded transition"
 						on:click={() => {
-							toggleResponseAutoCopy();
+							if (backgroundImageUrl !== null) {
+								backgroundImageUrl = null;
+								saveSettings({ backgroundImageUrl });
+							} else {
+								filesInputElement.click();
+							}
 						}}
 						type="button"
 					>
-						{#if responseAutoCopy === true}
-							<span class="ml-2 self-center">{$i18n.t('On')}</span>
+						{#if backgroundImageUrl !== null}
+							<span class="ml-2 self-center">{$i18n.t('Reset')}</span>
 						{:else}
-							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
+							<span class="ml-2 self-center">{$i18n.t('Upload')}</span>
 						{/if}
 					</button>
 				</div>
 			</div>
 
+			<div class=" my-1.5 text-sm font-medium">{$i18n.t('Chat')}</div>
+
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">{$i18n.t('Allow User Location')}</div>
+					<div class=" self-center text-xs">{$i18n.t('Title Auto-Generation')}</div>
 
 					<button
 						class="p-1 px-3 text-xs flex rounded transition"
 						on:click={() => {
-							toggleUserLocation();
+							toggleTitleAutoGenerate();
 						}}
 						type="button"
 					>
-						{#if userLocation === true}
+						{#if titleAutoGenerate === true}
 							<span class="ml-2 self-center">{$i18n.t('On')}</span>
 						{:else}
 							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -250,16 +357,18 @@
 
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">{$i18n.t('Display Emoji in Call')}</div>
+					<div class=" self-center text-xs">
+						{$i18n.t('Response AutoCopy to Clipboard')}
+					</div>
 
 					<button
 						class="p-1 px-3 text-xs flex rounded transition"
 						on:click={() => {
-							toggleEmojiInCall();
+							toggleResponseAutoCopy();
 						}}
 						type="button"
 					>
-						{#if showEmojiInCall === true}
+						{#if responseAutoCopy === true}
 							<span class="ml-2 self-center">{$i18n.t('On')}</span>
 						{:else}
 							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -268,44 +377,18 @@
 				</div>
 			</div>
 
-			{#if !$settings.chatBubble}
-				<div>
-					<div class=" py-0.5 flex w-full justify-between">
-						<div class=" self-center text-xs font-medium">
-							{$i18n.t('Display the username instead of You in the Chat')}
-						</div>
-
-						<button
-							class="p-1 px-3 text-xs flex rounded transition"
-							on:click={() => {
-								toggleShowUsername();
-							}}
-							type="button"
-						>
-							{#if showUsername === true}
-								<span class="ml-2 self-center">{$i18n.t('On')}</span>
-							{:else}
-								<span class="ml-2 self-center">{$i18n.t('Off')}</span>
-							{/if}
-						</button>
-					</div>
-				</div>
-			{/if}
-
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">
-						{$i18n.t('Fluidly stream large external response chunks')}
-					</div>
+					<div class=" self-center text-xs">{$i18n.t('Allow User Location')}</div>
 
 					<button
 						class="p-1 px-3 text-xs flex rounded transition"
 						on:click={() => {
-							toggleSplitLargeChunks();
+							toggleUserLocation();
 						}}
 						type="button"
 					>
-						{#if splitLargeChunks === true}
+						{#if userLocation === true}
 							<span class="ml-2 self-center">{$i18n.t('On')}</span>
 						{:else}
 							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -313,47 +396,28 @@
 					</button>
 				</div>
 			</div>
-		</div>
 
-		<div>
-			<div class=" py-0.5 flex w-full justify-between">
-				<div class=" self-center text-xs font-medium">{$i18n.t('Chat direction')}</div>
-
-				<button
-					class="p-1 px-3 text-xs flex rounded transition"
-					on:click={toggleChangeChatDirection}
-					type="button"
-				>
-					{#if chatDirection === 'LTR'}
-						<span class="ml-2 self-center">{$i18n.t('LTR')}</span>
-					{:else}
-						<span class="ml-2 self-center">{$i18n.t('RTL')}</span>
-					{/if}
-				</button>
-			</div>
-		</div>
+			<div class=" my-1.5 text-sm font-medium">{$i18n.t('Voice')}</div>
 
-		<hr class=" dark:border-gray-850" />
+			<div>
+				<div class=" py-0.5 flex w-full justify-between">
+					<div class=" self-center text-xs">{$i18n.t('Display Emoji in Call')}</div>
 
-		<div class=" space-y-1 mb-3">
-			<div class="mb-2">
-				<div class="flex justify-between items-center text-xs">
-					<div class=" text-xs font-medium">{$i18n.t('Default Model')}</div>
+					<button
+						class="p-1 px-3 text-xs flex rounded transition"
+						on:click={() => {
+							toggleEmojiInCall();
+						}}
+						type="button"
+					>
+						{#if showEmojiInCall === true}
+							<span class="ml-2 self-center">{$i18n.t('On')}</span>
+						{:else}
+							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
+						{/if}
+					</button>
 				</div>
 			</div>
-
-			<div class="flex-1 mr-2">
-				<select
-					class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
-					bind:value={defaultModelId}
-					placeholder="Select a model"
-				>
-					<option value="" disabled selected>{$i18n.t('Select a model')}</option>
-					{#each $models.filter((model) => model.id) as model}
-						<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
-					{/each}
-				</select>
-			</div>
 		</div>
 	</div>
 

+ 0 - 0
src/lib/utils/characters/index.ts


+ 4 - 1
src/routes/(app)/c/[id]/+page.svelte

@@ -1,6 +1,9 @@
 <script lang="ts">
-	import Chat from '$lib/components/chat/Chat.svelte';
 	import { page } from '$app/stores';
+
+	import Chat from '$lib/components/chat/Chat.svelte';
+	import Help from '$lib/components/layout/Help.svelte';
 </script>
 
+<Help />
 <Chat chatIdProp={$page.params.id} />