Timothy Jaeryang Baek 3 周之前
父節點
當前提交
3ed0a6d11f

+ 1 - 0
src/lib/components/chat/MessageInput.svelte

@@ -919,6 +919,7 @@
 
 <FilesOverlay show={dragged} />
 <ToolServersModal bind:show={showTools} {selectedToolIds} />
+
 <InputVariablesModal
 	bind:show={showInputVariablesModal}
 	variables={inputVariables}

+ 1 - 1
src/lib/components/chat/MessageInput/InputMenu.svelte

@@ -81,7 +81,7 @@
 
 	<div slot="content">
 		<DropdownMenu.Content
-			class="w-full max-w-[200px] rounded-2xl px-1 py-1  border border-gray-100  dark:border-gray-800 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-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg transition"
 			sideOffset={4}
 			alignOffset={-6}
 			side="bottom"

+ 176 - 165
src/lib/components/chat/MessageInput/IntegrationsMenu.svelte

@@ -18,6 +18,7 @@
 	import Terminal from '$lib/components/icons/Terminal.svelte';
 	import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
 	import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
+	import { crossfade, fade, fly, slide } from 'svelte/transition';
 
 	const i18n = getContext('i18n');
 
@@ -39,9 +40,10 @@
 
 	export let onClose: Function;
 
-	let tools = null;
 	let show = false;
-	let showAllTools = false;
+	let tab = '';
+
+	let tools = null;
 
 	$: if (show) {
 		init();
@@ -81,129 +83,114 @@
 	</Tooltip>
 	<div slot="content">
 		<DropdownMenu.Content
-			class="w-full max-w-[240px] rounded-2xl px-1 py-1  border border-gray-100  dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg max-h-72 overflow-y-auto scrollbar-thin"
+			class="w-full max-w-[240px] rounded-2xl px-1 py-1  border border-gray-100  dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg max-h-72 overflow-y-auto overflow-x-hidden scrollbar-thin"
 			sideOffset={4}
 			alignOffset={-6}
 			side="bottom"
 			align="start"
 			transition={flyAndScale}
 		>
-			{#if tools}
-				{#if Object.keys(tools).length > 0}
-					<button
-						class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
-						on:click={() => {
-							showAllTools = !showAllTools;
-						}}
-					>
-						{#if !showAllTools}
-							<Wrench />
-
-							<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>
+			{#if tab === ''}
+				<div in:fly={{ x: -20, duration: 150 }}>
+					{#if tools}
+						{#if Object.keys(tools).length > 0}
+							<button
+								class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
+								on:click={() => {
+									tab = 'tools';
+								}}
+							>
+								<Wrench />
 
-								<div class="text-gray-500">
-									<ChevronRight />
-								</div>
-							</div>
-						{:else}
-							<ChevronLeft />
+								<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="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 class="text-gray-500">
+										<ChevronRight />
+									</div>
 								</div>
-							</div>
+							</button>
 						{/if}
-					</button>
-				{/if}
-			{:else}
-				<div class="py-4">
-					<Spinner />
-				</div>
-			{/if}
+					{:else}
+						<div class="py-4">
+							<Spinner />
+						</div>
+					{/if}
 
-			{#if showAllTools}
-				{#each Object.keys(tools) as toolId}
-					<button
-						class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
-						on:click={() => {
-							tools[toolId].enabled = !tools[toolId].enabled;
-						}}
-					>
-						<div class="flex-1 truncate">
-							<div class="flex flex-1 gap-2 items-center">
-								<Tooltip content={tools[toolId]?.name ?? ''} placement="top">
-									<div class="shrink-0">
-										<Wrench />
+					{#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-1.5 text-sm 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>
+
+											<div class=" truncate">{filter?.name}</div>
+										</div>
 									</div>
-								</Tooltip>
-								<Tooltip content={tools[toolId]?.description ?? ''} placement="top-start">
-									<div class=" truncate">{tools[toolId].name}</div>
-								</Tooltip>
-							</div>
-						</div>
 
-						<div class=" shrink-0">
-							<Switch
-								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>
-				{/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">
+									<div class=" shrink-0">
+										<Switch
+											state={selectedFilterIds.includes(filter.id)}
+											on:change={async (e) => {
+												const state = e.detail;
+												await tick();
+											}}
+										/>
+									</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-1.5 text-sm 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];
-									}
+									webSearchEnabled = !webSearchEnabled;
 								}}
 							>
 								<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}
+											<GlobeAlt />
 										</div>
 
-										<div class=" truncate">{filter?.name}</div>
+										<div class=" truncate">{$i18n.t('Web Search')}</div>
 									</div>
 								</div>
 
 								<div class=" shrink-0">
 									<Switch
-										state={selectedFilterIds.includes(filter.id)}
+										state={webSearchEnabled}
 										on:change={async (e) => {
 											const state = e.detail;
 											await tick();
@@ -212,105 +199,129 @@
 								</div>
 							</button>
 						</Tooltip>
-					{/each}
-				{/if}
+					{/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-1.5 text-sm 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 />
+					{#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-1.5 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=" truncate">{$i18n.t('Web Search')}</div>
+								<div class=" shrink-0">
+									<Switch
+										state={imageGenerationEnabled}
+										on:change={async (e) => {
+											const state = e.detail;
+											await tick();
+										}}
+									/>
 								</div>
-							</div>
+							</button>
+						</Tooltip>
+					{/if}
 
-							<div class=" shrink-0">
-								<Switch
-									state={webSearchEnabled}
-									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-1.5 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>
 
-				{#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-1.5 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 class=" truncate">{$i18n.t('Code Interpreter')}</div>
 									</div>
+								</div>
 
-									<div class=" truncate">{$i18n.t('Image')}</div>
+								<div class=" shrink-0">
+									<Switch
+										state={codeInterpreterEnabled}
+										on:change={async (e) => {
+											const state = e.detail;
+											await tick();
+										}}
+									/>
 								</div>
-							</div>
+							</button>
+						</Tooltip>
+					{/if}
+				</div>
+			{:else if tab === 'tools' && tools}
+				<div in:fly={{ x: 20, duration: 150 }}>
+					<button
+						class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
+						on:click={() => {
+							tab = '';
+						}}
+					>
+						<ChevronLeft />
 
-							<div class=" shrink-0">
-								<Switch
-									state={imageGenerationEnabled}
-									on:change={async (e) => {
-										const state = e.detail;
-										await tick();
-									}}
-								/>
+						<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>
-						</button>
-					</Tooltip>
-				{/if}
+						</div>
+					</button>
 
-				{#if showCodeInterpreterButton}
-					<Tooltip content={$i18n.t('Execute code for analysis')} placement="top-start">
+					{#each Object.keys(tools) as toolId}
 						<button
 							class="flex w-full justify-between gap-2 items-center px-3 py-1.5 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;
+								tools[toolId].enabled = !tools[toolId].enabled;
 							}}
 						>
 							<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>
+									<Tooltip content={tools[toolId]?.name ?? ''} placement="top">
+										<div class="shrink-0">
+											<Wrench />
+										</div>
+									</Tooltip>
+									<Tooltip content={tools[toolId]?.description ?? ''} placement="top-start">
+										<div class=" truncate">{tools[toolId].name}</div>
+									</Tooltip>
 								</div>
 							</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}
+					{/each}
+				</div>
 			{/if}
 		</DropdownMenu.Content>
 	</div>