refac: input menu

This commit is contained in:
Timothy Jaeryang Baek
2025-09-12 15:05:37 +04:00
parent fe7b8d9617
commit a68342d5a8
9 changed files with 646 additions and 223 deletions
+137 -108
View File
@@ -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"
/>
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="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
})}
<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">
@@ -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,36 @@
<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=" 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>
<hr class="border-black/5 dark:border-white/5 my-1" />
{/if}
<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
</DropdownMenu.Item>
</Tooltip>
<Tooltip
content={fileUploadCapableModels.length !== selectedModels.length
@@ -191,7 +116,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,7 +133,7 @@
}
}}
>
<CameraSolid />
<Camera />
<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
</DropdownMenu.Item>
</Tooltip>
@@ -222,24 +147,63 @@
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>
<Note />
<div class=" line-clamp-1">{$i18n.t('Attach Notes')}</div>
</DropdownMenu.Item>
</Tooltip>
<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');
if (cameraInputElement) {
cameraInputElement.click();
}
}
}
}}
>
<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');
}}
@@ -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>