瀏覽代碼

refac: input menu

Timothy Jaeryang Baek 4 周之前
父節點
當前提交
a68342d5a8

+ 138 - 109
src/lib/components/chat/MessageInput.svelte

@@ -71,6 +71,9 @@
 	import Voice from '../icons/Voice.svelte';
 	import { getSessionUser } from '$lib/apis/auths';
 	import Terminal from '../icons/Terminal.svelte';
+	import OptionsMenu from './MessageInput/OptionsMenu.svelte';
+	import Component from '../icons/Component.svelte';
+	import PlusAlt from '../icons/PlusAlt.svelte';
 	const i18n = getContext('i18n');
 
 	export let onChange: Function = () => {};
@@ -1657,7 +1660,6 @@
 								<div class=" flex justify-between mt-0.5 mb-2.5 mx-0.5 max-w-full" dir="ltr">
 									<div class="ml-1 self-end flex items-center flex-1 max-w-[80%]">
 										<InputMenu
-											bind:selectedToolIds
 											selectedModels={atSelectedModel ? [atSelectedModel.id] : selectedModels}
 											{fileUploadCapableModels}
 											{screenCaptureHandler}
@@ -1708,66 +1710,76 @@
 											}}
 										>
 											<div
-												class="bg-transparent hover:bg-gray-100 text-gray-800 dark:text-white dark:hover:bg-gray-800 rounded-full p-1.5 outline-hidden focus:outline-hidden"
+												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"
 											>
-												<svg
-													xmlns="http://www.w3.org/2000/svg"
-													viewBox="0 0 20 20"
-													aria-hidden="true"
-													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>
+												<PlusAlt className="size-5.5" />
 											</div>
 										</InputMenu>
 
