open-webui/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte

329 lines
10 KiB
Svelte
Raw Normal View History

2025-09-12 11:05:37 +00:00
<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';
2025-09-12 11:41:12 +00:00
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
2025-09-14 07:08:23 +00:00
import { crossfade, fade, fly, slide } from 'svelte/transition';
2025-09-12 11:05:37 +00:00
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 show = false;
2025-09-14 07:08:23 +00:00
let tab = '';
let tools = null;
2025-09-12 11:05:37 +00:00
$: 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
2025-09-14 07:08:23 +00:00
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"
2025-09-12 11:05:37 +00:00
sideOffset={4}
alignOffset={-6}
side="bottom"
align="start"
transition={flyAndScale}
>
2025-09-14 07:08:23 +00:00
{#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 />
2025-09-12 11:57:04 +00:00
2025-09-14 07:08:23 +00:00
<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>
2025-09-12 11:57:04 +00:00
2025-09-14 07:08:23 +00:00
<div class="text-gray-500">
<ChevronRight />
</div>
2025-09-12 11:57:04 +00:00
</div>
2025-09-14 07:08:23 +00:00
</button>
2025-09-12 11:57:04 +00:00
{/if}
2025-09-14 07:08:23 +00:00
{:else}
<div class="py-4">
<Spinner />
</div>
{/if}
2025-09-12 11:05:37 +00:00
2025-09-14 07:08:23 +00:00
{#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>
2025-09-12 22:45:15 +00:00
</div>
2025-09-12 11:05:37 +00:00
2025-09-14 07:08:23 +00:00
<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">
2025-09-12 11:05:37 +00:00
<button
2025-09-12 11:54:42 +00:00
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"
2025-09-12 11:05:37 +00:00
on:click={() => {
2025-09-14 07:08:23 +00:00
webSearchEnabled = !webSearchEnabled;
2025-09-12 11:05:37 +00:00
}}
>
<div class="flex-1 truncate">
2025-09-12 11:41:12 +00:00
<div class="flex flex-1 gap-2 items-center">
2025-09-12 11:05:37 +00:00
<div class="shrink-0">
2025-09-14 07:08:23 +00:00
<GlobeAlt />
2025-09-12 11:05:37 +00:00
</div>
2025-09-14 07:08:23 +00:00
<div class=" truncate">{$i18n.t('Web Search')}</div>
2025-09-12 11:41:12 +00:00
</div>
2025-09-12 11:05:37 +00:00
</div>
<div class=" shrink-0">
<Switch
2025-09-14 07:08:23 +00:00
state={webSearchEnabled}
2025-09-12 11:05:37 +00:00
on:change={async (e) => {
const state = e.detail;
await tick();
}}
/>
</div>
</button>
2025-09-12 11:41:12 +00:00
</Tooltip>
2025-09-14 07:08:23 +00:00
{/if}
2025-09-12 11:41:12 +00:00
2025-09-14 07:08:23 +00:00
{#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>
2025-09-12 11:41:12 +00:00
</div>
2025-09-14 07:08:23 +00:00
</div>
2025-09-12 11:41:12 +00:00
2025-09-14 07:08:23 +00:00
<div class=" shrink-0">
<Switch
state={imageGenerationEnabled}
on:change={async (e) => {
const state = e.detail;
await tick();
}}
/>
2025-09-12 11:41:12 +00:00
</div>
2025-09-14 07:08:23 +00:00
</button>
</Tooltip>
{/if}
2025-09-12 11:41:12 +00:00
2025-09-14 07:08:23 +00:00
{#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>
2025-09-12 11:41:12 +00:00
2025-09-14 07:08:23 +00:00
<div class=" truncate">{$i18n.t('Code Interpreter')}</div>
2025-09-12 11:41:12 +00:00
</div>
2025-09-14 07:08:23 +00:00
</div>
2025-09-12 11:41:12 +00:00
2025-09-14 07:08:23 +00:00
<div class=" shrink-0">
<Switch
state={codeInterpreterEnabled}
on:change={async (e) => {
const state = e.detail;
await tick();
}}
/>
2025-09-12 11:41:12 +00:00
</div>
2025-09-14 07:08:23 +00:00
</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 />
2025-09-12 11:41:12 +00:00
2025-09-14 07:08:23 +00:00
<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>
2025-09-12 11:41:12 +00:00
</div>
2025-09-14 07:08:23 +00:00
</div>
</button>
2025-09-12 11:41:12 +00:00
2025-09-14 07:08:23 +00:00
{#each Object.keys(tools) as toolId}
2025-09-12 11:41:12 +00:00
<button
2025-09-12 11:54:42 +00:00
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"
2025-09-12 11:41:12 +00:00
on:click={() => {
2025-09-14 07:08:23 +00:00
tools[toolId].enabled = !tools[toolId].enabled;
2025-09-12 11:41:12 +00:00
}}
>
<div class="flex-1 truncate">
<div class="flex flex-1 gap-2 items-center">
2025-09-14 07:08:23 +00:00
<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>
2025-09-12 11:41:12 +00:00
</div>
</div>
<div class=" shrink-0">
<Switch
2025-09-14 07:08:23 +00:00
state={tools[toolId].enabled}
2025-09-12 11:41:12 +00:00
on:change={async (e) => {
const state = e.detail;
await tick();
2025-09-14 07:08:23 +00:00
if (state) {
selectedToolIds = [...selectedToolIds, toolId];
} else {
selectedToolIds = selectedToolIds.filter((id) => id !== toolId);
}
2025-09-12 11:41:12 +00:00
}}
/>
</div>
</button>
2025-09-14 07:08:23 +00:00
{/each}
</div>
2025-09-12 11:05:37 +00:00
{/if}
</DropdownMenu.Content>
</div>
</Dropdown>