generated from nhcarrigan/template
feat: add drag and drop file support (#64)
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user