-										{#if $_user && (showToolsButton || (toggleFilters && toggleFilters.length > 0) || showWebSearchButton || showImageGenerationButton || showCodeInterpreterButton)}
+										<div class="flex self-center w-[1px] h-4 mx-1 bg-gray-50 dark:bg-gray-800" />
+
+										<OptionsMenu
+											selectedModels={atSelectedModel ? [atSelectedModel.id] : selectedModels}
+											{toggleFilters}
+											{showWebSearchButton}
+											{showImageGenerationButton}
+											{showCodeInterpreterButton}
+											bind:selectedToolIds
+											bind:selectedFilterIds
+											bind:webSearchEnabled
+											bind:imageGenerationEnabled
+											bind:codeInterpreterEnabled
+											onClose={async () => {
+												await tick();
+
+												const chatInput = document.getElementById('chat-input');
+												chatInput?.focus();
+											}}
+										>
 											<div
-												class="flex self-center w-[1px] h-4 mx-1.5 bg-gray-50 dark:bg-gray-800"
-											/>
-
-											<div class="flex gap-1 items-center overflow-x-auto scrollbar-none flex-1">
-												{#if showToolsButton}
-													<Tooltip
-														content={$i18n.t('{{COUNT}} Available Tools', {
-															COUNT: toolServers.length + selectedToolIds.length
-														})}
+												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" />
+											</div>
+										</OptionsMenu>
+
+										<div class="ml-1 flex gap-1.5">
+											{#if showToolsButton}
+												<Tooltip
+													content={$i18n.t('{{COUNT}} Available Tools', {
+														COUNT: toolServers.length + selectedToolIds.length
+													})}
+												>
+													<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"
+														aria-label="Available Tools"
+														type="button"
+														on:click={() => {
+															showTools = !showTools;
+														}}
 													>
-														<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"
-															aria-label="Available Tools"
-															type="button"
-															on:click={() => {
-																showTools = !showTools;
-															}}
-														>
-															<Wrench className="size-4" strokeWidth="1.75" />
+														<Wrench className="size-4" strokeWidth="1.75" />
 
-															<span class="text-sm font-medium text-gray-600 dark:text-gray-300">
-																{toolServers.length + selectedToolIds.length}
-															</span>
-														</button>
-													</Tooltip>
-												{/if}
+														<span class="text-sm font-medium text-gray-600 dark:text-gray-300">
+															{toolServers.length + selectedToolIds.length}
+														</span>
+													</button>
+												</Tooltip>
+											{/if}
 
-												{#each toggleFilters as filter, filterIdx (filter.id)}
+											{#each selectedFilterIds as filterId}
+												{@const filter = toggleFilters.find((f) => f.id === filterId)}
+												{#if filter}
 													<Tooltip content={filter?.description} placement="top">
 														<button
 															on:click|preventDefault={() => {
-																if (selectedFilterIds.includes(filter.id)) {
-																	selectedFilterIds = selectedFilterIds.filter(
-																		(id) => id !== filter.id
-																	);
-																} else {
-																	selectedFilterIds = [...selectedFilterIds, filter.id];
-																}
+																selectedFilterIds = selectedFilterIds.filter(
+																	(id) => id !== filterId
+																);
 															}}
 															type="button"
-															class="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(
-																filter.id
+															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(
+																filterId
 															)
 																? 'text-sky-500 dark:text-sky-300 bg-sky-50 dark:bg-sky-200/5'
 																: 'bg-transparent text-gray-600 dark:text-gray-300  '} capitalize"
@@ -1790,75 +1802,92 @@
 																class="hidden @xl:block whitespace-nowrap text-ellipsis leading-none normal-case pr-0.5"
 																>{filter?.name}</span
 															>
-														</button>
-													</Tooltip>
-												{/each}
 
-												{#if showWebSearchButton}
-													<Tooltip content={$i18n.t('Search the internet')} placement="top">
-														<button
-															on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)}
-															type="button"
-															class="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 ||
-															($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>
 														</button>
 													</Tooltip>
 												{/if}
+											{/each}
 
-												{#if showImageGenerationButton}
-													<Tooltip content={$i18n.t('Generate an image')} placement="top">
-														<button
-															on:click|preventDefault={() =>
-																(imageGenerationEnabled = !imageGenerationEnabled)}
-															type="button"
-															class="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
-																? ' text-sky-500 dark:text-sky-300 bg-sky-50 dark:bg-sky-200/5'
-																: 'bg-transparent text-gray-600 dark:text-gray-300 '}"
+											{#if webSearchEnabled}
+												<Tooltip content={$i18n.t('Search the internet')} 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 ||
+														($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
 														>
-															<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
-															>
-														</button>
-													</Tooltip>
-												{/if}
 
-												{#if showCodeInterpreterButton}
-													<Tooltip content={$i18n.t('Execute code for analysis')} placement="top">
-														<button
-															aria-label={codeInterpreterEnabled
-																? $i18n.t('Disable Code Interpreter')
-																: $i18n.t('Enable Code Interpreter')}
-															aria-pressed={codeInterpreterEnabled}
-															on:click|preventDefault={() =>
-																(codeInterpreterEnabled = !codeInterpreterEnabled)}
-															type="button"
-															class="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
-																? ' 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)
-																? 'm-1'
-																: 'focus:outline-hidden rounded-full'}"
+														<div class="hidden group-hover:block">
+															<XMark className="size-4" strokeWidth="1.75" />
+														</div>
+													</button>
+												</Tooltip>
+											{/if}
+
+											{#if imageGenerationEnabled}
+												<Tooltip content={$i18n.t('Generate an 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
+															? ' 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
 														>
-															<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
-															>
-														</button>
-													</Tooltip>
-												{/if}
-											</div>
-										{/if}
+
+														<div class="hidden group-hover:block">
+															<XMark className="size-4" strokeWidth="1.75" />
+														</div>
+													</button>
+												</Tooltip>
+											{/if}
+
+											{#if codeInterpreterEnabled}
+												<Tooltip content={$i18n.t('Execute code for analysis')} placement="top">
+													<button
+														aria-label={codeInterpreterEnabled
+															? $i18n.t('Disable Code Interpreter')
+															: $i18n.t('Enable Code Interpreter')}
+														aria-pressed={codeInterpreterEnabled}
+														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
+															? ' 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)
+															? 'm-1'
+															: '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" />
+														</div>
+													</button>
+												</Tooltip>
+											{/if}
+										</div>
 									</div>
 
 									<div class="self-end flex space-x-1 mr-1 shrink-0">

+ 78 - 114
src/lib/components/chat/MessageInput/InputMenu.svelte

@@ -10,14 +10,10 @@
 
 	import Dropdown from '$lib/components/common/Dropdown.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
-	import DocumentArrowUpSolid from '$lib/components/icons/DocumentArrowUpSolid.svelte';
-	import Switch from '$lib/components/common/Switch.svelte';
-	import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte';
-	import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
-	import CameraSolid from '$lib/components/icons/CameraSolid.svelte';
-	import PhotoSolid from '$lib/components/icons/PhotoSolid.svelte';
-	import CommandLineSolid from '$lib/components/icons/CommandLineSolid.svelte';
-	import Spinner from '$lib/components/common/Spinner.svelte';
+	import DocumentArrowUp from '$lib/components/icons/DocumentArrowUp.svelte';
+	import Camera from '$lib/components/icons/Camera.svelte';
+	import Note from '$lib/components/icons/Note.svelte';
+	import Clip from '$lib/components/icons/Clip.svelte';
 
 	const i18n = getContext('i18n');
 
@@ -35,34 +31,13 @@
 
 	export let onClose: Function;
 
-	let tools = null;
 	let show = false;
-	let showAllTools = false;
-
-	$: if (show) {
-		init();
-	}
 
 	let fileUploadEnabled = true;
 	$: fileUploadEnabled =
 		fileUploadCapableModels.length === selectedModels.length &&
 		($user?.role === 'admin' || $user?.permissions?.chat?.file_upload);
 
-	const init = async () => {
-		await _tools.set(await getTools(localStorage.token));
-		if ($_tools) {
-			tools = $_tools.reduce((a, tool, i, arr) => {
-				a[tool.id] = {
-					name: tool.name,
-					description: tool.meta.description,
-					enabled: selectedToolIds.includes(tool.id)
-				};
-				return a;
-			}, {});
-			selectedToolIds = selectedToolIds.filter((id) => $_tools?.some((tool) => tool.id === id));
-		}
-	};
-
 	const detectMobile = () => {
 		const userAgent = navigator.userAgent || navigator.vendor || window.opera;
 		return /android|iphone|ipad|ipod|windows phone/i.test(userAgent);
@@ -101,86 +76,67 @@
 
 	<div slot="content">
 		<DropdownMenu.Content
-			class="w-full max-w-[240px] rounded-xl px-1 py-1 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
-			sideOffset={10}
-			alignOffset={-8}
-			side="top"
+			class="w-full max-w-[200px] 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"
+			sideOffset={4}
+			alignOffset={-6}
+			side="bottom"
 			align="start"
 			transition={flyAndScale}
 		>
-			{#if tools}
-				{#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}
-							<button
-								class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
-								on:click={() => {
-									tools[toolId].enabled = !tools[toolId].enabled;
-								}}
-							>
-								<div class="flex-1 truncate">
-									<Tooltip
-										content={tools[toolId]?.description ?? ''}
-										placement="top-start"
-										className="flex flex-1 gap-2 items-center"
-									>
-										<div class="shrink-0">
-											<WrenchSolid />
-										</div>
+			<Tooltip
+				content={fileUploadCapableModels.length !== selectedModels.length
+					? $i18n.t('Model(s) do not support file upload')
+					: !fileUploadEnabled
+						? $i18n.t('You do not have permission to upload files.')
+						: ''}
+				className="w-full"
+			>
+				<DropdownMenu.Item
+					class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
+						? 'opacity-50'
+						: ''}"
+					on:click={() => {
+						if (fileUploadEnabled) {
+							uploadFilesHandler();
+						}
+					}}
+				>
+					<Clip />
 
-										<div class=" truncate">{tools[toolId].name}</div>
-									</Tooltip>
-								</div>
+					<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
+				</DropdownMenu.Item>
+			</Tooltip>
 
-								<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}
-					</div>
-					{#if Object.keys(tools).length > 3}
-						<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"
-							on:click={() => {
-								showAllTools = !showAllTools;
-							}}
-							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>
-						</button>
-					{/if}
-					<hr class="border-black/5 dark:border-white/5 my-1" />
-				{/if}
-			{:else}
-				<div class="py-4">
-					<Spinner />
-				</div>
+			<Tooltip
+				content={fileUploadCapableModels.length !== selectedModels.length
+					? $i18n.t('Model(s) do not support file upload')
+					: !fileUploadEnabled
+						? $i18n.t('You do not have permission to upload files.')
+						: ''}
+				className="w-full"
+			>
+				<DropdownMenu.Item
+					class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800  rounded-xl {!fileUploadEnabled
+						? 'opacity-50'
+						: ''}"
+					on:click={() => {
+						if (fileUploadEnabled) {
+							if (!detectMobile()) {
+								screenCaptureHandler();
+							} else {
+								const cameraInputElement = document.getElementById('camera-input');
 
-				<hr class="border-black/5 dark:border-white/5 my-1" />
-			{/if}
+								if (cameraInputElement) {
+									cameraInputElement.click();
+								}
+							}
+						}
+					}}
+				>
+					<Camera />
+					<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
+				</DropdownMenu.Item>
+			</Tooltip>
 
 			<Tooltip
 				content={fileUploadCapableModels.length !== selectedModels.length
@@ -191,7 +147,7 @@
 				className="w-full"
 			>
 				<DropdownMenu.Item
-					class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800  rounded-xl {!fileUploadEnabled
+					class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800  rounded-xl {!fileUploadEnabled
 						? 'opacity-50'
 						: ''}"
 					on:click={() => {
@@ -208,8 +164,8 @@
 						}
 					}}
 				>
-					<CameraSolid />
-					<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
+					<Note />
+					<div class=" line-clamp-1">{$i18n.t('Attach Notes')}</div>
 				</DropdownMenu.Item>
 			</Tooltip>
 
@@ -222,24 +178,32 @@
 				className="w-full"
 			>
 				<DropdownMenu.Item
-					class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
+					class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800  rounded-xl {!fileUploadEnabled
 						? 'opacity-50'
 						: ''}"
 					on:click={() => {
 						if (fileUploadEnabled) {
-							uploadFilesHandler();
+							if (!detectMobile()) {
+								screenCaptureHandler();
+							} else {
+								const cameraInputElement = document.getElementById('camera-input');
+
+								if (cameraInputElement) {
+									cameraInputElement.click();
+								}
+							}
 						}
 					}}
 				>
-					<DocumentArrowUpSolid />
-					<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
+					<DocumentArrowUp />
+					<div class=" line-clamp-1">{$i18n.t('Attach Knowledge')}</div>
 				</DropdownMenu.Item>
 			</Tooltip>
 
 			{#if fileUploadEnabled}
 				{#if $config?.features?.enable_google_drive_integration}
 					<DropdownMenu.Item
-						class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
+						class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
 						on:click={() => {
 							uploadGoogleDriveHandler();
 						}}
@@ -277,7 +241,7 @@
 				{#if $config?.features?.enable_onedrive_integration}
 					<DropdownMenu.Sub>
 						<DropdownMenu.SubTrigger
-							class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full"
+							class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full"
 						>
 							<svg
 								xmlns="http://www.w3.org/2000/svg"
@@ -373,7 +337,7 @@
 							alignOffset={$mobile ? 0 : -8}
 						>
 							<DropdownMenu.Item
-								class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
+								class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
 								on:click={() => {
 									uploadOneDriveHandler('personal');
 								}}
@@ -381,7 +345,7 @@
 								<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (personal)')}</div>
 							</DropdownMenu.Item>
 							<DropdownMenu.Item
-								class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
+								class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
 								on:click={() => {
 									uploadOneDriveHandler('organizations');
 								}}

+ 309 - 0
src/lib/components/chat/MessageInput/OptionsMenu.svelte

@@ -0,0 +1,309 @@
+<script lang="ts">
+	import { DropdownMenu } from 'bits-ui';
+	import { flyAndScale } from '$lib/utils/transitions';
+	import { getContext, onMount, tick } from 'svelte';
+
+	import { config, user, tools as _tools, mobile, settings } from '$lib/stores';
+
+	import { getTools } from '$lib/apis/tools';
+
+	import Dropdown from '$lib/components/common/Dropdown.svelte';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import Switch from '$lib/components/common/Switch.svelte';
+	import Spinner from '$lib/components/common/Spinner.svelte';
+	import Wrench from '$lib/components/icons/Wrench.svelte';
+	import Sparkles from '$lib/components/icons/Sparkles.svelte';
+	import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
+	import Photo from '$lib/components/icons/Photo.svelte';
+	import Terminal from '$lib/components/icons/Terminal.svelte';
+
+	const i18n = getContext('i18n');
+
+	export let selectedToolIds: string[] = [];
+
+	export let selectedModels: string[] = [];
+	export let fileUploadCapableModels: string[] = [];
+
+	export let toggleFilters: { id: string; name: string; description?: string; icon?: string }[] =
+		[];
+	export let selectedFilterIds: string[] = [];
+
+	export let showWebSearchButton = false;
+	export let webSearchEnabled = false;
+	export let showImageGenerationButton = false;
+	export let imageGenerationEnabled = false;
+	export let showCodeInterpreterButton = false;
+	export let codeInterpreterEnabled = false;
+
+	export let onClose: Function;
+
+	let tools = null;
+	let show = false;
+	let showAllTools = false;
+
+	$: if (show) {
+		init();
+	}
+
+	let fileUploadEnabled = true;
+	$: fileUploadEnabled =
+		fileUploadCapableModels.length === selectedModels.length &&
+		($user?.role === 'admin' || $user?.permissions?.chat?.file_upload);
+
+	const init = async () => {
+		await _tools.set(await getTools(localStorage.token));
+		if ($_tools) {
+			tools = $_tools.reduce((a, tool, i, arr) => {
+				a[tool.id] = {
+					name: tool.name,
+					description: tool.meta.description,
+					enabled: selectedToolIds.includes(tool.id)
+				};
+				return a;
+			}, {});
+			selectedToolIds = selectedToolIds.filter((id) => $_tools?.some((tool) => tool.id === id));
+		}
+	};
+</script>
+
+<Dropdown
+	bind:show
+	on:change={(e) => {
+		if (e.detail === false) {
+			onClose();
+		}
+	}}
+>
+	<Tooltip content={$i18n.t('Integrations')} placement="top">
+		<slot />
+	</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-850 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg transition"
+			sideOffset={4}
+			alignOffset={-6}
+			side="bottom"
+			align="start"
+			transition={flyAndScale}
+		>
+			{#if toggleFilters && toggleFilters.length > 0}
+				{#each toggleFilters 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>
+
+									<div class=" truncate">{filter?.name}</div>
+								</div>
+							</div>
+
+							<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-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}
+
+			{#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>
+						</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 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')}
+						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}
+
+			{#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}
+							<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={() => {
+									tools[toolId].enabled = !tools[toolId].enabled;
+								}}
+							>
+								<div class="flex-1 truncate">
+									<Tooltip
+										content={tools[toolId]?.description ?? ''}
+										placement="top-start"
+										className="flex flex-1 gap-2 items-center"
+									>
+										<div class="shrink-0">
+											<Wrench />
+										</div>
+
+										<div class=" truncate">{tools[toolId].name}</div>
+									</Tooltip>
+								</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}
+					</div>
+					{#if Object.keys(tools).length > 3}
+						<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"
+							on:click={() => {
+								showAllTools = !showAllTools;
+							}}
+							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>
+						</button>
+					{/if}
+				{/if}
+			{:else}
+				<div class="py-4">
+					<Spinner />
+				</div>
+			{/if}
+		</DropdownMenu.Content>
+	</div>
+</Dropdown>

+ 22 - 0
src/lib/components/icons/Camera.svelte

@@ -0,0 +1,22 @@
+<script lang="ts">
+	export let className = 'size-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	fill="none"
+	viewBox="0 0 24 24"
+	stroke-width={strokeWidth}
+	stroke="currentColor"
+	class={className}
+	><path
+		d="M2 19V9C2 7.89543 2.89543 7 4 7H4.5C5.12951 7 5.72229 6.70361 6.1 6.2L8.32 3.24C8.43331 3.08892 8.61115 3 8.8 3H15.2C15.3889 3 15.5667 3.08892 15.68 3.24L17.9 6.2C18.2777 6.70361 18.8705 7 19.5 7H20C21.1046 7 22 7.89543 22 9V19C22 20.1046 21.1046 21 20 21H4C2.89543 21 2 20.1046 2 19Z"
+		stroke-linecap="round"
+		stroke-linejoin="round"
+	></path><path
+		d="M12 17C14.2091 17 16 15.2091 16 13C16 10.7909 14.2091 9 12 9C9.79086 9 8 10.7909 8 13C8 15.2091 9.79086 17 12 17Z"
+		stroke-linecap="round"
+		stroke-linejoin="round"
+	></path></svg
+>

+ 18 - 0
src/lib/components/icons/Clip.svelte

@@ -0,0 +1,18 @@
+<script lang="ts">
+	export let className = 'size-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	fill="none"
+	viewBox="0 0 24 24"
+	stroke-width={strokeWidth}
+	stroke="currentColor"
+	class={className}
+	><path
+		d="M21.4383 11.6622L12.2483 20.8522C11.1225 21.9781 9.59552 22.6106 8.00334 22.6106C6.41115 22.6106 4.88418 21.9781 3.75834 20.8522C2.63249 19.7264 2 18.1994 2 16.6072C2 15.015 2.63249 13.4881 3.75834 12.3622L12.9483 3.17222C13.6989 2.42166 14.7169 2 15.7783 2C16.8398 2 17.8578 2.42166 18.6083 3.17222C19.3589 3.92279 19.7806 4.94077 19.7806 6.00222C19.7806 7.06368 19.3589 8.08166 18.6083 8.83222L9.40834 18.0222C9.03306 18.3975 8.52406 18.6083 7.99334 18.6083C7.46261 18.6083 6.95362 18.3975 6.57834 18.0222C6.20306 17.6469 5.99222 17.138 5.99222 16.6072C5.99222 16.0765 6.20306 15.5675 6.57834 15.1922L15.0683 6.71222"
+		stroke-linecap="round"
+		stroke-linejoin="round"
+	></path></svg
+>

+ 22 - 0
src/lib/components/icons/Component.svelte

@@ -0,0 +1,22 @@
+<script lang="ts">
+	export let className = 'size-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	fill="none"
+	viewBox="0 0 24 24"
+	stroke-width={strokeWidth}
+	stroke="currentColor"
+	class={className}
+	><path
+		d="M5.21173 15.1113L2.52473 12.4243C2.29041 12.1899 2.29041 11.8101 2.52473 11.5757L5.21173 8.88873C5.44605 8.65442 5.82595 8.65442 6.06026 8.88873L8.74727 11.5757C8.98158 11.8101 8.98158 12.1899 8.74727 12.4243L6.06026 15.1113C5.82595 15.3456 5.44605 15.3456 5.21173 15.1113Z"
+	></path><path
+		d="M11.5757 21.475L8.88874 18.788C8.65443 18.5537 8.65443 18.1738 8.88874 17.9395L11.5757 15.2525C11.8101 15.0182 12.19 15.0182 12.4243 15.2525L15.1113 17.9395C15.3456 18.1738 15.3456 18.5537 15.1113 18.788L12.4243 21.475C12.19 21.7094 11.8101 21.7094 11.5757 21.475Z"
+	></path><path
+		d="M11.5757 8.7475L8.88874 6.06049C8.65443 5.82618 8.65443 5.44628 8.88874 5.21197L11.5757 2.52496C11.8101 2.29065 12.19 2.29065 12.4243 2.52496L15.1113 5.21197C15.3456 5.44628 15.3456 5.82618 15.1113 6.06049L12.4243 8.7475C12.19 8.98181 11.8101 8.98181 11.5757 8.7475Z"
+	></path><path
+		d="M17.9396 15.1113L15.2526 12.4243C15.0183 12.1899 15.0183 11.8101 15.2526 11.5757L17.9396 8.88873C18.174 8.65442 18.5539 8.65442 18.7882 8.88873L21.4752 11.5757C21.7095 11.8101 21.7095 12.1899 21.4752 12.4243L18.7882 15.1113C18.5539 15.3456 18.174 15.3456 17.9396 15.1113Z"
+	></path></svg
+>

+ 22 - 0
src/lib/components/icons/Grid.svelte

@@ -0,0 +1,22 @@
+<script lang="ts">
+	export let className = 'size-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	fill="none"
+	viewBox="0 0 24 24"
+	stroke-width={strokeWidth}
+	stroke="currentColor"
+	class={className}
+	><path
+		d="M14 20.4V14.6C14 14.2686 14.2686 14 14.6 14H20.4C20.7314 14 21 14.2686 21 14.6V20.4C21 20.7314 20.7314 21 20.4 21H14.6C14.2686 21 14 20.7314 14 20.4Z"
+	></path><path
+		d="M3 20.4V14.6C3 14.2686 3.26863 14 3.6 14H9.4C9.73137 14 10 14.2686 10 14.6V20.4C10 20.7314 9.73137 21 9.4 21H3.6C3.26863 21 3 20.7314 3 20.4Z"
+	></path><path
+		d="M14 9.4V3.6C14 3.26863 14.2686 3 14.6 3H20.4C20.7314 3 21 3.26863 21 3.6V9.4C21 9.73137 20.7314 10 20.4 10H14.6C14.2686 10 14 9.73137 14 9.4Z"
+	></path><path
+		d="M3 9.4V3.6C3 3.26863 3.26863 3 3.6 3H9.4C9.73137 3 10 3.26863 10 3.6V9.4C10 9.73137 9.73137 10 9.4 10H3.6C3.26863 10 3 9.73137 3 9.4Z"
+	></path></svg
+>

+ 15 - 0
src/lib/components/icons/PlusAlt.svelte

@@ -0,0 +1,15 @@
+<script lang="ts">
+	export let className = 'size-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	fill="none"
+	viewBox="0 0 24 24"
+	stroke-width={strokeWidth}
+	stroke="currentColor"
+	class={className}
+	><path d="M6 12H12M18 12H12M12 12V6M12 12V18" stroke-linecap="round" stroke-linejoin="round"
+	></path></svg
+>

+ 22 - 0
src/lib/components/icons/Union.svelte

@@ -0,0 +1,22 @@
+<script lang="ts">
+	export let className = 'size-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	fill="none"
+	viewBox="0 0 24 24"
+	stroke-width={strokeWidth}
+	stroke="currentColor"
+	class={className}
+	><path
+		d="M9 22C12.866 22 16 18.866 16 15C16 11.134 12.866 8 9 8C5.13401 8 2 11.134 2 15C2 18.866 5.13401 22 9 22Z"
+		stroke-linecap="round"
+		stroke-linejoin="round"
+	></path><path
+		d="M15 16C18.866 16 22 12.866 22 9C22 5.13401 18.866 2 15 2C11.134 2 8 5.13401 8 9C8 12.866 11.134 16 15 16Z"
+		stroke-linecap="round"
+		stroke-linejoin="round"
+	></path></svg
+>