feat: add drag and drop file support (#64)

This commit is contained in:
2026-01-24 14:02:36 -08:00
committed by Naomi Carrigan
parent a191bdef23
commit daa7317aab
+112 -17
View File
@@ -37,6 +37,7 @@
let matchingCommands = $state<SlashCommand[]>([]); let matchingCommands = $state<SlashCommand[]>([]);
let selectedCommandIndex = $state(0); let selectedCommandIndex = $state(0);
let attachments = $state<Attachment[]>([]); let attachments = $state<Attachment[]>([]);
let isDragging = $state(false);
// Input history state // Input history state
let inputHistory = $state<string[]>([]); let inputHistory = $state<string[]>([]);
@@ -301,6 +302,79 @@ User: ${formattedMessage}`;
claudeStore.removeAttachment(id); claudeStore.removeAttachment(id);
} }
function getFileTypeFromExtension(extension: string): "image" | "document" | "other" {
const imageExtensions = ["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"];
const documentExtensions = ["pdf", "txt", "md", "doc", "docx", "csv", "json", "xml"];
if (imageExtensions.includes(extension)) {
return "image";
} else if (documentExtensions.includes(extension)) {
return "document";
}
return "other";
}
function handleDragEnter(event: DragEvent) {
event.preventDefault();
event.stopPropagation();
if (event.dataTransfer?.types.includes("Files")) {
isDragging = true;
}
}
function handleDragOver(event: DragEvent) {
event.preventDefault();
event.stopPropagation();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = "copy";
}
}
function handleDragLeave(event: DragEvent) {
event.preventDefault();
event.stopPropagation();
// Only set isDragging to false if we're leaving the form element entirely
const relatedTarget = event.relatedTarget as Node | null;
const currentTarget = event.currentTarget as HTMLElement;
if (!relatedTarget || !currentTarget.contains(relatedTarget)) {
isDragging = false;
}
}
function handleDrop(event: DragEvent) {
event.preventDefault();
event.stopPropagation();
isDragging = false;
const files = event.dataTransfer?.files;
if (!files || files.length === 0) return;
for (const file of files) {
const filename = file.name;
const extension = filename.split(".").pop()?.toLowerCase() || "";
const fileType = getFileTypeFromExtension(extension);
// Create attachment from dropped file
// Note: For dropped files, we create a preview URL for images
let previewUrl: string | undefined;
if (fileType === "image") {
previewUrl = URL.createObjectURL(file);
}
const attachment: Attachment = {
id: `attachment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
filename,
path: (file as File & { path?: string }).path || file.name,
size: file.size,
type: fileType,
mimeType: file.type,
previewUrl,
};
claudeStore.addAttachment(attachment);
}
}
async function handleFilePicker() { async function handleFilePicker() {
try { try {
const selected = await open({ const selected = await open({
@@ -348,29 +422,15 @@ User: ${formattedMessage}`;
const files = Array.isArray(selected) ? selected : [selected]; const files = Array.isArray(selected) ? selected : [selected];
for (const filePath of files) { for (const filePath of files) {
// Get file info
const filename = filePath.split(/[/\\]/).pop() || "unknown"; const filename = filePath.split(/[/\\]/).pop() || "unknown";
const extension = filename.split(".").pop()?.toLowerCase() || ""; const extension = filename.split(".").pop()?.toLowerCase() || "";
const fileType = getFileTypeFromExtension(extension);
// Determine file type
const imageExtensions = ["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"];
const documentExtensions = ["pdf", "txt", "md", "doc", "docx", "csv", "json", "xml"];
let fileType: "image" | "document" | "other";
if (imageExtensions.includes(extension)) {
fileType = "image";
} else if (documentExtensions.includes(extension)) {
fileType = "document";
} else {
fileType = "other";
}
// Create attachment
const attachment: Attachment = { const attachment: Attachment = {
id: `attachment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, id: `attachment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
filename, filename,
path: filePath, path: filePath,
size: 0, // We don't have easy access to file size from the dialog size: 0,
type: fileType, type: fileType,
}; };
@@ -444,7 +504,15 @@ User: ${formattedMessage}`;
} }
</script> </script>
<form onsubmit={handleSubmit} class="input-bar"> <form
onsubmit={handleSubmit}
ondragenter={handleDragEnter}
ondragover={handleDragOver}
ondragleave={handleDragLeave}
ondrop={handleDrop}
class="input-bar"
class:is-dragging={isDragging}
>
<AttachmentPreview {attachments} onRemove={handleRemoveAttachment} /> <AttachmentPreview {attachments} onRemove={handleRemoveAttachment} />
<div class="input-controls flex gap-2 mb-2"> <div class="input-controls flex gap-2 mb-2">
@@ -532,6 +600,33 @@ User: ${formattedMessage}`;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
position: relative;
transition: border-color 0.2s, background 0.2s;
}
.input-bar.is-dragging {
background: var(--bg-secondary);
border: 2px dashed var(--accent-primary);
border-radius: 12px;
padding: 8px;
}
.input-bar.is-dragging::before {
content: "Drop files here";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 18px;
font-weight: 500;
color: var(--accent-primary);
pointer-events: none;
z-index: 10;
}
.input-bar.is-dragging > * {
opacity: 0.3;
pointer-events: none;
} }
.input-controls { .input-controls {