feat: Integrate Resource Attachments into Chat Form UI

This commit is contained in:
Aleksander Grygier 2026-01-28 18:28:03 +01:00
parent 23e4ef7495
commit aa7089d598
4 changed files with 102 additions and 5 deletions

View File

@ -4,8 +4,10 @@
ChatFormActions, ChatFormActions,
ChatFormFileInputInvisible, ChatFormFileInputInvisible,
ChatFormPromptPicker, ChatFormPromptPicker,
ChatFormTextarea ChatFormTextarea,
McpResourcePicker
} from '$lib/components/app'; } from '$lib/components/app';
import ChatFormResourceAttachments from '../ChatFormResourceAttachments.svelte';
import { INPUT_CLASSES } from '$lib/constants/css-classes'; import { INPUT_CLASSES } from '$lib/constants/css-classes';
import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config'; import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config';
import { MimeTypeText, SpecialFileType } from '$lib/enums'; import { MimeTypeText, SpecialFileType } from '$lib/enums';
@ -14,6 +16,7 @@
import { isRouterMode } from '$lib/stores/server.svelte'; import { isRouterMode } from '$lib/stores/server.svelte';
import { chatStore } from '$lib/stores/chat.svelte'; import { chatStore } from '$lib/stores/chat.svelte';
import { mcpStore } from '$lib/stores/mcp.svelte'; import { mcpStore } from '$lib/stores/mcp.svelte';
import { mcpHasResourceAttachments } from '$lib/stores/mcp-resources.svelte';
import { conversationsStore, activeMessages } from '$lib/stores/conversations.svelte'; import { conversationsStore, activeMessages } from '$lib/stores/conversations.svelte';
import type { GetPromptResult, MCPPromptInfo, PromptMessage } from '$lib/types'; import type { GetPromptResult, MCPPromptInfo, PromptMessage } from '$lib/types';
import { isIMEComposing, parseClipboardContent } from '$lib/utils'; import { isIMEComposing, parseClipboardContent } from '$lib/utils';
@ -91,6 +94,10 @@
let isPromptPickerOpen = $state(false); let isPromptPickerOpen = $state(false);
let promptSearchQuery = $state(''); let promptSearchQuery = $state('');
// Resource Picker State
let isResourcePickerOpen = $state(false);
let preSelectedResourceUri = $state<string | undefined>(undefined);
/** /**
* *
* *
@ -489,7 +496,7 @@
onpaste={handlePaste} onpaste={handlePaste}
> >
<ChatFormTextarea <ChatFormTextarea
class="px-2 py-1 md:py-0" class="px-2 py-1.5 md:pt-0"
bind:this={textareaRef} bind:this={textareaRef}
bind:value bind:value
onKeydown={handleKeydown} onKeydown={handleKeydown}
@ -501,6 +508,16 @@
{placeholder} {placeholder}
/> />
{#if mcpHasResourceAttachments()}
<ChatFormResourceAttachments
class="mb-3"
onResourceClick={(uri) => {
preSelectedResourceUri = uri;
isResourcePickerOpen = true;
}}
/>
{/if}
<ChatFormActions <ChatFormActions
bind:this={chatFormActionsRef} bind:this={chatFormActionsRef}
canSend={canSubmit} canSend={canSubmit}
@ -514,7 +531,18 @@
{onStop} {onStop}
onSystemPromptClick={() => onSystemPromptClick?.({ message: value, files: uploadedFiles })} onSystemPromptClick={() => onSystemPromptClick?.({ message: value, files: uploadedFiles })}
onMcpPromptClick={showMcpPromptButton ? () => (isPromptPickerOpen = true) : undefined} onMcpPromptClick={showMcpPromptButton ? () => (isPromptPickerOpen = true) : undefined}
onMcpResourcesClick={() => (isResourcePickerOpen = true)}
/> />
</div> </div>
</div> </div>
</form> </form>
<McpResourcePicker
bind:open={isResourcePickerOpen}
preSelectedUri={preSelectedResourceUri}
onOpenChange={(newOpen) => {
if (!newOpen) {
preSelectedResourceUri = undefined;
}
}}
/>

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import { Plus, MessageSquare, Zap } from '@lucide/svelte'; import { Plus, MessageSquare, Zap, FolderOpen } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu'; import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Tooltip from '$lib/components/ui/tooltip'; import * as Tooltip from '$lib/components/ui/tooltip';
@ -14,10 +14,12 @@
hasAudioModality?: boolean; hasAudioModality?: boolean;
hasVisionModality?: boolean; hasVisionModality?: boolean;
hasMcpPromptsSupport?: boolean; hasMcpPromptsSupport?: boolean;
hasMcpResourcesSupport?: boolean;
onFileUpload?: () => void; onFileUpload?: () => void;
onSystemPromptClick?: () => void; onSystemPromptClick?: () => void;
onMcpPromptClick?: () => void; onMcpPromptClick?: () => void;
onMcpServersClick?: () => void; onMcpServersClick?: () => void;
onMcpResourcesClick?: () => void;
} }
let { let {
@ -26,10 +28,12 @@
hasAudioModality = false, hasAudioModality = false,
hasVisionModality = false, hasVisionModality = false,
hasMcpPromptsSupport = false, hasMcpPromptsSupport = false,
hasMcpResourcesSupport = false,
onFileUpload, onFileUpload,
onSystemPromptClick, onSystemPromptClick,
onMcpPromptClick, onMcpPromptClick,
onMcpServersClick onMcpServersClick,
onMcpResourcesClick
}: Props = $props(); }: Props = $props();
let isNewChat = $derived(!page.params.id); let isNewChat = $derived(!page.params.id);
@ -52,6 +56,11 @@
onMcpServersClick?.(); onMcpServersClick?.();
} }
function handleMcpResourcesClick() {
dropdownOpen = false;
onMcpResourcesClick?.();
}
const fileUploadTooltipText = 'Add files, system prompt or MCP Servers'; const fileUploadTooltipText = 'Add files, system prompt or MCP Servers';
</script> </script>
@ -195,6 +204,17 @@
<span>MCP Prompt</span> <span>MCP Prompt</span>
</DropdownMenu.Item> </DropdownMenu.Item>
{/if} {/if}
{#if hasMcpResourcesSupport}
<DropdownMenu.Item
class="flex cursor-pointer items-center gap-2"
onclick={handleMcpResourcesClick}
>
<FolderOpen class="h-4 w-4" />
<span>MCP Resources</span>
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root>
</div> </div>

View File

@ -31,6 +31,7 @@
onStop?: () => void; onStop?: () => void;
onSystemPromptClick?: () => void; onSystemPromptClick?: () => void;
onMcpPromptClick?: () => void; onMcpPromptClick?: () => void;
onMcpResourcesClick?: () => void;
} }
let { let {
@ -45,7 +46,8 @@
onMicClick, onMicClick,
onStop, onStop,
onSystemPromptClick, onSystemPromptClick,
onMcpPromptClick onMcpPromptClick,
onMcpResourcesClick
}: Props = $props(); }: Props = $props();
let currentConfig = $derived(config()); let currentConfig = $derived(config());
@ -165,6 +167,11 @@
const perChatOverrides = conversationsStore.getAllMcpServerOverrides(); const perChatOverrides = conversationsStore.getAllMcpServerOverrides();
return mcpStore.hasEnabledServers(perChatOverrides); return mcpStore.hasEnabledServers(perChatOverrides);
}); });
let hasMcpResourcesSupport = $derived.by(() => {
const perChatOverrides = conversationsStore.getAllMcpServerOverrides();
return mcpStore.hasEnabledServers(perChatOverrides) && mcpStore.hasResourcesCapability();
});
</script> </script>
<div class="flex w-full items-center gap-3 {className}" style="container-type: inline-size"> <div class="flex w-full items-center gap-3 {className}" style="container-type: inline-size">
@ -174,9 +181,11 @@
{hasAudioModality} {hasAudioModality}
{hasVisionModality} {hasVisionModality}
{hasMcpPromptsSupport} {hasMcpPromptsSupport}
{hasMcpResourcesSupport}
{onFileUpload} {onFileUpload}
{onSystemPromptClick} {onSystemPromptClick}
{onMcpPromptClick} {onMcpPromptClick}
{onMcpResourcesClick}
onMcpServersClick={() => (showMcpDialog = true)} onMcpServersClick={() => (showMcpDialog = true)}
/> />

View File

@ -0,0 +1,40 @@
<script lang="ts">
import { mcpStore } from '$lib/stores/mcp.svelte';
import {
mcpResourceAttachments,
mcpHasResourceAttachments
} from '$lib/stores/mcp-resources.svelte';
import { ChatAttachmentMcpResource, HorizontalScrollCarousel } from '$lib/components/app';
interface Props {
class?: string;
onResourceClick?: (uri: string) => void;
}
let { class: className, onResourceClick }: Props = $props();
const attachments = $derived(mcpResourceAttachments());
const hasAttachments = $derived(mcpHasResourceAttachments());
function handleRemove(attachmentId: string) {
mcpStore.removeResourceAttachment(attachmentId);
}
function handleResourceClick(uri: string) {
onResourceClick?.(uri);
}
</script>
{#if hasAttachments}
<div class={className}>
<HorizontalScrollCarousel gapSize="2">
{#each attachments as attachment (attachment.id)}
<ChatAttachmentMcpResource
{attachment}
onRemove={handleRemove}
onClick={() => handleResourceClick(attachment.resource.uri)}
/>
{/each}
</HorizontalScrollCarousel>
</div>
{/if}