2 import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
3 import type { Snippet } from 'svelte';
4 import * as Tooltip from '$lib/components/ui/tooltip';
5 import * as Sheet from '$lib/components/ui/sheet';
6 import * as Collapsible from '$lib/components/ui/collapsible';
7 import { File, MessageSquare, Zap, FolderOpen } from '@lucide/svelte';
8 import { Switch } from '$lib/components/ui/switch';
9 import { Checkbox } from '$lib/components/ui/checkbox';
10 import { TOOLTIP_DELAY_DURATION } from '$lib/constants';
11 import { ATTACHMENT_FILE_ITEMS } from '$lib/constants/attachment-menu';
12 import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
13 import { useToolsPanel } from '$lib/hooks/use-tools-panel.svelte';
14 import { useReasoningMenu } from '$lib/hooks/use-reasoning-menu.svelte';
15 import { conversationsStore } from '$lib/stores/conversations.svelte';
16 import { mcpStore } from '$lib/stores/mcp.svelte';
17 import { McpLogo } from '$lib/components/app';
25 } from '@lucide/svelte';
26 import { HealthCheckStatus } from '$lib/enums';
27 import { AttachmentAction } from '$lib/enums/attachment.enums';
32 hasAudioModality?: boolean;
33 hasVideoModality?: boolean;
34 hasVisionModality?: boolean;
35 hasMcpPromptsSupport?: boolean;
36 hasMcpResourcesSupport?: boolean;
37 onFileUpload?: () => void;
38 onSystemPromptClick?: () => void;
39 onMcpPromptClick?: () => void;
40 onMcpResourcesClick?: () => void;
41 trigger: Snippet<[{ disabled: boolean; onclick?: () => void }]>;
45 class: className = '',
47 hasAudioModality = false,
48 hasVisionModality = false,
49 hasVideoModality = false,
50 hasMcpPromptsSupport = false,
51 hasMcpResourcesSupport = false,
59 let sheetOpen = $state(false);
60 let reasoningExpanded = $state(false);
61 let filesExpanded = $state(true);
62 let toolsExpanded = $state(false);
63 let mcpExpanded = $state(false);
65 const attachmentMenu = useAttachmentMenu(
71 hasMcpResourcesSupport
73 () => ({ onFileUpload, onSystemPromptClick, onMcpPromptClick, onMcpResourcesClick }),
79 const toolsPanel = useToolsPanel();
80 const reasoning = useReasoningMenu();
82 const sheetItemClass =
83 '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';
85 const sheetItemRowClass =
86 'flex w-full items-center justify-between gap-2 rounded-md px-3 py-2 text-left text-sm transition-colors hover:bg-accent';
88 let mcpServers = $derived(mcpStore.getServers());
91 <div class="flex items-center gap-1 {className}">
92 <Sheet.Root bind:open={sheetOpen}>
93 {@render trigger({ disabled, onclick: () => (sheetOpen = true) })}
95 <Sheet.Content side="bottom" class="max-h-[85vh] gap-0 overflow-y-auto">
97 <Sheet.Title>Add to chat</Sheet.Title>
99 <Sheet.Description class="sr-only">
100 Add files, system prompt or configure MCP servers
104 <div class="flex flex-col gap-1 px-1.5 pb-2">
105 {#if reasoning.modelSupportsThinking}
107 open={reasoningExpanded}
108 onOpenChange={(open) => (reasoningExpanded = open)}
110 <Collapsible.Trigger class={sheetItemClass}>
111 {#if reasoningExpanded}
112 <ChevronDown class="{ICON_CLASS_DEFAULT} shrink-0" />
114 <ChevronRight class="{ICON_CLASS_DEFAULT} shrink-0" />
117 {#if reasoning.thinkingEnabled}
118 <Lightbulb class="{ICON_CLASS_DEFAULT} shrink-0 text-amber-400" />
120 <LightbulbOff class="{ICON_CLASS_DEFAULT} shrink-0 text-muted-foreground" />
123 <span class="flex-1">Reasoning</span>
125 <span class="text-xs capitalize text-muted-foreground">
126 {reasoning.thinkingEnabled ? reasoning.currentEffort : 'off'}
128 </Collapsible.Trigger>
130 <Collapsible.Content>
131 <div class="flex flex-col gap-0.5 pl-4">
132 {#each reasoning.levels as level (level.value)}
133 {@const tokenLabel = reasoning.tokenLabel(level)}
136 class={sheetItemRowClass}
137 class:bg-accent={reasoning.isSelected(level)}
138 onclick={() => reasoning.select(level)}
140 <div class="flex min-w-0 items-center gap-3">
141 {#if reasoning.isSelected(level)}
142 <Check class="{ICON_CLASS_DEFAULT} shrink-0 text-foreground" />
144 <div class="{ICON_CLASS_DEFAULT} shrink-0"></div>
147 <span class="text-sm">{level.label}</span>
151 <span class="shrink-0 text-[11px] text-muted-foreground opacity-60">
158 </Collapsible.Content>
162 <Collapsible.Root open={filesExpanded} onOpenChange={(open) => (filesExpanded = open)}>
163 <Collapsible.Trigger class={sheetItemClass}>
165 <ChevronDown class="{ICON_CLASS_DEFAULT} shrink-0" />
167 <ChevronRight class="{ICON_CLASS_DEFAULT} shrink-0" />
170 <File class="{ICON_CLASS_DEFAULT} shrink-0" />
172 <span class="flex-1">Add files</span>
173 </Collapsible.Trigger>
175 <Collapsible.Content>
176 <div class="flex flex-col gap-0.5 pl-4">
177 {#each ATTACHMENT_FILE_ITEMS as item (item.id)}
178 {@const enabled = attachmentMenu.isItemEnabled(item.enabledWhen)}
182 class={sheetItemClass}
183 onclick={() => attachmentMenu.callbacks[item.action]()}
185 <item.icon class="{ICON_CLASS_DEFAULT} shrink-0" />
187 <span>{item.label}</span>
189 {:else if item.disabledTooltip}
190 <Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
192 <button type="button" class={sheetItemClass} disabled>
193 <item.icon class="{ICON_CLASS_DEFAULT} shrink-0" />
195 <span>{item.label}</span>
199 <Tooltip.Content side="right">
200 <p>{item.disabledTooltip}</p>
206 </Collapsible.Content>
209 <Collapsible.Root open={mcpExpanded} onOpenChange={(open) => (mcpExpanded = open)}>
210 <Collapsible.Trigger class={sheetItemClass}>
212 <ChevronDown class="{ICON_CLASS_DEFAULT} shrink-0" />
214 <ChevronRight class="{ICON_CLASS_DEFAULT} shrink-0" />
217 <McpLogo class="inline {ICON_CLASS_DEFAULT} shrink-0" />
219 <span class="flex-1">MCP Servers</span>
221 <span class="text-xs text-muted-foreground">
222 {mcpServers.length} server{mcpServers.length !== 1 ? 's' : ''}
224 </Collapsible.Trigger>
226 <Collapsible.Content>
227 <div class="flex flex-col gap-0.5 pl-4">
228 {#each mcpServers as server (server.id)}
229 {@const healthState = mcpStore.getHealthCheckState(server.id)}
230 {@const hasError = healthState.status === HealthCheckStatus.ERROR}
231 {@const displayName = mcpStore.getServerLabel(server)}
232 {@const faviconUrl = mcpStore.getServerFavicon(server.id)}
233 {@const isEnabled = conversationsStore.isMcpServerEnabledForChat(server.id)}
237 class={sheetItemRowClass}
238 onclick={() => !hasError && conversationsStore.toggleMcpServerForChat(server.id)}
241 <div class="flex min-w-0 flex-1 items-center gap-2">
246 class="{ICON_CLASS_DEFAULT} shrink-0 rounded-sm"
248 (e.currentTarget as HTMLImageElement).style.display = 'none';
253 <span class="min-w-0 truncate text-sm">{displayName}</span>
258 class="shrink-0 rounded bg-destructive/15 px-1.5 py-0.5 text-xs text-destructive"
265 onCheckedChange={() => conversationsStore.toggleMcpServerForChat(server.id)}
271 {#if mcpServers.length === 0}
272 <div class="px-3 py-2 text-center text-sm text-muted-foreground">
273 No MCP servers configured
277 </Collapsible.Content>
280 {#if toolsPanel.totalToolCount > 0}
281 <Collapsible.Root open={toolsExpanded} onOpenChange={(open) => (toolsExpanded = open)}>
282 <Collapsible.Trigger class={sheetItemClass}>
284 <ChevronDown class="{ICON_CLASS_DEFAULT} shrink-0" />
286 <ChevronRight class="{ICON_CLASS_DEFAULT} shrink-0" />
289 <PencilRuler class="inline {ICON_CLASS_DEFAULT} shrink-0" />
291 <span class="flex-1">Tools</span>
293 <span class="text-xs text-muted-foreground">
294 {toolsPanel.totalToolCount} tool{toolsPanel.totalToolCount !== 1 ? 's' : ''}
296 </Collapsible.Trigger>
298 <Collapsible.Content>
299 <div class="flex flex-col gap-0.5 pl-4">
300 {#each toolsPanel.activeGroups as group (group.label)}
301 {@const checked = toolsPanel.isGroupChecked(group)}
302 {@const enabledCount = toolsPanel.getEnabledToolCount(group)}
303 {@const favicon = toolsPanel.getFavicon(group)}
307 class={sheetItemRowClass}
308 onclick={() => toolsPanel.toggleGroupByLabel(group.label)}
314 class="{ICON_CLASS_DEFAULT} shrink-0 rounded-sm"
316 (e.currentTarget as HTMLImageElement).style.display = 'none';
321 <span class="min-w-0 flex-1 truncate text-sm font-medium">{group.label}</span>
323 <span class="shrink-0 text-xs text-muted-foreground">
324 {enabledCount}/{group.tools.length}
329 class="{ICON_CLASS_DEFAULT} shrink-0"
330 onclick={(e) => e.stopPropagation()}
331 onCheckedChange={() => toolsPanel.toggleGroupByLabel(group.label)}
336 </Collapsible.Content>
342 class={sheetItemClass}
343 onclick={() => attachmentMenu.callbacks[AttachmentAction.SYSTEM_PROMPT_CLICK]()}
345 <MessageSquare class="{ICON_CLASS_DEFAULT} shrink-0" />
347 <span>System Message</span>
350 {#if hasMcpPromptsSupport}
353 class={sheetItemClass}
354 onclick={() => attachmentMenu.callbacks[AttachmentAction.MCP_PROMPT_CLICK]()}
356 <Zap class="{ICON_CLASS_DEFAULT} shrink-0" />
358 <span>MCP Prompt</span>
362 {#if hasMcpResourcesSupport}
365 class={sheetItemClass}
366 onclick={() => attachmentMenu.callbacks[AttachmentAction.MCP_RESOURCES_CLICK]()}
368 <FolderOpen class="{ICON_CLASS_DEFAULT} shrink-0" />
370 <span>MCP Resources</span>