2 import type { Snippet } from 'svelte';
3 import * as Tooltip from '$lib/components/ui/tooltip';
4 import * as Sheet from '$lib/components/ui/sheet';
5 import * as Collapsible from '$lib/components/ui/collapsible';
6 import { File, MessageSquare, Zap, FolderOpen } from '@lucide/svelte';
7 import { Switch } from '$lib/components/ui/switch';
8 import { Checkbox } from '$lib/components/ui/checkbox';
9 import { TOOLTIP_DELAY_DURATION } from '$lib/constants';
10 import { ATTACHMENT_FILE_ITEMS } from '$lib/constants/attachment-menu';
11 import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
12 import { useToolsPanel } from '$lib/hooks/use-tools-panel.svelte';
13 import { conversationsStore } from '$lib/stores/conversations.svelte';
14 import { mcpStore } from '$lib/stores/mcp.svelte';
15 import { McpLogo } from '$lib/components/app';
16 import { PencilRuler, ChevronDown, ChevronRight } from '@lucide/svelte';
17 import { HealthCheckStatus } from '$lib/enums';
18 import { AttachmentAction } from '$lib/enums/attachment.enums';
23 hasAudioModality?: boolean;
24 hasVideoModality?: boolean;
25 hasVisionModality?: boolean;
26 hasMcpPromptsSupport?: boolean;
27 hasMcpResourcesSupport?: boolean;
28 onFileUpload?: () => void;
29 onSystemPromptClick?: () => void;
30 onMcpPromptClick?: () => void;
31 onMcpResourcesClick?: () => void;
32 trigger: Snippet<[{ disabled: boolean; onclick?: () => void }]>;
36 class: className = '',
38 hasAudioModality = false,
39 hasVisionModality = false,
40 hasVideoModality = false,
41 hasMcpPromptsSupport = false,
42 hasMcpResourcesSupport = false,
50 let sheetOpen = $state(false);
51 let filesExpanded = $state(true);
52 let toolsExpanded = $state(false);
53 let mcpExpanded = $state(false);
55 const attachmentMenu = useAttachmentMenu(
61 hasMcpResourcesSupport
63 () => ({ onFileUpload, onSystemPromptClick, onMcpPromptClick, onMcpResourcesClick }),
69 const toolsPanel = useToolsPanel();
71 const sheetItemClass =
72 'flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-left text-sm transition-colors hover:bg-accent active:bg-accent disabled:cursor-not-allowed disabled:opacity-50';
74 const sheetItemRowClass =
75 'flex w-full items-center justify-between gap-2 rounded-md px-3 py-2 text-left text-sm transition-colors hover:bg-accent';
77 let visibleMcpServers = $derived(mcpStore.visibleMcpServers);
80 <div class="flex items-center gap-1 {className}">
81 <Sheet.Root bind:open={sheetOpen}>
82 {@render trigger({ disabled, onclick: () => (sheetOpen = true) })}
84 <Sheet.Content side="bottom" class="max-h-[85vh] gap-0 overflow-y-auto">
86 <Sheet.Title>Add to chat</Sheet.Title>
88 <Sheet.Description class="sr-only">
89 Add files, system prompt or configure MCP servers
93 <div class="flex flex-col gap-1 px-1.5 pb-2">
94 <Collapsible.Root open={filesExpanded} onOpenChange={(open) => (filesExpanded = open)}>
95 <Collapsible.Trigger class={sheetItemClass}>
97 <ChevronDown class="h-4 w-4 shrink-0" />
99 <ChevronRight class="h-4 w-4 shrink-0" />
102 <File class="h-4 w-4 shrink-0" />
104 <span class="flex-1">Add files</span>
105 </Collapsible.Trigger>
107 <Collapsible.Content>
108 <div class="flex flex-col gap-0.5 pl-4">
109 {#each ATTACHMENT_FILE_ITEMS as item (item.id)}
110 {@const enabled = attachmentMenu.isItemEnabled(item.enabledWhen)}
114 class={sheetItemClass}
115 onclick={() => attachmentMenu.callbacks[item.action]()}
117 <item.icon class="h-4 w-4 shrink-0" />
119 <span>{item.label}</span>
121 {:else if item.disabledTooltip}
122 <Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
124 <button type="button" class={sheetItemClass} disabled>
125 <item.icon class="h-4 w-4 shrink-0" />
127 <span>{item.label}</span>
131 <Tooltip.Content side="right">
132 <p>{item.disabledTooltip}</p>
138 </Collapsible.Content>
141 <Collapsible.Root open={mcpExpanded} onOpenChange={(open) => (mcpExpanded = open)}>
142 <Collapsible.Trigger class={sheetItemClass}>
144 <ChevronDown class="h-4 w-4 shrink-0" />
146 <ChevronRight class="h-4 w-4 shrink-0" />
149 <McpLogo class="inline h-4 w-4 shrink-0" />
151 <span class="flex-1">MCP Servers</span>
153 <span class="text-xs text-muted-foreground">
154 {visibleMcpServers.length} server{visibleMcpServers.length !== 1 ? 's' : ''}
156 </Collapsible.Trigger>
158 <Collapsible.Content>
159 <div class="flex flex-col gap-0.5 pl-4">
160 {#each visibleMcpServers as server (server.id)}
161 {@const healthState = mcpStore.getHealthCheckState(server.id)}
162 {@const hasError = healthState.status === HealthCheckStatus.ERROR}
163 {@const displayName = mcpStore.getServerLabel(server)}
164 {@const faviconUrl = mcpStore.getServerFavicon(server.id)}
165 {@const isEnabled = conversationsStore.isMcpServerEnabledForChat(server.id)}
169 class={sheetItemRowClass}
170 onclick={() => !hasError && conversationsStore.toggleMcpServerForChat(server.id)}
173 <div class="flex min-w-0 flex-1 items-center gap-2">
178 class="h-4 w-4 shrink-0 rounded-sm"
180 (e.currentTarget as HTMLImageElement).style.display = 'none';
185 <span class="min-w-0 truncate text-sm">{displayName}</span>
190 class="shrink-0 rounded bg-destructive/15 px-1.5 py-0.5 text-xs text-destructive"
197 onCheckedChange={() => conversationsStore.toggleMcpServerForChat(server.id)}
203 {#if visibleMcpServers.length === 0}
204 <div class="px-3 py-2 text-center text-sm text-muted-foreground">
205 No MCP servers configured
209 </Collapsible.Content>
212 {#if toolsPanel.totalToolCount > 0}
213 <Collapsible.Root open={toolsExpanded} onOpenChange={(open) => (toolsExpanded = open)}>
214 <Collapsible.Trigger class={sheetItemClass}>
216 <ChevronDown class="h-4 w-4 shrink-0" />
218 <ChevronRight class="h-4 w-4 shrink-0" />
221 <PencilRuler class="inline h-4 w-4 shrink-0" />
223 <span class="flex-1">Tools</span>
225 <span class="text-xs text-muted-foreground">
226 {toolsPanel.totalToolCount} tool{toolsPanel.totalToolCount !== 1 ? 's' : ''}
228 </Collapsible.Trigger>
230 <Collapsible.Content>
231 <div class="flex flex-col gap-0.5 pl-4">
232 {#each toolsPanel.activeGroups as group (group.label)}
233 {@const checked = toolsPanel.isGroupChecked(group)}
234 {@const enabledCount = toolsPanel.getEnabledToolCount(group)}
235 {@const favicon = toolsPanel.getFavicon(group)}
239 class={sheetItemRowClass}
240 onclick={() => toolsPanel.toggleGroupByLabel(group.label)}
246 class="h-4 w-4 shrink-0 rounded-sm"
248 (e.currentTarget as HTMLImageElement).style.display = 'none';
253 <span class="min-w-0 flex-1 truncate text-sm font-medium">{group.label}</span>
255 <span class="shrink-0 text-xs text-muted-foreground">
256 {enabledCount}/{group.tools.length}
261 class="h-4 w-4 shrink-0"
262 onclick={(e) => e.stopPropagation()}
263 onCheckedChange={() => toolsPanel.toggleGroupByLabel(group.label)}
268 </Collapsible.Content>
274 class={sheetItemClass}
275 onclick={() => attachmentMenu.callbacks[AttachmentAction.SYSTEM_PROMPT_CLICK]()}
277 <MessageSquare class="h-4 w-4 shrink-0" />
279 <span>System Message</span>
282 {#if hasMcpPromptsSupport}
285 class={sheetItemClass}
286 onclick={() => attachmentMenu.callbacks[AttachmentAction.MCP_PROMPT_CLICK]()}
288 <Zap class="h-4 w-4 shrink-0" />
290 <span>MCP Prompt</span>
294 {#if hasMcpResourcesSupport}
297 class={sheetItemClass}
298 onclick={() => attachmentMenu.callbacks[AttachmentAction.MCP_RESOURCES_CLICK]()}
300 <FolderOpen class="h-4 w-4 shrink-0" />
302 <span>MCP Resources</span>