|
10 | 10 | } from '$lib/stores/chat.svelte'; |
11 | 11 | import { onMount } from 'svelte'; |
12 | 12 | import { fly, slide } from 'svelte/transition'; |
| 13 | + import { Upload } from '@lucide/svelte'; |
| 14 | + import type { ChatUploadedFile } from '$lib/types/chat.d.ts'; |
13 | 15 |
|
14 | 16 | let { showCenteredEmpty = false } = $props(); |
15 | 17 | let chatScrollContainer: HTMLDivElement | undefined = $state(); |
16 | 18 | let scrollInterval: ReturnType<typeof setInterval> | undefined; |
17 | 19 | let autoScrollEnabled = $state(true); |
| 20 | + let uploadedFiles = $state<ChatUploadedFile[]>([]); |
| 21 | + let isDragOver = $state(false); |
| 22 | + let dragCounter = $state(0); |
18 | 23 |
|
19 | 24 | const isEmpty = $derived( |
20 | 25 | showCenteredEmpty && !activeConversation() && activeMessages().length === 0 && !isLoading() |
|
58 | 63 | scrollInterval = undefined; |
59 | 64 | } |
60 | 65 | }) |
| 66 | +
|
| 67 | + function processFiles(files: File[]) { |
| 68 | + files.forEach((file) => { |
| 69 | + const id = Date.now().toString() + Math.random().toString(36).substr(2, 9); |
| 70 | + const uploadedFile: ChatUploadedFile = { |
| 71 | + id, |
| 72 | + name: file.name, |
| 73 | + size: file.size, |
| 74 | + type: file.type, |
| 75 | + file |
| 76 | + }; |
| 77 | +
|
| 78 | + // Create preview for images |
| 79 | + if (file.type.startsWith('image/')) { |
| 80 | + const reader = new FileReader(); |
| 81 | + reader.onload = (e) => { |
| 82 | + uploadedFile.preview = e.target?.result as string; |
| 83 | + // Add file to array after preview is ready |
| 84 | + uploadedFiles = [...uploadedFiles, uploadedFile]; |
| 85 | + }; |
| 86 | + reader.readAsDataURL(file); |
| 87 | + } else { |
| 88 | + // For non-image files, add immediately |
| 89 | + uploadedFiles = [...uploadedFiles, uploadedFile]; |
| 90 | + } |
| 91 | + }); |
| 92 | + } |
| 93 | +
|
| 94 | + function handleFileUpload(files: File[]) { |
| 95 | + processFiles(files); |
| 96 | + } |
| 97 | +
|
| 98 | + function handleFileRemove(fileId: string) { |
| 99 | + uploadedFiles = uploadedFiles.filter(f => f.id !== fileId); |
| 100 | + } |
| 101 | +
|
| 102 | + function handleDragEnter(event: DragEvent) { |
| 103 | + event.preventDefault(); |
| 104 | + dragCounter++; |
| 105 | + if (event.dataTransfer?.types.includes('Files')) { |
| 106 | + isDragOver = true; |
| 107 | + } |
| 108 | + } |
| 109 | +
|
| 110 | + function handleDragLeave(event: DragEvent) { |
| 111 | + event.preventDefault(); |
| 112 | + dragCounter--; |
| 113 | + if (dragCounter === 0) { |
| 114 | + isDragOver = false; |
| 115 | + } |
| 116 | + } |
| 117 | +
|
| 118 | + function handleDragOver(event: DragEvent) { |
| 119 | + event.preventDefault(); |
| 120 | + } |
| 121 | +
|
| 122 | + function handleDrop(event: DragEvent) { |
| 123 | + event.preventDefault(); |
| 124 | + isDragOver = false; |
| 125 | + dragCounter = 0; |
| 126 | + |
| 127 | + if (event.dataTransfer?.files) { |
| 128 | + processFiles(Array.from(event.dataTransfer.files)); |
| 129 | + } |
| 130 | + } |
61 | 131 | </script> |
62 | 132 |
|
| 133 | +<!-- Drag and drop overlay --> |
| 134 | +{#if isDragOver} |
| 135 | + <div class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"> |
| 136 | + <div class="bg-background border-border flex flex-col items-center justify-center rounded-2xl border-2 border-dashed p-12 shadow-lg"> |
| 137 | + <Upload class="text-muted-foreground mb-4 h-12 w-12" /> |
| 138 | + <p class="text-foreground text-lg font-medium">Attach a file</p> |
| 139 | + <p class="text-muted-foreground text-sm">Drop your files here to upload</p> |
| 140 | + </div> |
| 141 | + </div> |
| 142 | +{/if} |
| 143 | + |
63 | 144 | {#if !isEmpty} |
64 | | - <div class="flex h-full flex-col overflow-y-auto" bind:this={chatScrollContainer} onscroll={handleScroll}> |
| 145 | + <div |
| 146 | + class="flex h-full flex-col overflow-y-auto" |
| 147 | + bind:this={chatScrollContainer} |
| 148 | + onscroll={handleScroll} |
| 149 | + ondragenter={handleDragEnter} |
| 150 | + ondragleave={handleDragLeave} |
| 151 | + ondragover={handleDragOver} |
| 152 | + ondrop={handleDrop} |
| 153 | + role="main" |
| 154 | + aria-label="Chat interface with file drop zone" |
| 155 | + > |
65 | 156 | <ChatMessages class="mb-36" messages={activeMessages()} /> |
66 | 157 |
|
67 | 158 | <div |
|
74 | 165 | showHelperText={false} |
75 | 166 | onSend={handleSendMessage} |
76 | 167 | onStop={() => stopGeneration()} |
| 168 | + uploadedFiles={uploadedFiles} |
| 169 | + onFileUpload={handleFileUpload} |
| 170 | + onFileRemove={handleFileRemove} |
77 | 171 | /> |
78 | 172 | </div> |
79 | 173 | </div> |
80 | 174 | </div> |
81 | 175 | {:else} |
82 | | - <div class="flex h-full items-center justify-center"> |
| 176 | + <div |
| 177 | + class="flex h-full items-center justify-center" |
| 178 | + ondragenter={handleDragEnter} |
| 179 | + ondragleave={handleDragLeave} |
| 180 | + ondragover={handleDragOver} |
| 181 | + ondrop={handleDrop} |
| 182 | + role="main" |
| 183 | + aria-label="Welcome screen with file drop zone" |
| 184 | + > |
83 | 185 | <div class="w-full max-w-2xl px-4"> |
84 | 186 | <div class="mb-8 text-center" in:fly={{ y: -30, duration: 600 }}> |
85 | 187 | <h1 class="mb-2 text-3xl font-semibold tracking-tight">llama.cpp</h1> |
|
100 | 202 | showHelperText={true} |
101 | 203 | onSend={handleSendMessage} |
102 | 204 | onStop={() => stopGeneration()} |
| 205 | + uploadedFiles={uploadedFiles} |
| 206 | + onFileUpload={handleFileUpload} |
| 207 | + onFileRemove={handleFileRemove} |
103 | 208 | /> |
104 | 209 | </div> |
105 | 210 | </div> |
|
0 commit comments