Skip to content

Commit 009f77b

Browse files
committed
refactor: Decouple chat attachments display into components
Improves code organization and reusability by introducing separate components for displaying image and file attachments in the chat interface. This change simplifies the ChatAttachmentsList component and enhances the maintainability of the codebase.
1 parent 028fb8c commit 009f77b

File tree

8 files changed

+167
-67
lines changed

8 files changed

+167
-67
lines changed

tools/server/webui/src/lib/components/MarkdownContent.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
1717
let containerRef = $state<HTMLDivElement>();
1818
let processedHtml = $state('');
19-
20-
const processor = $derived(() => {
19+
20+
let processor = $derived(() => {
2121
return remark()
2222
.use(remarkGfm) // GitHub Flavored Markdown
2323
.use(remarkBreaks) // Convert line breaks to <br>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<script lang="ts">
2+
import { Button } from '$lib/components/ui/button';
3+
import { X } from '@lucide/svelte';
4+
5+
interface Props {
6+
id: string;
7+
name: string;
8+
type: string;
9+
size?: number;
10+
readonly?: boolean;
11+
onRemove?: (id: string) => void;
12+
class?: string;
13+
}
14+
15+
let {
16+
id,
17+
name,
18+
type,
19+
size,
20+
readonly = false,
21+
onRemove,
22+
class: className = ''
23+
}: Props = $props();
24+
25+
function formatFileSize(bytes: number): string {
26+
if (bytes === 0) return '0 Bytes';
27+
const k = 1024;
28+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
29+
const i = Math.floor(Math.log(bytes) / Math.log(k));
30+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
31+
}
32+
33+
function getFileTypeLabel(fileType: string): string {
34+
return fileType.split('/').pop()?.toUpperCase() || 'FILE';
35+
}
36+
</script>
37+
38+
<div class="bg-muted border-border flex items-center gap-2 rounded-lg border p-2 {className}">
39+
<div class="bg-primary/10 text-primary flex h-8 w-8 items-center justify-center rounded text-xs font-medium">
40+
{getFileTypeLabel(type)}
41+
</div>
42+
<div class="flex flex-col">
43+
<span class="text-foreground text-sm font-medium truncate max-w-48">{name}</span>
44+
{#if size}
45+
<span class="text-muted-foreground text-xs">{formatFileSize(size)}</span>
46+
{/if}
47+
</div>
48+
{#if !readonly}
49+
<Button
50+
type="button"
51+
variant="ghost"
52+
size="sm"
53+
class="h-6 w-6 p-0"
54+
onclick={() => onRemove?.(id)}
55+
>
56+
<X class="h-3 w-3" />
57+
</Button>
58+
{/if}
59+
</div>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<script lang="ts">
2+
import { Button } from '$lib/components/ui/button';
3+
import { X } from '@lucide/svelte';
4+
5+
interface Props {
6+
id: string;
7+
name: string;
8+
preview: string;
9+
size?: number;
10+
readonly?: boolean;
11+
onRemove?: (id: string) => void;
12+
class?: string;
13+
// Customizable size props
14+
width?: string;
15+
height?: string;
16+
imageClass?: string;
17+
}
18+
19+
let {
20+
id,
21+
name,
22+
preview,
23+
size,
24+
readonly = false,
25+
onRemove,
26+
class: className = '',
27+
// Default to small size for form previews
28+
width = 'w-auto',
29+
height = 'h-24',
30+
imageClass = ''
31+
}: Props = $props();
32+
33+
function formatFileSize(bytes: number): string {
34+
if (bytes === 0) return '0 Bytes';
35+
const k = 1024;
36+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
37+
const i = Math.floor(Math.log(bytes) / Math.log(k));
38+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
39+
}
40+
</script>
41+
42+
<div class="bg-muted border-border relative rounded-lg border overflow-hidden {className}">
43+
<img
44+
src={preview}
45+
alt={name}
46+
class="{height} {width} object-cover {imageClass}"
47+
/>
48+
{#if !readonly}
49+
<div class="absolute top-1 right-1 opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center">
50+
<Button
51+
type="button"
52+
variant="ghost"
53+
size="sm"
54+
class="h-6 w-6 p-0 bg-white/20 hover:bg-white/30 text-white"
55+
onclick={() => onRemove?.(id)}
56+
>
57+
<X class="h-3 w-3" />
58+
</Button>
59+
</div>
60+
{/if}
61+
<div class="absolute bottom-0 left-0 right-0 bg-black/60 text-white p-1">
62+
{#if size}
63+
<p class="text-xs opacity-80">{formatFileSize(size)}</p>
64+
{/if}
65+
</div>
66+
</div>

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

Lines changed: 32 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,37 @@
11
<script lang="ts">
2-
import { Button } from '$lib/components/ui/button';
3-
import { X } from '@lucide/svelte';
2+
import { ChatAttachmentImagePreview, ChatAttachmentFilePreview } from '$lib/components';
43
import type { ChatUploadedFile } from '$lib/types/chat.d.ts';
54
import type { DatabaseMessageExtra } from '$lib/types/database.d.ts';
65
76
interface Props {
7+
class?: string;
88
// For ChatForm - pending uploads
99
uploadedFiles?: ChatUploadedFile[];
1010
onFileRemove?: (fileId: string) => void;
1111
// For ChatMessage - stored attachments
1212
attachments?: DatabaseMessageExtra[];
1313
readonly?: boolean;
14-
class?: string;
14+
// Image size customization
15+
imageHeight?: string;
16+
imageWidth?: string;
17+
imageClass?: string;
1518
}
1619
1720
let {
18-
uploadedFiles = [],
21+
uploadedFiles = $bindable([]),
1922
onFileRemove,
2023
attachments = [],
2124
readonly = false,
22-
class: className = ''
25+
class: className = '',
26+
// Default to small size for form previews
27+
imageHeight = 'h-24',
28+
imageWidth = 'w-auto',
29+
imageClass = ''
2330
}: Props = $props();
2431
2532
let displayItems = $derived(getDisplayItems());
2633
27-
function formatFileSize(bytes: number): string {
28-
if (bytes === 0) return '0 Bytes';
29-
const k = 1024;
30-
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
31-
const i = Math.floor(Math.log(bytes) / Math.log(k));
32-
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
33-
}
34+
3435
3536
function getDisplayItems() {
3637
const items: Array<{
@@ -89,54 +90,26 @@
8990
<div class="flex flex-wrap items-start gap-3 {className}">
9091
{#each displayItems as item (item.id)}
9192
{#if item.isImage && item.preview}
92-
<div class="bg-muted border-border relative rounded-lg border overflow-hidden">
93-
<img
94-
src={item.preview}
95-
alt={item.name}
96-
class="h-24 w-24 object-cover"
97-
/>
98-
{#if !readonly}
99-
<div class="absolute top-1 right-1 opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center">
100-
<Button
101-
type="button"
102-
variant="ghost"
103-
size="sm"
104-
class="h-6 w-6 p-0 bg-white/20 hover:bg-white/30 text-white"
105-
onclick={() => onFileRemove?.(item.id)}
106-
>
107-
<X class="h-3 w-3" />
108-
</Button>
109-
</div>
110-
{/if}
111-
<div class="absolute bottom-0 left-0 right-0 bg-black/60 text-white p-1">
112-
{#if item.size}
113-
<p class="text-xs opacity-80">{formatFileSize(item.size)}</p>
114-
{/if}
115-
</div>
116-
</div>
93+
<ChatAttachmentImagePreview
94+
id={item.id}
95+
name={item.name}
96+
preview={item.preview}
97+
size={item.size}
98+
readonly={readonly}
99+
onRemove={onFileRemove}
100+
height={imageHeight}
101+
width={imageWidth}
102+
imageClass={imageClass}
103+
/>
117104
{:else}
118-
<div class="bg-muted border-border flex items-center gap-2 rounded-lg border p-2">
119-
<div class="bg-primary/10 text-primary flex h-8 w-8 items-center justify-center rounded text-xs font-medium">
120-
{item.type.split('/').pop()?.toUpperCase() || 'FILE'}
121-
</div>
122-
<div class="flex flex-col">
123-
<span class="text-foreground text-sm font-medium truncate max-w-48">{item.name}</span>
124-
{#if item.size}
125-
<span class="text-muted-foreground text-xs">{formatFileSize(item.size)}</span>
126-
{/if}
127-
</div>
128-
{#if !readonly}
129-
<Button
130-
type="button"
131-
variant="ghost"
132-
size="sm"
133-
class="h-6 w-6 p-0"
134-
onclick={() => onFileRemove?.(item.id)}
135-
>
136-
<X class="h-3 w-3" />
137-
</Button>
138-
{/if}
139-
</div>
105+
<ChatAttachmentFilePreview
106+
id={item.id}
107+
name={item.name}
108+
type={item.type}
109+
size={item.size}
110+
readonly={readonly}
111+
onRemove={onFileRemove}
112+
/>
140113
{/if}
141114
{/each}
142115
</div>

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
onSend,
2525
onStop,
2626
showHelperText = true,
27-
uploadedFiles = [],
27+
uploadedFiles = $bindable([]),
2828
onFileUpload,
2929
onFileRemove
3030
}: Props = $props();
@@ -39,6 +39,7 @@
3939
4040
onSend?.(message.trim(), uploadedFiles);
4141
message = '';
42+
uploadedFiles = [];
4243
4344
if (textareaElement) {
4445
textareaElement.style.height = 'auto';
@@ -53,6 +54,7 @@
5354
5455
onSend?.(message.trim(), uploadedFiles);
5556
message = '';
57+
uploadedFiles = [];
5658
5759
if (textareaElement) {
5860
textareaElement.style.height = 'auto';
@@ -90,9 +92,8 @@
9092
onsubmit={handleSubmit}
9193
class="border bg-muted/30 border-border/40 focus-within:border-primary/40 bg-background dark:bg-muted border-radius-bottom-none mx-auto max-w-4xl overflow-hidden rounded-3xl {className}"
9294
>
93-
<!-- File previews -->
9495
<ChatAttachmentsList
95-
uploadedFiles={uploadedFiles}
96+
bind:uploadedFiles={uploadedFiles}
9697
onFileRemove={onFileRemove}
9798
class="mb-3 px-5 pt-3"
9899
/>

tools/server/webui/src/lib/components/chat/ChatMessage.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
<ChatAttachmentsList
139139
attachments={message.extra}
140140
readonly={true}
141+
imageHeight="h-80"
141142
/>
142143
</div>
143144
{/if}

tools/server/webui/src/lib/components/chat/ChatScreen.svelte

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@
2828
2929
async function handleSendMessage(message: string, files?: ChatUploadedFile[]) {
3030
const extras = files ? await convertFilesToExtras(files) : undefined;
31-
3231
await sendMessage(message, extras);
33-
uploadedFiles = [];
3432
}
3533
3634
async function convertFilesToExtras(files: ChatUploadedFile[]): Promise<DatabaseMessageExtra[]> {
@@ -184,7 +182,7 @@
184182
showHelperText={false}
185183
onSend={handleSendMessage}
186184
onStop={() => stopGeneration()}
187-
uploadedFiles={uploadedFiles}
185+
bind:uploadedFiles={uploadedFiles}
188186
onFileUpload={handleFileUpload}
189187
onFileRemove={handleFileRemove}
190188
/>

tools/server/webui/src/lib/components/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export { default as ChatAttachmentsList } from './chat/ChatAttachmentsList.svelte';
2+
export { default as ChatAttachmentFilePreview } from './chat/ChatAttachmentFilePreview.svelte';
3+
export { default as ChatAttachmentImagePreview } from './chat/ChatAttachmentImagePreview.svelte';
24
export { default as ChatForm } from './chat/ChatForm.svelte';
35
export { default as ChatMessage } from './chat/ChatMessage.svelte';
46
export { default as ChatMessages } from './chat/ChatMessages.svelte';

0 commit comments

Comments
 (0)