Timothy Jaeryang Baek 4 nedēļas atpakaļ
vecāks
revīzija
96b8aaf83f

+ 12 - 32
src/lib/components/chat/MessageInput.svelte

@@ -1739,11 +1739,11 @@
 											<div
 												class="bg-transparent hover:bg-gray-100 text-gray-700 dark:text-white dark:hover:bg-gray-800 rounded-full size-8 flex justify-center items-center outline-hidden focus:outline-hidden"
 											>
-												<Component className="size-4.5" strokeWidth="1.75" />
+												<Component className="size-4.5" strokeWidth="1.5" />
 											</div>
 										</OptionsMenu>
 
-										<div class="ml-1 flex gap-1.5">
+										<div class="ml-0.5 flex gap-1.5">
 											{#if showToolsButton}
 												<Tooltip
 													content={$i18n.t('{{COUNT}} Available Tools', {
@@ -1751,7 +1751,7 @@
 													})}
 												>
 													<button
-														class="translate-y-[0.5px] flex gap-1 items-center text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 rounded-lg p-1 self-center transition"
+														class="translate-y-[0.5px] px-1 flex gap-1 items-center text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 rounded-lg self-center transition"
 														aria-label="Available Tools"
 														type="button"
 														on:click={() => {
@@ -1760,7 +1760,7 @@
 													>
 														<Wrench className="size-4" strokeWidth="1.75" />
 
-														<span class="text-sm font-medium text-gray-600 dark:text-gray-300">
+														<span class="text-sm">
 															{toolServers.length + selectedToolIds.length}
 														</span>
 													</button>
@@ -1770,7 +1770,7 @@
 											{#each selectedFilterIds as filterId}
 												{@const filter = toggleFilters.find((f) => f.id === filterId)}
 												{#if filter}
-													<Tooltip content={filter?.description} placement="top">
+													<Tooltip content={filter?.name} placement="top">
 														<button
 															on:click|preventDefault={() => {
 																selectedFilterIds = selectedFilterIds.filter(
@@ -1778,7 +1778,7 @@
 																);
 															}}
 															type="button"
-															class="group px-2 @xl:px-2.5 py-2 flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {selectedFilterIds.includes(
+															class="group px-2 py-2 flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {selectedFilterIds.includes(
 																filterId
 															)
 																? 'text-sky-500 dark:text-sky-300 bg-sky-50 dark:bg-sky-200/5'
@@ -1798,11 +1798,6 @@
 															{:else}
 																<Sparkles className="size-4" strokeWidth="1.75" />
 															{/if}
-															<span
-																class="hidden @xl:block whitespace-nowrap text-ellipsis leading-none normal-case pr-0.5"
-																>{filter?.name}</span
-															>
-
 															<div class="hidden group-hover:block">
 																<XMark className="size-4" strokeWidth="1.75" />
 															</div>
@@ -1812,21 +1807,16 @@
 											{/each}
 
 											{#if webSearchEnabled}
-												<Tooltip content={$i18n.t('Search the internet')} placement="top">
+												<Tooltip content={$i18n.t('Web Search')} placement="top">
 													<button
 														on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)}
 														type="button"
-														class="group px-2 @xl:px-2.5 py-2 flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {webSearchEnabled ||
+														class="group px-2 py-2 flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {webSearchEnabled ||
 														($settings?.webSearch ?? false) === 'always'
 															? ' text-sky-500 dark:text-sky-300 bg-sky-50 dark:bg-sky-200/5'
 															: 'bg-transparent text-gray-600 dark:text-gray-300 '}"
 													>
 														<GlobeAlt className="size-4" strokeWidth="1.75" />
-														<span
-															class="hidden @xl:block whitespace-nowrap text-ellipsis leading-none normal-case pr-0.5"
-															>{$i18n.t('Web Search')}</span
-														>
-
 														<div class="hidden group-hover:block">
 															<XMark className="size-4" strokeWidth="1.75" />
 														</div>
@@ -1835,22 +1825,16 @@
 											{/if}
 
 											{#if imageGenerationEnabled}
-												<Tooltip content={$i18n.t('Generate an image')} placement="top">
+												<Tooltip content={$i18n.t('Image')} placement="top">
 													<button
 														on:click|preventDefault={() =>
 															(imageGenerationEnabled = !imageGenerationEnabled)}
 														type="button"
-														class="group px-2 @xl:px-2.5 py-2 flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {imageGenerationEnabled
+														class="group px-2 py-2 flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {imageGenerationEnabled
 															? ' text-sky-500 dark:text-sky-300 bg-sky-50 dark:bg-sky-200/5'
 															: 'bg-transparent text-gray-600 dark:text-gray-300 '}"
 													>
 														<Photo className="size-4" strokeWidth="1.75" />
-
-														<span
-															class="hidden @xl:block whitespace-nowrap text-ellipsis leading-none normal-case pr-0.5"
-															>{$i18n.t('Image')}</span
-														>
-
 														<div class="hidden group-hover:block">
 															<XMark className="size-4" strokeWidth="1.75" />
 														</div>
@@ -1859,7 +1843,7 @@
 											{/if}
 
 											{#if codeInterpreterEnabled}
-												<Tooltip content={$i18n.t('Execute code for analysis')} placement="top">
+												<Tooltip content={$i18n.t('Code Interpreter')} placement="top">
 													<button
 														aria-label={codeInterpreterEnabled
 															? $i18n.t('Disable Code Interpreter')
@@ -1868,7 +1852,7 @@
 														on:click|preventDefault={() =>
 															(codeInterpreterEnabled = !codeInterpreterEnabled)}
 														type="button"
-														class=" group px-2 @xl:px-2.5 py-2 flex gap-1.5 items-center text-sm transition-colors duration-300 max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {codeInterpreterEnabled
+														class=" group px-2 py-2 flex gap-1.5 items-center text-sm transition-colors duration-300 max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {codeInterpreterEnabled
 															? ' text-sky-500 dark:text-sky-300 bg-sky-50 dark:bg-sky-200/5'
 															: 'bg-transparent text-gray-600 dark:text-gray-300 '} {($settings?.highContrastMode ??
 														false)
@@ -1876,10 +1860,6 @@
 															: 'focus:outline-hidden rounded-full'}"
 													>
 														<Terminal className="size-3.5" strokeWidth="2" />
-														<span
-															class="hidden @xl:block whitespace-nowrap text-ellipsis leading-none normal-case pr-0.5"
-															>{$i18n.t('Code Interpreter')}</span
-														>
 
 														<div class="hidden group-hover:block">
 															<XMark className="size-4" strokeWidth="1.75" />

+ 173 - 163
src/lib/components/chat/MessageInput/OptionsMenu.svelte

@@ -16,6 +16,8 @@
 	import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
 	import Photo from '$lib/components/icons/Photo.svelte';
 	import Terminal from '$lib/components/icons/Terminal.svelte';
+	import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
+	import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
 
 	const i18n = getContext('i18n');
 
@@ -78,231 +80,239 @@
 		<slot />
 	</Tooltip>
 
+	<!-- class="w-full max-w-[240px] rounded-2xl px-1 py-1  border border-gray-100  dark:border-gray-850 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg transition max-h-96 overflow-y-auto scrollbar-thin" -->
+
 	<div slot="content">
 		<DropdownMenu.Content
-			class="w-full max-w-[240px] rounded-2xl px-1 py-1  border border-gray-100  dark:border-gray-850 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg transition"
+			class="w-full max-w-[240px] rounded-2xl px-1 py-1  border border-gray-100  dark:border-gray-850 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg max-h-72 overflow-y-auto scrollbar-thin"
 			sideOffset={4}
 			alignOffset={-6}
 			side="bottom"
 			align="start"
 			transition={flyAndScale}
 		>
-			{#if toggleFilters && toggleFilters.length > 0}
-				{#each toggleFilters.sort( (a, b) => a.name.localeCompare( b.name, undefined, { sensitivity: 'base' } ) ) as filter, filterIdx (filter.id)}
-					<Tooltip content={filter?.description} placement="top">
-						<button
-							class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
-							on:click={() => {
-								if (selectedFilterIds.includes(filter.id)) {
-									selectedFilterIds = selectedFilterIds.filter((id) => id !== filter.id);
-								} else {
-									selectedFilterIds = [...selectedFilterIds, filter.id];
-								}
-							}}
-						>
-							<div class="flex-1 truncate">
-								<div class="flex flex-1 gap-2 items-center">
-									<div class="shrink-0">
-										{#if filter?.icon}
-											<div class="size-4 items-center flex justify-center">
-												<img
-													src={filter.icon}
-													class="size-3.5 {filter.icon.includes('svg') ? 'dark:invert-[80%]' : ''}"
-													style="fill: currentColor;"
-													alt={filter.name}
-												/>
-											</div>
-										{:else}
-											<Sparkles className="size-4" strokeWidth="1.75" />
-										{/if}
-									</div>
+			{#if tools}
+				<button
+					class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
+					on:click={() => {
+						showAllTools = !showAllTools;
+					}}
+				>
+					{#if !showAllTools}
+						<Wrench />
 
-									<div class=" truncate">{filter?.name}</div>
-								</div>
+						<div class="flex items-center w-full justify-between">
+							<div>
+								{$i18n.t('Tools')}
+								<span class="ml-0.5 text-gray-500">{Object.keys(tools).length}</span>
 							</div>
 
-							<div class=" shrink-0">
-								<Switch
-									state={selectedFilterIds.includes(filter.id)}
-									on:change={async (e) => {
-										const state = e.detail;
-										await tick();
-									}}
-								/>
+							<div class="text-gray-500">
+								<ChevronRight />
 							</div>
-						</button>
-					</Tooltip>
-				{/each}
-			{/if}
-
-			{#if showWebSearchButton}
-				<Tooltip content={$i18n.t('Search the internet')} placement="top-start">
-					<button
-						class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
-						on:click={() => {
-							webSearchEnabled = !webSearchEnabled;
-						}}
-					>
-						<div class="flex-1 truncate">
-							<div class="flex flex-1 gap-2 items-center">
-								<div class="shrink-0">
-									<GlobeAlt />
-								</div>
-
-								<div class=" truncate">{$i18n.t('Web Search')}</div>
-							</div>
-						</div>
-
-						<div class=" shrink-0">
-							<Switch
-								state={webSearchEnabled}
-								on:change={async (e) => {
-									const state = e.detail;
-									await tick();
-								}}
-							/>
 						</div>
-					</button>
-				</Tooltip>
-			{/if}
+					{:else}
+						<ChevronLeft />
 
-			{#if showImageGenerationButton}
-				<Tooltip content={$i18n.t('Generate an image')} placement="top-start">
-					<button
-						class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
-						on:click={() => {
-							imageGenerationEnabled = !imageGenerationEnabled;
-						}}
-					>
-						<div class="flex-1 truncate">
-							<div class="flex flex-1 gap-2 items-center">
-								<div class="shrink-0">
-									<Photo className="size-4" strokeWidth="1.5" />
-								</div>
-
-								<div class=" truncate">{$i18n.t('Image')}</div>
+						<div class="flex items-center w-full justify-between">
+							<div>
+								{$i18n.t('Tools')}
+								<span class="ml-0.5 text-gray-500">{Object.keys(tools).length}</span>
 							</div>
 						</div>
-
-						<div class=" shrink-0">
-							<Switch
-								state={imageGenerationEnabled}
-								on:change={async (e) => {
-									const state = e.detail;
-									await tick();
-								}}
-							/>
-						</div>
-					</button>
-				</Tooltip>
+					{/if}
+				</button>
+			{:else}
+				<div class="py-4">
+					<Spinner />
+				</div>
 			{/if}
 
-			{#if showCodeInterpreterButton}
-				<Tooltip content={$i18n.t('Execute code for analysis')} placement="top-start">
+			{#if showAllTools}
+				{#each Object.keys(tools) as toolId}
 					<button
-						class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
-						aria-pressed={codeInterpreterEnabled}
-						aria-label={codeInterpreterEnabled
-							? $i18n.t('Disable Code Interpreter')
-							: $i18n.t('Enable Code Interpreter')}
+						class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
 						on:click={() => {
-							codeInterpreterEnabled = !codeInterpreterEnabled;
+							tools[toolId].enabled = !tools[toolId].enabled;
 						}}
 					>
 						<div class="flex-1 truncate">
-							<div class="flex flex-1 gap-2 items-center">
+							<Tooltip
+								content={tools[toolId]?.description ?? ''}
+								placement="top-start"
+								className="flex flex-1 gap-2 items-center"
+							>
 								<div class="shrink-0">
-									<Terminal className="size-3.5" strokeWidth="1.75" />
+									<Wrench />
 								</div>
 
-								<div class=" truncate">{$i18n.t('Code Interpreter')}</div>
-							</div>
+								<div class=" truncate">{tools[toolId].name}</div>
+							</Tooltip>
 						</div>
 
 						<div class=" shrink-0">
 							<Switch
-								state={codeInterpreterEnabled}
+								state={tools[toolId].enabled}
 								on:change={async (e) => {
 									const state = e.detail;
 									await tick();
+									if (state) {
+										selectedToolIds = [...selectedToolIds, toolId];
+									} else {
+										selectedToolIds = selectedToolIds.filter((id) => id !== toolId);
+									}
 								}}
 							/>
 						</div>
 					</button>
-				</Tooltip>
-			{/if}
-
-			{#if tools}
-				<hr class="my-1 border-gray-50 dark:border-gray-850" />
-
-				{#if Object.keys(tools).length > 0}
-					<div class="{showAllTools ? ' max-h-96' : 'max-h-28'} overflow-y-auto scrollbar-thin">
-						{#each Object.keys(tools) as toolId}
+				{/each}
+			{:else}
+				{#if toggleFilters && toggleFilters.length > 0}
+					{#each toggleFilters.sort( (a, b) => a.name.localeCompare( b.name, undefined, { sensitivity: 'base' } ) ) as filter, filterIdx (filter.id)}
+						<Tooltip content={filter?.description} placement="top-start">
 							<button
-								class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
+								class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
 								on:click={() => {
-									tools[toolId].enabled = !tools[toolId].enabled;
+									if (selectedFilterIds.includes(filter.id)) {
+										selectedFilterIds = selectedFilterIds.filter((id) => id !== filter.id);
+									} else {
+										selectedFilterIds = [...selectedFilterIds, filter.id];
+									}
 								}}
 							>
 								<div class="flex-1 truncate">
-									<Tooltip
-										content={tools[toolId]?.description ?? ''}
-										placement="top-start"
-										className="flex flex-1 gap-2 items-center"
-									>
+									<div class="flex flex-1 gap-2 items-center">
 										<div class="shrink-0">
-											<Wrench />
+											{#if filter?.icon}
+												<div class="size-4 items-center flex justify-center">
+													<img
+														src={filter.icon}
+														class="size-3.5 {filter.icon.includes('svg')
+															? 'dark:invert-[80%]'
+															: ''}"
+														style="fill: currentColor;"
+														alt={filter.name}
+													/>
+												</div>
+											{:else}
+												<Sparkles className="size-4" strokeWidth="1.75" />
+											{/if}
 										</div>
 
-										<div class=" truncate">{tools[toolId].name}</div>
-									</Tooltip>
+										<div class=" truncate">{filter?.name}</div>
+									</div>
 								</div>
 
 								<div class=" shrink-0">
 									<Switch
-										state={tools[toolId].enabled}
+										state={selectedFilterIds.includes(filter.id)}
 										on:change={async (e) => {
 											const state = e.detail;
 											await tick();
-											if (state) {
-												selectedToolIds = [...selectedToolIds, toolId];
-											} else {
-												selectedToolIds = selectedToolIds.filter((id) => id !== toolId);
-											}
 										}}
 									/>
 								</div>
 							</button>
-						{/each}
-					</div>
-					{#if Object.keys(tools).length > 3}
+						</Tooltip>
+					{/each}
+				{/if}
+
+				{#if showWebSearchButton}
+					<Tooltip content={$i18n.t('Search the internet')} placement="top-start">
 						<button
-							class="flex w-full justify-center items-center text-sm font-medium cursor-pointer rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800"
+							class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
 							on:click={() => {
-								showAllTools = !showAllTools;
+								webSearchEnabled = !webSearchEnabled;
 							}}
-							title={showAllTools ? $i18n.t('Show Less') : $i18n.t('Show All')}
 						>
-							<svg
-								xmlns="http://www.w3.org/2000/svg"
-								fill="none"
-								viewBox="0 0 24 24"
-								stroke-width="2.5"
-								stroke="currentColor"
-								class="size-3 transition-transform duration-200 {showAllTools
-									? 'rotate-180'
-									: ''} text-gray-300 dark:text-gray-600"
-							>
-								<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"
-								></path>
-							</svg>
+							<div class="flex-1 truncate">
+								<div class="flex flex-1 gap-2 items-center">
+									<div class="shrink-0">
+										<GlobeAlt />
+									</div>
+
+									<div class=" truncate">{$i18n.t('Web Search')}</div>
+								</div>
+							</div>
+
+							<div class=" shrink-0">
+								<Switch
+									state={webSearchEnabled}
+									on:change={async (e) => {
+										const state = e.detail;
+										await tick();
+									}}
+								/>
+							</div>
 						</button>
-					{/if}
+					</Tooltip>
+				{/if}
+
+				{#if showImageGenerationButton}
+					<Tooltip content={$i18n.t('Generate an image')} placement="top-start">
+						<button
+							class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
+							on:click={() => {
+								imageGenerationEnabled = !imageGenerationEnabled;
+							}}
+						>
+							<div class="flex-1 truncate">
+								<div class="flex flex-1 gap-2 items-center">
+									<div class="shrink-0">
+										<Photo className="size-4" strokeWidth="1.5" />
+									</div>
+
+									<div class=" truncate">{$i18n.t('Image')}</div>
+								</div>
+							</div>
+
+							<div class=" shrink-0">
+								<Switch
+									state={imageGenerationEnabled}
+									on:change={async (e) => {
+										const state = e.detail;
+										await tick();
+									}}
+								/>
+							</div>
+						</button>
+					</Tooltip>
+				{/if}
+
+				{#if showCodeInterpreterButton}
+					<Tooltip content={$i18n.t('Execute code for analysis')} placement="top-start">
+						<button
+							class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
+							aria-pressed={codeInterpreterEnabled}
+							aria-label={codeInterpreterEnabled
+								? $i18n.t('Disable Code Interpreter')
+								: $i18n.t('Enable Code Interpreter')}
+							on:click={() => {
+								codeInterpreterEnabled = !codeInterpreterEnabled;
+							}}
+						>
+							<div class="flex-1 truncate">
+								<div class="flex flex-1 gap-2 items-center">
+									<div class="shrink-0">
+										<Terminal className="size-3.5" strokeWidth="1.75" />
+									</div>
+
+									<div class=" truncate">{$i18n.t('Code Interpreter')}</div>
+								</div>
+							</div>
+
+							<div class=" shrink-0">
+								<Switch
+									state={codeInterpreterEnabled}
+									on:change={async (e) => {
+										const state = e.detail;
+										await tick();
+									}}
+								/>
+							</div>
+						</button>
+					</Tooltip>
 				{/if}
-			{:else}
-				<div class="py-4">
-					<Spinner />
-				</div>
 			{/if}
 		</DropdownMenu.Content>
 	</div>