Skip to content

Commit 98b55cd

Browse files
committed
feat: Handles empty file uploads gracefully
1 parent c81046e commit 98b55cd

File tree

4 files changed

+99
-17
lines changed

4 files changed

+99
-17
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<script lang="ts">
2+
import * as AlertDialog from '$lib/components/ui/alert-dialog';
3+
import { FileX } from '@lucide/svelte';
4+
5+
interface Props {
6+
open: boolean;
7+
emptyFiles: string[];
8+
onOpenChange?: (open: boolean) => void;
9+
}
10+
11+
let { open = $bindable(), emptyFiles, onOpenChange }: Props = $props();
12+
13+
function handleOpenChange(newOpen: boolean) {
14+
open = newOpen;
15+
onOpenChange?.(newOpen);
16+
}
17+
</script>
18+
19+
<AlertDialog.Root {open} onOpenChange={handleOpenChange}>
20+
<AlertDialog.Content>
21+
<AlertDialog.Header>
22+
<AlertDialog.Title class="flex items-center gap-2">
23+
<FileX class="text-destructive h-5 w-5" />
24+
Empty Files Detected
25+
</AlertDialog.Title>
26+
<AlertDialog.Description>
27+
The following files are empty and have been removed from your attachments:
28+
</AlertDialog.Description>
29+
</AlertDialog.Header>
30+
31+
<div class="space-y-3 text-sm">
32+
<div class="bg-muted rounded-lg p-3">
33+
<div class="mb-2 font-medium">Empty Files:</div>
34+
<ul class="text-muted-foreground list-inside list-disc space-y-1">
35+
{#each emptyFiles as fileName}
36+
<li class="font-mono text-sm">{fileName}</li>
37+
{/each}
38+
</ul>
39+
</div>
40+
41+
<div>
42+
<div class="mb-2 font-medium">What happened:</div>
43+
<ul class="text-muted-foreground list-inside list-disc space-y-1">
44+
<li>Empty files cannot be processed or sent to the AI model</li>
45+
<li>These files have been automatically removed from your attachments</li>
46+
<li>You can try uploading files with content instead</li>
47+
</ul>
48+
</div>
49+
</div>
50+
51+
<AlertDialog.Footer>
52+
<AlertDialog.Action onclick={() => handleOpenChange(false)}>Got it</AlertDialog.Action>
53+
</AlertDialog.Footer>
54+
</AlertDialog.Content>
55+
</AlertDialog.Root>

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

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { processFilesToChatUploaded } from '$lib/utils/process-uploaded-files';
44
import { serverStore } from '$lib/stores/server.svelte';
55
import { isFileTypeSupported } from '$lib/constants/supported-file-types';
6+
import EmptyFileAlertDialog from '$lib/components/app/EmptyFileAlertDialog.svelte';
67
import { filterFilesByModalities } from '$lib/utils/modality-file-validation';
78
import { supportsVision, supportsAudio, serverError, serverLoading } from '$lib/stores/server.svelte';
89
import { ChatForm, ChatScreenHeader, ChatMessages, ServerInfo, ServerErrorSplash, ServerLoadingSplash, SlotsInfo } from '$lib/components/app';
@@ -30,9 +31,8 @@
3031
let uploadedFiles = $state<ChatUploadedFile[]>([]);
3132
let isDragOver = $state(false);
3233
let dragCounter = $state(0);
33-
34-
// Alert Dialog state for file upload errors
3534
let showFileErrorDialog = $state(false);
35+
3636
let fileErrorData = $state<{
3737
generallyUnsupported: File[];
3838
modalityUnsupported: File[];
@@ -47,6 +47,9 @@
4747
4848
let showDeleteDialog = $state(false);
4949
50+
let showEmptyFileDialog = $state(false);
51+
let emptyFileNames = $state<string[]>([]);
52+
5053
const isEmpty = $derived(
5154
showCenteredEmpty && !activeConversation() && activeMessages().length === 0 && !isLoading()
5255
);
@@ -109,7 +112,20 @@
109112
message: string,
110113
files?: ChatUploadedFile[]
111114
): Promise<boolean> {
112-
const extras = files ? await parseFilesToMessageExtras(files) : undefined;
115+
const result = files ? await parseFilesToMessageExtras(files) : undefined;
116+
117+
if (result?.emptyFiles && result.emptyFiles.length > 0) {
118+
emptyFileNames = result.emptyFiles;
119+
showEmptyFileDialog = true;
120+
121+
if (files) {
122+
const emptyFileNamesSet = new Set(result.emptyFiles);
123+
uploadedFiles = uploadedFiles.filter(file => !emptyFileNamesSet.has(file.name));
124+
}
125+
return false;
126+
}
127+
128+
const extras = result?.extras;
113129
114130
// Check context limit using real-time slots data
115131
const contextCheck = await contextService.checkContextLimit();
@@ -133,7 +149,6 @@
133149
}
134150
135151
async function processFiles(files: File[]) {
136-
// First filter by general file type support
137152
const generallySupported: File[] = [];
138153
const generallyUnsupported: File[] = [];
139154
@@ -145,20 +160,17 @@
145160
}
146161
}
147162
148-
// Then filter by model modalities
149163
const { supportedFiles, unsupportedFiles, modalityReasons } =
150164
filterFilesByModalities(generallySupported);
151165
152-
// Combine all unsupported files
153166
const allUnsupportedFiles = [...generallyUnsupported, ...unsupportedFiles];
154167
155168
if (allUnsupportedFiles.length > 0) {
156-
// Determine supported types for current model
157169
const supportedTypes: string[] = ['text files', 'PDFs'];
170+
158171
if (supportsVision()) supportedTypes.push('images');
159172
if (supportsAudio()) supportedTypes.push('audio files');
160173
161-
// Structure error data for better presentation
162174
fileErrorData = {
163175
generallyUnsupported,
164176
modalityUnsupported: unsupportedFiles,
@@ -399,6 +411,16 @@
399411
</AlertDialog.Portal>
400412
</AlertDialog.Root>
401413

414+
<EmptyFileAlertDialog
415+
bind:open={showEmptyFileDialog}
416+
emptyFiles={emptyFileNames}
417+
onOpenChange={(open) => {
418+
if (!open) {
419+
emptyFileNames = [];
420+
}
421+
}}
422+
/>
423+
402424
<style>
403425
.conversation-chat-form {
404426
position: relative;

tools/server/webui/src/lib/utils/convert-files-to-extra.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { convertPDFToImage, convertPDFToText, isPdfMimeType } from "./pdf-processing";
1+
import { convertPDFToImage, convertPDFToText } from "./pdf-processing";
22
import { isSvgMimeType, svgBase64UrlToPngDataURL } from "./svg-to-png";
33
import { isWebpMimeType, webpBase64UrlToPngDataURL } from "./webp-to-png";
44
import { config, settingsStore } from '$lib/stores/settings.svelte';
@@ -24,16 +24,17 @@ function readFileAsBase64(file: File): Promise<string> {
2424
});
2525
}
2626

27-
// Note: This function is now redundant since we use getFileTypeCategory(file.type) === FileTypeCategory.AUDIO
28-
// Keeping for backward compatibility, but consider removing in future cleanup
29-
function isAudioMimeType(mimeType: string): boolean {
30-
return getFileTypeCategory(mimeType) === FileTypeCategory.AUDIO;
27+
28+
export interface FileProcessingResult {
29+
extras: DatabaseMessageExtra[];
30+
emptyFiles: string[];
3131
}
3232

3333
export async function parseFilesToMessageExtras(
3434
files: ChatUploadedFile[]
35-
): Promise<DatabaseMessageExtra[]> {
35+
): Promise<FileProcessingResult> {
3636
const extras: DatabaseMessageExtra[] = [];
37+
const emptyFiles: string[] = [];
3738

3839
for (const file of files) {
3940
if (getFileTypeCategory(file.type) === FileTypeCategory.IMAGE) {
@@ -151,7 +152,11 @@ export async function parseFilesToMessageExtras(
151152
try {
152153
const content = await readFileAsText(file.file);
153154

154-
if (isLikelyTextFile(content)) {
155+
// Check if file is empty
156+
if (content.trim() === '') {
157+
console.warn(`File ${file.name} is empty and will be skipped`);
158+
emptyFiles.push(file.name);
159+
} else if (isLikelyTextFile(content)) {
155160
extras.push({
156161
type: 'textFile',
157162
name: file.name,
@@ -166,5 +171,5 @@ export async function parseFilesToMessageExtras(
166171
}
167172
}
168173

169-
return extras;
174+
return { extras, emptyFiles };
170175
}

tools/server/webui/src/lib/utils/text-files.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export async function readFileAsText(file: File): Promise<string> {
2424
return new Promise((resolve, reject) => {
2525
const reader = new FileReader();
2626
reader.onload = (event) => {
27-
if (event.target?.result) {
27+
if (event.target?.result !== null && event.target?.result !== undefined) {
2828
resolve(event.target.result as string);
2929
} else {
3030
reject(new Error('Failed to read file'));

0 commit comments

Comments
 (0)