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 { useReasoningMenu } from '$lib/hooks/use-reasoning-menu.svelte';
14 import { conversationsStore } from '$lib/stores/conversations.svelte';
15 import { mcpStore } from '$lib/stores/mcp.svelte';
16 import { McpLogo } from '$lib/components/app';
24 } from '@lucide/svelte';
25 import { HealthCheckStatus } from '$lib/enums';
26 import { AttachmentAction } from '$lib/enums/attachment.enums';
31 hasAudioModality?: boolean;
32 hasVideoModality?: boolean;
33 hasVisionModality?: boolean;
34 hasMcpPromptsSupport?: boolean;
35 hasMcpResourcesSupport?: boolean;
36 onFileUpload?: () => void;
37 onSystemPromptClick?: () => void;
38 onMcpPromptClick?: () => void;
39 onMcpResourcesClick?: () => void;
40 trigger: Snippet<[{ disabled: boolean; onclick?: () => void }]>;
44 class: className = '',
46 hasAudioModality = false,
47 hasVisionModality = false,
48 hasVideoModality = false,
49 hasMcpPromptsSupport = false,
50 hasMcpResourcesSupport = false,
58 let sheetOpen = $state(false);
59 let reasoningExpanded = $state(false);
60 let filesExpanded = $state(true);
61 let toolsExpanded = $state(false);
62 let mcpExpanded = $state(false);
64 const attachmentMenu = useAttachmentMenu(
70 hasMcpResourcesSupport
72 () => ({ onFileUpload, onSystemPromptClick, onMcpPromptClick, onMcpResourcesClick }),
78 const toolsPanel = useToolsPanel();
79 const reasoning = useReasoningMenu();
81 const sheetItemClass =
82 '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';
84 const sheetItemRowClass =
85 'flex w-full items-center justify-between gap-2 rounded-md px-3 py-2 text-left text-sm transition-colors hover:bg-accent';
87 let mcpServers = $derived(mcpStore.getServers());
90 <div class="flex items-center gap-1 {className}">
91 <Sheet.Root bind:open={sheetOpen}>
92 {@render trigger({ disabled, onclick: () => (sheetOpen = true) })}
94 <Sheet.Content side="bottom" class="max-h-[85vh] gap-0 overflow-y-auto">
96 <Sheet.Title>Add to chat</Sheet.Title>
98 <Sheet.Description class="sr-only">
99 Add files, system prompt or configure MCP servers
103 <div class="flex flex-col gap-1 px-1.5 pb-2">
104 {#if reasoning.modelSupportsThinking}
106 open={reasoningExpanded}
107 onOpenChange={(open) => (reasoningExpanded = open)}
109 <Collapsible.Trigger class={sheetItemClass}>
110 {#if reasoningExpanded}
111 <ChevronDown class="h-4 w-4 shrink-0" />
113 <ChevronRight class="h-4 w-4 shrink-0" />
116 {#if reasoning.thinkingEnabled}
117 <Lightbulb class="h-4 w-4 shrink-0 text-amber-400" />
119 <LightbulbOff class="h-4 w-4 shrink-0 text-muted-foreground" />
122 <span class="flex-1">Reasoning</span>
124 <span class="text-xs capitalize text-muted-foreground">
125 {reasoning.thinkingEnabled ? reasoning.currentEffort : 'off'}
127 </Collapsible.Trigger>
129 <Collapsible.Content>
130 <div class="flex flex-col gap-0.5 pl-4">
131 {#each reasoning.levels as level (level.value)}
132 {@const tokenLabel = reasoning.tokenLabel(level)}
135 class={sheetItemRowClass}
136 class:bg-accent={reasoning.isSelected(level)}
137 onclick={() => reasoning.select(level)}
139 <div class="flex min-w-0 items-center gap-3">
140 {#if reasoning.isSelected(level)}
141 <Check class="h-4 w-4 shrink-0 text-foreground" />
143 <div class="h-4 w-4 shrink-0"></div>
146 <span class="text-sm">{level.label}</span>
150 <span class="shrink-0 text-[11px] text-muted-foreground opacity-60">
157 </Collapsible.Content>
161 <Collapsible.Root open={filesExpanded} onOpenChange={(open) => (filesExpanded = open)}>
162 <Collapsible.Trigger class={sheetItemClass}>
164 <ChevronDown class="h-4 w-4 shrink-0" />
166 <ChevronRight class="h-4 w-4 shrink-0" />
169 <File class="h-4 w-4 shrink-0" />
171 <span class="flex-1">Add files</span>
172 </Collapsible.Trigger>
174 <Collapsible.Content>
175 <div class="flex flex-col gap-0.5 pl-4">
176 {#each ATTACHMENT_FILE_ITEMS as item (item.id)}
177 {@const enabled = attachmentMenu.isItemEnabled(item.enabledWhen)}
181 class={sheetItemClass}
182 onclick={() => attachmentMenu.callbacks[item.action]()}
184 <item.icon class="h-4 w-4 shrink-0" />
186 <span>{item.label}</span>
188 {:else if item.disabledTooltip}
189 <Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
191 <button type="button" class={sheetItemClass} disabled>
192 <item.icon class="h-4 w-4 shrink-0" />
194 <span>{item.label}</span>
198 <Tooltip.Content side="right">
199 <p>{item.disabledTooltip}</p>
205 </Collapsible.Content>
208 <Collapsible.Root open={mcpExpanded} onOpenChange={(open) => (mcpExpanded = open)}>
209 <Collapsible.Trigger class={sheetItemClass}>
211 <ChevronDown class="h-4 w-4 shrink-0" />
213 <ChevronRight class="h-4 w-4 shrink-0" />
216 <McpLogo class="inline h-4 w-4 shrink-0" />
218 <span class="flex-1">MCP Servers</span>
220 <span class="text-xs text-muted-foreground">
221 {mcpServers.length} server{mcpServers.length !== 1 ? 's' : ''}
223 </Collapsible.Trigger>
225 <Collapsible.Content>
226 <div class="flex flex-col gap-0.5 pl-4">
227 {#each mcpServers as server (server.id)}
228 {@const healthState = mcpStore.getHealthCheckState(server.id)}
229 {@const hasError = healthState.status === HealthCheckStatus.ERROR}
230 {@const displayName = mcpStore.getServerLabel(server)}
231 {@const faviconUrl = mcpStore.getServerFavicon(server.id)}
232 {@const isEnabled = conversationsStore.isMcpServerEnabledForChat(server.id)}
236 class={sheetItemRowClass}
237 onclick={() => !hasError && conversationsStore.toggleMcpServerForChat(server.id)}
240 <div class="flex min-w-0 flex-1 items-center gap-2">
245 class="h-4 w-4 shrink-0 rounded-sm"
247 (e.currentTarget as HTMLImageElement).style.display = 'none';
252 <span class="min-w-0 truncate text-sm">{displayName}</span>
257 class="shrink-0 rounded bg-destructive/15 px-1.5 py-0.5 text-xs text-destructive"
264 onCheckedChange={() => conversationsStore.toggleMcpServerForChat(server.id)}
270 {#if mcpServers.length === 0}
271 <div class="px-3 py-2 text-center text-sm text-muted-foreground">
272 No MCP servers configured
276 </Collapsible.Content>
279 {#if toolsPanel.totalToolCount > 0}
280 <Collapsible.Root open={toolsExpanded} onOpenChange={(open) => (toolsExpanded = open)}>
281 <Collapsible.Trigger class={sheetItemClass}>
283 <ChevronDown class="h-4 w-4 shrink-0" />
285 <ChevronRight class="h-4 w-4 shrink-0" />
288 <PencilRuler class="inline h-4 w-4 shrink-0" />
290 <span class="flex-1">Tools</span>
292 <span class="text-xs text-muted-foreground">
293 {toolsPanel.totalToolCount} tool{toolsPanel.totalToolCount !== 1 ? 's' : ''}
295 </Collapsible.Trigger>
297 <Collapsible.Content>
298 <div class="flex flex-col gap-0.5 pl-4">
299 {#each toolsPanel.activeGroups as group (group.label)}
300 {@const checked = toolsPanel.isGroupChecked(group)}
301 {@const enabledCount = toolsPanel.getEnabledToolCount(group)}
302 {@const favicon = toolsPanel.getFavicon(group)}
306 class={sheetItemRowClass}
307 onclick={() => toolsPanel.toggleGroupByLabel(group.label)}
313 class="h-4 w-4 shrink-0 rounded-sm"
315 (e.currentTarget as HTMLImageElement).style.display = 'none';
320 <span class="min-w-0 flex-1 truncate text-sm font-medium">{group.label}</span>
322 <span class="shrink-0 text-xs text-muted-foreground">
323 {enabledCount}/{group.tools.length}
328 class="h-4 w-4 shrink-0"
329 onclick={(e) => e.stopPropagation()}
330 onCheckedChange={() => toolsPanel.toggleGroupByLabel(group.label)}
335 </Collapsible.Content>
341 class={sheetItemClass}
342 onclick={() => attachmentMenu.callbacks[AttachmentAction.SYSTEM_PROMPT_CLICK]()}
344 <MessageSquare class="h-4 w-4 shrink-0" />
346 <span>System Message</span>
349 {#if hasMcpPromptsSupport}
352 class={sheetItemClass}
353 onclick={() => attachmentMenu.callbacks[AttachmentAction.MCP_PROMPT_CLICK]()}
355 <Zap class="h-4 w-4 shrink-0" />
357 <span>MCP Prompt</span>
361 {#if hasMcpResourcesSupport}
364 class={sheetItemClass}
365 onclick={() => attachmentMenu.callbacks[AttachmentAction.MCP_RESOURCES_CLICK]()}
367 <FolderOpen class="h-4 w-4 shrink-0" />
369 <span>MCP Resources</span>