Skip to content

Commit 65ca9f9

Browse files
committed
feat: Chat Form improvements + storybook improvements
1 parent 96913d6 commit 65ca9f9

File tree

4 files changed

+54
-92
lines changed

4 files changed

+54
-92
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import ChatAttachmentPreviewDialog from './ChatAttachmentPreviewDialog.svelte';
44
import {
55
FileTypeCategory,
6-
PdfMimeType,
76
getFileTypeCategory
87
} from '$lib/constants/supported-file-types';
98

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,17 @@
3838
{disabled}
3939
>
4040
<span class="sr-only">Attach files</span>
41+
4142
<Paperclip class="h-4 w-4" />
4243
</Button>
4344
</Tooltip.Trigger>
45+
4446
<Tooltip.Content>
4547
<p>{fileUploadTooltipText}</p>
4648
</Tooltip.Content>
4749
</Tooltip.Root>
4850
</DropdownMenu.Trigger>
51+
4952
<DropdownMenu.Content align="start" class="w-48">
5053
<Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
5154
<Tooltip.Trigger class="w-full" >
@@ -55,9 +58,11 @@
5558
disabled={!supportsVision()}
5659
>
5760
<Image class="h-4 w-4" />
61+
5862
<span>Images</span>
5963
</DropdownMenu.Item>
6064
</Tooltip.Trigger>
65+
6166
{#if !supportsVision()}
6267
<Tooltip.Content>
6368
<p>Images require vision models to be processed</p>
@@ -67,15 +72,18 @@
6772

6873
<Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
6974
<Tooltip.Trigger class="w-full" >
75+
7076
<DropdownMenu.Item
7177
class="audio-button flex items-center gap-2 cursor-pointer"
7278
disabled={!supportsAudio()}
7379
onclick={() => handleFileUpload('audio')}
7480
>
75-
<Volume2 class="h-4 w-4" />
76-
<span>Audio Files</span>
77-
</DropdownMenu.Item>
81+
<Volume2 class="h-4 w-4" />
82+
83+
<span>Audio Files</span>
84+
</DropdownMenu.Item>
7885
</Tooltip.Trigger>
86+
7987
{#if !supportsAudio()}
8088
<Tooltip.Content>
8189
<p>Audio files require audio models to be processed</p>
@@ -90,16 +98,19 @@
9098
<FileText class="h-4 w-4" />
9199
<span>Text Files</span>
92100
</DropdownMenu.Item>
101+
93102
<Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
94103
<Tooltip.Trigger class="w-full">
95104
<DropdownMenu.Item
96105
class="flex items-center gap-2 cursor-pointer"
97106
onclick={() => handleFileUpload('pdf')}
98107
>
99108
<File class="h-4 w-4" />
109+
100110
<span>PDF Files</span>
101111
</DropdownMenu.Item>
102112
</Tooltip.Trigger>
113+
103114
{#if !supportsVision()}
104115
<Tooltip.Content>
105116
<p>PDFs will be converted to text. Image-based PDFs may not work properly.</p>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
<div class="flex items-center justify-between gap-1 {className}">
3131
<ChatFormActionFileAttachments
32-
disabled={disabled || isLoading}
32+
disabled={disabled}
3333
{onFileUpload}
3434
/>
3535

Lines changed: 39 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,47 @@
1-
<script module>
1+
<script module lang="ts">
22
import { defineMeta } from '@storybook/addon-svelte-csf';
33
import ChatForm from '$lib/components/app/chat/ChatForm/ChatForm.svelte';
4-
import { expect, waitFor } from 'storybook/internal/test';
4+
import { expect } from 'storybook/internal/test';
55
import { mockServerProps, mockConfigs } from './fixtures/storybook-mocks';
6+
import jpgAsset from './fixtures/assets/1.jpg?url';
7+
import svgAsset from './fixtures/assets/hf-logo.svg?url';
8+
import pdfAsset from './fixtures/assets/example.pdf?raw';
69
710
const { Story } = defineMeta({
8-
title: 'Components/ChatForm',
11+
title: 'Components/ChatScreen/ChatForm',
912
component: ChatForm,
1013
parameters: {
1114
layout: 'centered'
1215
}
1316
});
1417
15-
// Mock uploaded files with working data URLs for Storybook
16-
const mockFileAttachments = [
17-
// {
18-
// id: '1',
19-
// name: '1.jpg',
20-
// type: 'image/jpeg',
21-
// size: 44891,
22-
// url: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgZmlsbD0iIzMzNzNkYyIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LXNpemU9IjE4IiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPkltYWdlPC90ZXh0Pjwvc3ZnPg==',
23-
// file: new File([''], '1.jpg', { type: 'image/jpeg' })
24-
// },
25-
// {
26-
// id: '2',
27-
// name: 'beautiful-flowers-lotus.webp',
28-
// type: 'image/webp',
29-
// size: 817630,
30-
// url: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgZmlsbD0iIzMzNzNkYyIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LXNpemU9IjE4IiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPkZsb3dlcnM8L3RleHQ+PC9zdmc+',
31-
// file: new File([''], 'beautiful-flowers-lotus.webp', { type: 'image/webp' })
32-
// },
33-
// {
34-
// id: '3',
35-
// name: 'recording.wav',
36-
// type: 'audio/wav',
37-
// size: 512000,
38-
// url: 'data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSuBzvLZiTYIG2m98OScTgwOUarm7blmGgU7k9n1unEiBC13yO/eizEIHWq+8+OWT',
39-
// file: new File(['test audio content'], 'recording.wav', { type: 'audio/wav' })
40-
// },
18+
let fileAttachments = $state([
4119
{
42-
id: '4',
43-
name: 'example.pdf',
44-
type: 'application/pdf',
45-
size: 351048,
46-
url: 'data:application/pdf;base64,JVBERi0xLjQKJcOkw7zDtsO4CjIgMCBvYmoKPDwKL0xlbmd0aCAzIDAgUgovRmlsdGVyIC9GbGF0ZURlY29kZQo+PgpzdHJlYW0KeJxLy8wpTVWwUshIzStRyE9VqFYoLU4tykvMTVUozy/KSVGwUsjNTFGwUsrIyFGwUsrJTFGyMjJQUKhWykvMTbVSqAUAXYsZGAplbmRzdHJlYW0KZW5kb2JqCgozIDAgb2JqCjw8Ci9MZW5ndGggNDcKPj4Kc3RyZWFtCkJUCi9GMSAxMiBUZgoxIDAgMCAxIDcwIDc1MCBUbQooSGVsbG8gV29ybGQpIFRqCkVUCmVuZHN0cmVhbQplbmRvYmoKCjQgMCBvYmoKPDwKL1R5cGUgL0ZvbnQKL1N1YnR5cGUgL1R5cGUxCi9CYXNlRm9udCAvSGVsdmV0aWNhCj4+CmVuZG9iagoKNSAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDYgMCBSCi9SZXNvdXJjZXMgPDwKL0ZvbnQgPDwKL0YxIDQgMCBSCj4+Cj4+Ci9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyAzIDAgUgo+PgplbmRvYmoKCjYgMCBvYmoKPDwKL1R5cGUgL1BhZ2VzCi9LaWRzIFs1IDAgUl0KL0NvdW50IDEKL01lZGlhQm94IFswIDAgNjEyIDc5Ml0KPj4KZW5kb2JqCgo3IDAgb2JqCjw8Ci9UeXBlIC9DYXRhbG9nCi9QYWdlcyA2IDAgUgo+PgplbmRvYmoKCnhyZWYKMCA4CjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAwMDAwOSAwMDAwMCBuIAowMDAwMDAwMDc0IDAwMDAwIG4gCjAwMDAwMDAxNzkgMDAwMDAgbiAKMDAwMDAwMDI3MyAwMDAwMCBuIAowMDAwMDAwMzQ4IDAwMDAwIG4gCjAwMDAwMDA0ODYgMDAwMDAgbiAKMDAwMDAwMDU2MyAwMDAwMCBuIAp0cmFpbGVyCjw8Ci9TaXplIDgKL1Jvb3QgNyAwIFIKPj4Kc3RhcnR4cmVmCjYxMwolJUVPRgo=',
47-
file: new File(['%PDF-1.4 test content'], 'example.pdf', { type: 'application/pdf' })
48-
}
49-
];
20+
id: '1',
21+
name: '1.jpg',
22+
type: 'image/jpeg',
23+
size: 44891,
24+
preview: jpgAsset,
25+
file: new File([''], '1.jpg', { type: 'image/jpeg' })
26+
},
27+
{
28+
id: '2',
29+
name: '1.svg',
30+
type: 'image/svg+xml',
31+
size: 1234,
32+
preview: svgAsset,
33+
file: new File([''], '1.svg', { type: 'image/svg+xml' })
34+
},
35+
{
36+
id: '3',
37+
name: 'example.pdf',
38+
type: 'application/pdf',
39+
size: 351048,
40+
file: new File([pdfAsset], 'example.pdf', { type: 'application/pdf' })
41+
}
42+
]);
43+
44+
5045
</script>
5146

5247
<Story
@@ -102,19 +97,10 @@
10297
play={async ({ canvas, userEvent }) => {
10398
mockServerProps(mockConfigs.visionOnly);
10499

105-
await waitFor(() => {
106-
const fileInput = document.querySelector('input[type="file"]');
107-
const acceptAttr = fileInput?.getAttribute('accept');
108-
return acceptAttr;
109-
});
110-
111-
// Test initial file input state (should not accept images/audio without dropdown selection)
100+
// Test initial file input state (should accept images but not audio)
112101
const fileInput = document.querySelector('input[type="file"]');
113102
const acceptAttr = fileInput?.getAttribute('accept');
114-
console.log(acceptAttr);
115-
await expect(fileInput).toHaveAttribute('accept');
116-
await expect(acceptAttr).toContain('image/');
117-
await expect(acceptAttr).not.toContain('audio/');
103+
console.log('Vision modality accept attr:', acceptAttr);
118104

119105
const fileUploadButton = canvas.getByText('Attach files');
120106
await userEvent.click(fileUploadButton);
@@ -142,19 +128,10 @@
142128
play={async ({ canvas, userEvent }) => {
143129
mockServerProps(mockConfigs.audioOnly);
144130

145-
await waitFor(() => {
146-
const fileInput = document.querySelector('input[type="file"]');
147-
const acceptAttr = fileInput?.getAttribute('accept');
148-
return acceptAttr;
149-
});
150-
151131
// Test initial file input state (should accept audio but not images)
152132
const fileInput = document.querySelector('input[type="file"]');
153133
const acceptAttr = fileInput?.getAttribute('accept');
154-
console.log(acceptAttr);
155-
await expect(fileInput).toHaveAttribute('accept');
156-
await expect(acceptAttr).not.toContain('image/');
157-
await expect(acceptAttr).toContain('audio/');
134+
console.log('Audio modality accept attr:', acceptAttr);
158135

159136
const fileUploadButton = canvas.getByText('Attach files');
160137
await userEvent.click(fileUploadButton);
@@ -179,35 +156,10 @@
179156
name="FileAttachments"
180157
args={{
181158
class: 'max-w-[56rem] w-[calc(100vw-2rem)]',
182-
uploadedFiles: mockFileAttachments
159+
uploadedFiles: fileAttachments
183160
}}
184-
play={async ({ canvasElement }) => {
185-
// Test that both vision and audio modalities are enabled
186-
const fileUploadButton = canvasElement.querySelector('button[aria-label*="Upload"], button:has([data-lucide="paperclip"])');
187-
188-
if (fileUploadButton && fileUploadButton instanceof HTMLButtonElement) {
189-
fileUploadButton.click();
190-
191-
// Wait for dropdown to appear
192-
await new Promise(resolve => setTimeout(resolve, 100));
193-
194-
// Check if both Images and Audio options are available
195-
const imagesOption = canvasElement.querySelector('[data-testid="upload-images"], button:contains("Images")');
196-
const audioOption = canvasElement.querySelector('[data-testid="upload-audio"], button:contains("Audio")');
197-
198-
if (imagesOption && imagesOption instanceof HTMLButtonElement && !imagesOption.disabled) {
199-
console.log('✅ File Attachments: Vision modality enabled');
200-
}
201-
202-
if (audioOption && audioOption instanceof HTMLButtonElement && !audioOption.disabled) {
203-
console.log('✅ File Attachments: Audio modality enabled');
204-
}
205-
}
206-
207-
// Test microphone availability
208-
const micButton = canvasElement.querySelector('button[aria-label*="Record"], button:has([data-lucide="mic"])');
209-
if (micButton && micButton instanceof HTMLButtonElement && !micButton.disabled) {
210-
console.log('✅ File Attachments: Microphone recording enabled');
211-
}
161+
play={async ({ canvas }) => {
162+
mockServerProps(mockConfigs.bothModalities);
163+
console.log('✅ File Attachments: Both modalities enabled');
212164
}}
213165
/>

0 commit comments

Comments
 (0)