2 import { page } from '$app/state';
3 import { Plus, MessageSquare, Settings, Zap, FolderOpen } from '@lucide/svelte';
4 import { Button } from '$lib/components/ui/button';
5 import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
6 import * as Tooltip from '$lib/components/ui/tooltip';
7 import { Switch } from '$lib/components/ui/switch';
8 import { FILE_TYPE_ICONS, TOOLTIP_DELAY_DURATION } from '$lib/constants';
9 import { McpLogo, DropdownMenuSearchable } from '$lib/components/app';
10 import { conversationsStore } from '$lib/stores/conversations.svelte';
11 import { mcpStore } from '$lib/stores/mcp.svelte';
13 import { HealthCheckStatus } from '$lib/enums';
14 import type { MCPServerSettingsEntry } from '$lib/types';
19 hasAudioModality?: boolean;
20 hasVisionModality?: boolean;
21 hasMcpPromptsSupport?: boolean;
22 hasMcpResourcesSupport?: boolean;
23 onFileUpload?: () => void;
24 onSystemPromptClick?: () => void;
25 onMcpPromptClick?: () => void;
26 onMcpSettingsClick?: () => void;
27 onMcpResourcesClick?: () => void;
31 class: className = '',
33 hasAudioModality = false,
34 hasVisionModality = false,
35 hasMcpPromptsSupport = false,
36 hasMcpResourcesSupport = false,
44 let isNewChat = $derived(!page.params.id);
46 let systemMessageTooltip = $derived(
48 ? 'Add custom system message for a new conversation'
49 : 'Inject custom system message at the beginning of the conversation'
52 let dropdownOpen = $state(false);
54 let mcpServers = $derived(mcpStore.getServersSorted().filter((s) => s.enabled));
55 let hasMcpServers = $derived(mcpServers.length > 0);
56 let mcpSearchQuery = $state('');
57 let filteredMcpServers = $derived.by(() => {
58 const query = mcpSearchQuery.toLowerCase().trim();
59 if (!query) return mcpServers;
60 return mcpServers.filter((s) => {
61 const name = getServerLabel(s).toLowerCase();
62 const url = s.url.toLowerCase();
63 return name.includes(query) || url.includes(query);
67 function getServerLabel(server: MCPServerSettingsEntry): string {
68 return mcpStore.getServerLabel(server);
71 function isServerEnabledForChat(serverId: string): boolean {
72 return conversationsStore.isMcpServerEnabledForChat(serverId);
75 async function toggleServerForChat(serverId: string) {
76 await conversationsStore.toggleMcpServerForChat(serverId);
79 function handleMcpSubMenuOpen(open: boolean) {
82 mcpStore.runHealthChecksForServers(mcpServers);
86 function handleMcpPromptClick() {
91 function handleMcpSettingsClick() {
93 onMcpSettingsClick?.();
96 function handleMcpResourcesClick() {
98 onMcpResourcesClick?.();
101 const fileUploadTooltipText = 'Add files, system prompt or MCP Servers';
104 <div class="flex items-center gap-1 {className}">
105 <DropdownMenu.Root bind:open={dropdownOpen}>
106 <DropdownMenu.Trigger name="Attach files" {disabled}>
108 <Tooltip.Trigger class="w-full">
110 class="file-upload-button h-8 w-8 rounded-full p-0"
115 <span class="sr-only">{fileUploadTooltipText}</span>
117 <Plus class="h-4 w-4" />
122 <p>{fileUploadTooltipText}</p>
125 </DropdownMenu.Trigger>
127 <DropdownMenu.Content align="start" class="w-48">
128 {#if hasVisionModality}
130 class="images-button flex cursor-pointer items-center gap-2"
131 onclick={() => onFileUpload?.()}
133 <FILE_TYPE_ICONS.image class="h-4 w-4" />
138 <Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
139 <Tooltip.Trigger class="w-full">
141 class="images-button flex cursor-pointer items-center gap-2"
144 <FILE_TYPE_ICONS.image class="h-4 w-4" />
150 <Tooltip.Content side="right">
151 <p>Images require vision models to be processed</p>
156 {#if hasAudioModality}
158 class="audio-button flex cursor-pointer items-center gap-2"
159 onclick={() => onFileUpload?.()}
161 <FILE_TYPE_ICONS.audio class="h-4 w-4" />
163 <span>Audio Files</span>
166 <Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
167 <Tooltip.Trigger class="w-full">
168 <DropdownMenu.Item class="audio-button flex cursor-pointer items-center gap-2" disabled>
169 <FILE_TYPE_ICONS.audio class="h-4 w-4" />
171 <span>Audio Files</span>
175 <Tooltip.Content side="right">
176 <p>Audio files require audio models to be processed</p>
182 class="flex cursor-pointer items-center gap-2"
183 onclick={() => onFileUpload?.()}
185 <FILE_TYPE_ICONS.text class="h-4 w-4" />
187 <span>Text Files</span>
190 {#if hasVisionModality}
192 class="flex cursor-pointer items-center gap-2"
193 onclick={() => onFileUpload?.()}
195 <FILE_TYPE_ICONS.pdf class="h-4 w-4" />
197 <span>PDF Files</span>
200 <Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
201 <Tooltip.Trigger class="w-full">
203 class="flex cursor-pointer items-center gap-2"
204 onclick={() => onFileUpload?.()}
206 <FILE_TYPE_ICONS.pdf class="h-4 w-4" />
208 <span>PDF Files</span>
212 <Tooltip.Content side="right">
213 <p>PDFs will be converted to text. Image-based PDFs may not work properly.</p>
218 <Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
219 <Tooltip.Trigger class="w-full">
221 class="flex cursor-pointer items-center gap-2"
222 onclick={() => onSystemPromptClick?.()}
224 <MessageSquare class="h-4 w-4" />
226 <span>System Message</span>
230 <Tooltip.Content side="right">
231 <p>{systemMessageTooltip}</p>
235 <DropdownMenu.Separator />
237 <DropdownMenu.Sub onOpenChange={handleMcpSubMenuOpen}>
238 <DropdownMenu.SubTrigger class="flex cursor-pointer items-center gap-2">
239 <McpLogo class="h-4 w-4" />
241 <span>MCP Servers</span>
242 </DropdownMenu.SubTrigger>
244 <DropdownMenu.SubContent class="w-72 pt-0">
245 <DropdownMenuSearchable
246 placeholder="Search servers..."
247 bind:searchValue={mcpSearchQuery}
248 emptyMessage={hasMcpServers ? 'No servers found' : 'No MCP servers configured'}
249 isEmpty={filteredMcpServers.length === 0}
251 <div class="max-h-64 overflow-y-auto">
252 {#each filteredMcpServers as server (server.id)}
253 {@const healthState = mcpStore.getHealthCheckState(server.id)}
254 {@const hasError = healthState.status === HealthCheckStatus.ERROR}
255 {@const isEnabledForChat = isServerEnabledForChat(server.id)}
259 class="flex w-full items-center justify-between gap-2 rounded-sm px-2 py-2 text-left transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
260 onclick={() => !hasError && toggleServerForChat(server.id)}
263 <div class="flex min-w-0 flex-1 items-center gap-2">
264 {#if mcpStore.getServerFavicon(server.id)}
266 src={mcpStore.getServerFavicon(server.id)}
268 class="h-4 w-4 shrink-0 rounded-sm"
270 (e.currentTarget as HTMLImageElement).style.display = 'none';
275 <span class="truncate text-sm">{getServerLabel(server)}</span>
279 class="shrink-0 rounded bg-destructive/15 px-1.5 py-0.5 text-xs text-destructive"
287 checked={isEnabledForChat}
289 onclick={(e: MouseEvent) => e.stopPropagation()}
290 onCheckedChange={() => toggleServerForChat(server.id)}
298 class="flex cursor-pointer items-center gap-2"
299 onclick={handleMcpSettingsClick}
301 <Settings class="h-4 w-4" />
303 <span>Manage MCP Servers</span>
306 </DropdownMenuSearchable>
307 </DropdownMenu.SubContent>
310 {#if hasMcpPromptsSupport}
312 class="flex cursor-pointer items-center gap-2"
313 onclick={handleMcpPromptClick}
315 <Zap class="h-4 w-4" />
317 <span>MCP Prompt</span>
321 {#if hasMcpResourcesSupport}
323 class="flex cursor-pointer items-center gap-2"
324 onclick={handleMcpResourcesClick}
326 <FolderOpen class="h-4 w-4" />
328 <span>MCP Resources</span>
331 </DropdownMenu.Content>