Skip to content

Commit 549bf25

Browse files
committed
feat: Enhances chat form with file attachment options
Refactors the chat form to include a dropdown menu for selecting different file types to attach. Adds a new component for the file attachment action. Also prevents form submission when clicking on attachments.
1 parent 49931c4 commit 549bf25

12 files changed

+287
-119
lines changed

tools/server/webui/src/lib/components/app/ServerInfo.svelte

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@
99
</script>
1010

1111
{#if props}
12-
<div class="text-muted-foreground flex items-center justify-center gap-3 text-sm">
12+
<div class="text-muted-foreground flex flex-wrap items-center justify-center gap-4 text-sm">
1313
{#if model}
1414
<Badge variant="outline" class="text-xs">
1515
<Server class="mr-1 h-3 w-3" />
1616
<span class="block max-w-[50vw] truncate">{model}</span>
1717
</Badge>
1818
{/if}
1919

20-
{#if props.default_generation_settings.n_ctx}
20+
<div class="flex gap-4">
21+
{#if props.default_generation_settings.n_ctx}
2122
<Badge variant="secondary" class="text-xs">
2223
ctx: {props.default_generation_settings.n_ctx.toLocaleString()}
2324
</Badge>
@@ -35,5 +36,6 @@
3536
</Badge>
3637
{/each}
3738
{/if}
39+
</div>
3840
</div>
3941
{/if}

tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentFilePreview.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
interface Props {
88
class?: string;
99
id: string;
10-
onClick?: () => void;
10+
onClick?: (event?: MouseEvent) => void;
1111
onRemove?: (id: string) => void;
1212
name: string;
1313
readonly?: boolean;

tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentImagePreview.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
size?: number;
1010
readonly?: boolean;
1111
onRemove?: (id: string) => void;
12-
onClick?: () => void;
12+
onClick?: (event?: MouseEvent) => void;
1313
class?: string;
1414
// Customizable size props
1515
width?: string;

tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList.svelte

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@
4343
textContent?: string;
4444
} | null>(null);
4545
46-
function openPreview(item: (typeof displayItems)[0]) {
46+
function openPreview(item: (typeof displayItems)[0], event?: Event) {
47+
// Prevent form submission when clicking on attachments
48+
if (event) {
49+
event.preventDefault();
50+
event.stopPropagation();
51+
}
52+
4753
previewItem = {
4854
uploadedFile: item.uploadedFile,
4955
attachment: item.attachment,
@@ -147,7 +153,7 @@
147153
height={imageHeight}
148154
width={imageWidth}
149155
{imageClass}
150-
onClick={() => openPreview(item)}
156+
onClick={(event) => openPreview(item, event)}
151157
/>
152158
{:else}
153159
<ChatAttachmentFilePreview
@@ -159,7 +165,7 @@
159165
{readonly}
160166
onRemove={onFileRemove}
161167
textContent={item.textContent}
162-
onClick={() => openPreview(item)}
168+
onClick={(event) => openPreview(item, event)}
163169
/>
164170
{/if}
165171
{/each}

tools/server/webui/src/lib/components/app/chat/ChatForm/ChatForm.svelte

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import { ChatAttachmentsList } from '$lib/components/app';
3-
import { ChatFormActionButtons, ChatFormFileInputInvisible, ChatFormHelperText, ChatFormTextarea } from '$lib/components/app';
3+
import { ChatFormActions, ChatFormFileInputInvisible, ChatFormHelperText, ChatFormTextarea } from '$lib/components/app';
44
import { inputClasses } from '$lib/constants/input-classes';
55
import { onMount } from 'svelte';
66
import { config } from '$lib/stores/settings.svelte';
@@ -10,7 +10,16 @@
1010
createAudioFile,
1111
isAudioRecordingSupported
1212
} from '$lib/utils/audio-recording';
13-
import { TextMimeType } from '$lib/constants/supported-file-types';
13+
import {
14+
TextMimeType,
15+
ImageExtension,
16+
ImageMimeType,
17+
AudioExtension,
18+
AudioMimeType,
19+
PdfExtension,
20+
PdfMimeType,
21+
TextExtension
22+
} from '$lib/constants/supported-file-types';
1423
1524
interface Props {
1625
class?: string;
@@ -41,11 +50,12 @@
4150
4251
let audioRecorder: AudioRecorder | undefined;
4352
let isRecording = $state(false);
44-
let fileInputRef: ChatFormFileInputInvisible | undefined;
53+
let fileInputRef: ChatFormFileInputInvisible | undefined = $state(undefined);
4554
let message = $state('');
4655
let previousIsLoading = $state(isLoading);
4756
let recordingSupported = $state(false);
48-
let textareaRef: ChatFormTextarea | undefined;
57+
let textareaRef: ChatFormTextarea | undefined = $state(undefined);
58+
let fileAcceptString = $state<string | undefined>(undefined);
4959
5060
async function handleKeydown(event: KeyboardEvent) {
5161
if (event.key === 'Enter' && !event.shiftKey) {
@@ -74,8 +84,44 @@
7484
onFileUpload?.(files);
7585
}
7686
77-
function handleFileUpload() {
78-
fileInputRef?.click();
87+
function handleFileUpload(fileType?: 'image' | 'audio' | 'pdf' | 'file') {
88+
if (fileType) {
89+
fileAcceptString = getAcceptStringForFileType(fileType);
90+
} else {
91+
fileAcceptString = undefined;
92+
}
93+
94+
// Use setTimeout to ensure the accept attribute is applied before opening dialog
95+
setTimeout(() => {
96+
fileInputRef?.click();
97+
}, 10);
98+
}
99+
100+
function getAcceptStringForFileType(fileType: 'image' | 'audio' | 'file' | 'pdf'): string {
101+
switch (fileType) {
102+
case 'image':
103+
return [
104+
...Object.values(ImageExtension),
105+
...Object.values(ImageMimeType)
106+
].join(',');
107+
case 'audio':
108+
return [
109+
...Object.values(AudioExtension),
110+
...Object.values(AudioMimeType)
111+
].join(',');
112+
case 'pdf':
113+
return [
114+
...Object.values(PdfExtension),
115+
...Object.values(PdfMimeType)
116+
].join(',');
117+
case 'file':
118+
return [
119+
...Object.values(TextExtension),
120+
TextMimeType.PLAIN
121+
].join(',');
122+
default:
123+
return '';
124+
}
79125
}
80126
81127
function handlePaste(event: ClipboardEvent) {
@@ -176,7 +222,7 @@
176222
});
177223
</script>
178224

179-
<ChatFormFileInputInvisible bind:this={fileInputRef} onFileSelect={handleFileSelect} />
225+
<ChatFormFileInputInvisible bind:this={fileInputRef} bind:accept={fileAcceptString} onFileSelect={handleFileSelect} />
180226

181227
<form
182228
onsubmit={handleSubmit}
@@ -195,7 +241,7 @@
195241
disabled={isLoading}
196242
/>
197243

198-
<ChatFormActionButtons
244+
<ChatFormActions
199245
canSend={message.trim().length > 0 || uploadedFiles.length > 0}
200246
{disabled}
201247
{isLoading}

tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActionButtons.svelte

Lines changed: 0 additions & 100 deletions
This file was deleted.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<script lang="ts">
2+
import { Button } from '$lib/components/ui/button';
3+
import { Paperclip, Image, FileText, File, Volume2 } from '@lucide/svelte';
4+
import { supportsVision } from '$lib/stores/server.svelte';
5+
import * as Tooltip from '$lib/components/ui/tooltip';
6+
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
7+
import { TOOLTIP_DELAY_DURATION } from '$lib/constants/tooltip-config';
8+
9+
interface Props {
10+
disabled?: boolean;
11+
onFileUpload?: (fileType?: 'image' | 'audio' | 'file' | 'pdf') => void;
12+
class?: string;
13+
}
14+
15+
let {
16+
disabled = false,
17+
onFileUpload,
18+
class: className = ''
19+
}: Props = $props();
20+
21+
const fileUploadTooltipText = !supportsVision()
22+
? 'Text files and PDFs supported. Images, audio, and video require vision models.'
23+
: 'Attach files';
24+
25+
function handleFileUpload(fileType?: 'image' | 'audio' | 'file' | 'pdf') {
26+
onFileUpload?.(fileType);
27+
}
28+
</script>
29+
30+
<div class="flex items-center gap-1 {className}">
31+
<DropdownMenu.Root>
32+
<DropdownMenu.Trigger>
33+
<Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
34+
<Tooltip.Trigger>
35+
<Button
36+
type="button"
37+
class="text-muted-foreground bg-transparent hover:bg-foreground/10 hover:text-foreground h-8 w-8 rounded-full p-0"
38+
{disabled}
39+
>
40+
<span class="sr-only">Attach files</span>
41+
<Paperclip class="h-4 w-4" />
42+
</Button>
43+
</Tooltip.Trigger>
44+
<Tooltip.Content>
45+
<p>{fileUploadTooltipText}</p>
46+
</Tooltip.Content>
47+
</Tooltip.Root>
48+
</DropdownMenu.Trigger>
49+
<DropdownMenu.Content align="start" class="w-48">
50+
<Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
51+
<Tooltip.Trigger class="w-full">
52+
<DropdownMenu.Item
53+
class="flex items-center gap-2 cursor-pointer"
54+
onclick={() => handleFileUpload('image')}
55+
disabled={!supportsVision()}
56+
>
57+
<Image class="h-4 w-4" />
58+
<span>Images</span>
59+
</DropdownMenu.Item>
60+
</Tooltip.Trigger>
61+
{#if !supportsVision()}
62+
<Tooltip.Content>
63+
<p>Images require vision models to be processed</p>
64+
</Tooltip.Content>
65+
{/if}
66+
</Tooltip.Root>
67+
<DropdownMenu.Item
68+
class="flex items-center gap-2 cursor-pointer"
69+
onclick={() => handleFileUpload('audio')}
70+
>
71+
<Volume2 class="h-4 w-4" />
72+
<span>Audio Files</span>
73+
</DropdownMenu.Item>
74+
<DropdownMenu.Item
75+
class="flex items-center gap-2 cursor-pointer"
76+
onclick={() => handleFileUpload('file')}
77+
>
78+
<FileText class="h-4 w-4" />
79+
<span>Text Files</span>
80+
</DropdownMenu.Item>
81+
<Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
82+
<Tooltip.Trigger class="w-full">
83+
<DropdownMenu.Item
84+
class="flex items-center gap-2 cursor-pointer"
85+
onclick={() => handleFileUpload('pdf')}
86+
>
87+
<File class="h-4 w-4" />
88+
<span>PDF Files</span>
89+
</DropdownMenu.Item>
90+
</Tooltip.Trigger>
91+
{#if !supportsVision()}
92+
<Tooltip.Content>
93+
<p>PDFs will be converted to text. Image-based PDFs may not work properly.</p>
94+
</Tooltip.Content>
95+
{/if}
96+
</Tooltip.Root>
97+
</DropdownMenu.Content>
98+
</DropdownMenu.Root>
99+
</div>

0 commit comments

Comments
 (0)