Unverified 提交 8f87e0f3 authored 作者: Mohamed Aziz Mejri's avatar Mohamed Aziz Mejri 提交者: GitHub

Prompt the user for attachement type after dragging the file (#2563)

<!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2563" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Show a dialog after dragging or pasting files into chat to choose how to attach them—as chat context or upload to codebase. This makes intent explicit and blocks submit, drop, and paste until you choose. - **New Features** - Added FileAttachmentTypeDialog; integrated in ChatInput and HomeChatInput. i18n (en, pt-BR, zh-CN) with singular/plural titles and descriptions. - Updated useAttachments with pendingFiles and confirm/cancel. Drag/paste set pendingFiles; prevent attaching while pending; clearAttachments also clears pendingFiles; submit blocked when dialog is open. - Fixed e2e to select “Attach file as chat context”; dialog buttons use type="button" with focus-visible ring. - **Refactors** - confirmPendingFiles reuses addAttachments to deduplicate logic. <sup>Written for commit b625847b5ed5f82bea4616db27b87b16b5b33613. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
上级 c3c6d3e9
......@@ -154,6 +154,13 @@ test("attach image via drag - chat", async ({ po }) => {
});
}, fileBase64);
// Choose "Attach as chat context" in the attachment type dialog
const chatContextButton = po.page.getByRole("button", {
name: "Attach file as chat context",
});
await expect(chatContextButton).toBeVisible();
await chatContextButton.click();
// submit and verify
await po.sendPrompt("[dump]");
// Note: this should match EXACTLY the server dump from the previous test.
......
......@@ -55,6 +55,7 @@ import { useVersions } from "@/hooks/useVersions";
import { useAttachments } from "@/hooks/useAttachments";
import { AttachmentsList } from "./AttachmentsList";
import { DragDropOverlay } from "./DragDropOverlay";
import { FileAttachmentTypeDialog } from "./FileAttachmentTypeDialog";
import { showExtraFilesToast, showInfo } from "@/lib/toast";
import { useSummarizeInNewChat } from "./SummarizeInNewChatButton";
import { ChatInputControls } from "../ChatInputControls";
......@@ -147,6 +148,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
const {
attachments,
isDraggingOver,
pendingFiles,
handleFileSelect,
removeAttachment,
handleDragOver,
......@@ -154,6 +156,8 @@ export function ChatInput({ chatId }: { chatId?: number }) {
handleDrop,
clearAttachments,
handlePaste,
confirmPendingFiles,
cancelPendingFiles,
} = useAttachments();
// Use the hook to fetch the proposal
......@@ -241,7 +245,8 @@ export function ChatInput({ chatId }: { chatId?: number }) {
if (
(!inputValue.trim() && attachments.length === 0) ||
isStreaming ||
!chatId
!chatId ||
pendingFiles
) {
return;
}
......@@ -554,6 +559,13 @@ export function ChatInput({ chatId }: { chatId?: number }) {
{/* Use the DragDropOverlay component */}
<DragDropOverlay isDraggingOver={isDraggingOver} />
{/* Dialog for choosing attachment type */}
<FileAttachmentTypeDialog
pendingFiles={pendingFiles}
onConfirm={confirmPendingFiles}
onCancel={cancelPendingFiles}
/>
<div className="flex items-start space-x-2 ">
<LexicalChatInput
value={inputValue}
......
import { MessageSquare, Upload } from "lucide-react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { useTranslation } from "react-i18next";
interface FileAttachmentTypeDialogProps {
pendingFiles: File[] | null;
onConfirm: (type: "chat-context" | "upload-to-codebase") => void;
onCancel: () => void;
}
export function FileAttachmentTypeDialog({
pendingFiles,
onConfirm,
onCancel,
}: FileAttachmentTypeDialogProps) {
const { t } = useTranslation("chat");
const isOpen = !!pendingFiles && pendingFiles.length > 0;
const fileCount = pendingFiles?.length ?? 0;
return (
<Dialog
open={isOpen}
onOpenChange={(open) => {
if (!open) onCancel();
}}
>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>
{fileCount === 1
? t("attachmentTypeDialog.titleSingular")
: t("attachmentTypeDialog.titlePlural", { count: fileCount })}
</DialogTitle>
<DialogDescription>
{fileCount === 1
? t("attachmentTypeDialog.descriptionSingular")
: t("attachmentTypeDialog.descriptionPlural")}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2">
<button
type="button"
className="flex items-start gap-3 rounded-lg border border-border p-4 text-left hover:bg-muted/50 transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
onClick={() => onConfirm("chat-context")}
>
<MessageSquare
size={20}
className="mt-0.5 text-green-500 flex-shrink-0"
/>
<div>
<div className="font-medium text-sm">
{t("attachFileContext")}
</div>
<div className="text-xs text-muted-foreground mt-0.5">
{t("attachFileContextExample")}
</div>
</div>
</button>
<button
type="button"
className="flex items-start gap-3 rounded-lg border border-border p-4 text-left hover:bg-muted/50 transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
onClick={() => onConfirm("upload-to-codebase")}
>
<Upload size={20} className="mt-0.5 text-blue-500 flex-shrink-0" />
<div>
<div className="font-medium text-sm">
{t("uploadFileCodebase")}
</div>
<div className="text-xs text-muted-foreground mt-0.5">
{t("uploadFileCodebaseExample")}
</div>
</div>
</button>
</div>
</DialogContent>
</Dialog>
);
}
......@@ -12,6 +12,7 @@ import { useStreamChat } from "@/hooks/useStreamChat";
import { useAttachments } from "@/hooks/useAttachments";
import { AttachmentsList } from "./AttachmentsList";
import { DragDropOverlay } from "./DragDropOverlay";
import { FileAttachmentTypeDialog } from "./FileAttachmentTypeDialog";
import { usePostHog } from "posthog-js/react";
import { HomeSubmitOptions } from "@/pages/home";
import { ChatInputControls } from "../ChatInputControls";
......@@ -44,6 +45,7 @@ export function HomeChatInput({
const {
attachments,
isDraggingOver,
pendingFiles,
handleFileSelect,
removeAttachment,
handleDragOver,
......@@ -51,11 +53,17 @@ export function HomeChatInput({
handleDrop,
clearAttachments,
handlePaste,
confirmPendingFiles,
cancelPendingFiles,
} = useAttachments();
// Custom submit function that wraps the provided onSubmit
const handleCustomSubmit = () => {
if ((!inputValue.trim() && attachments.length === 0) || isStreaming) {
if (
(!inputValue.trim() && attachments.length === 0) ||
isStreaming ||
pendingFiles
) {
return;
}
......@@ -93,6 +101,13 @@ export function HomeChatInput({
{/* Drag and drop overlay */}
<DragDropOverlay isDraggingOver={isDraggingOver} />
{/* Dialog for choosing attachment type */}
<FileAttachmentTypeDialog
pendingFiles={pendingFiles}
onConfirm={confirmPendingFiles}
onCancel={cancelPendingFiles}
/>
<div className="flex items-start space-x-2 ">
<LexicalChatInput
value={inputValue}
......
import React, { useRef, useState } from "react";
import React, { useCallback, useRef, useState } from "react";
import type { FileAttachment } from "@/ipc/types";
import { useAtom } from "jotai";
import { attachmentsAtom } from "@/atoms/chatAtoms";
......@@ -7,6 +7,7 @@ export function useAttachments() {
const [attachments, setAttachments] = useAtom(attachmentsAtom);
const fileInputRef = useRef<HTMLInputElement>(null);
const [isDraggingOver, setIsDraggingOver] = useState(false);
const [pendingFiles, setPendingFiles] = useState<File[] | null>(null);
const handleAttachmentClick = () => {
fileInputRef.current?.click();
......@@ -46,7 +47,9 @@ export function useAttachments() {
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setIsDraggingOver(true);
if (!pendingFiles) {
setIsDraggingOver(true);
}
};
const handleDragLeave = () => {
......@@ -57,20 +60,14 @@ export function useAttachments() {
e.preventDefault();
setIsDraggingOver(false);
if (pendingFiles) return;
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
const files = Array.from(e.dataTransfer.files);
const fileAttachments: FileAttachment[] = files.map((file) => ({
file,
type: "chat-context" as const,
}));
setAttachments((attachments) => [...attachments, ...fileAttachments]);
setPendingFiles(files);
}
};
const clearAttachments = () => {
setAttachments([]);
};
const addAttachments = (
files: File[],
type: "chat-context" | "upload-to-codebase" = "chat-context",
......@@ -82,7 +79,28 @@ export function useAttachments() {
setAttachments((attachments) => [...attachments, ...fileAttachments]);
};
const confirmPendingFiles = useCallback(
(type: "chat-context" | "upload-to-codebase") => {
if (pendingFiles) {
addAttachments(pendingFiles, type);
setPendingFiles(null);
}
},
[pendingFiles, addAttachments],
);
const cancelPendingFiles = useCallback(() => {
setPendingFiles(null);
}, []);
const clearAttachments = () => {
setAttachments([]);
setPendingFiles(null);
};
const handlePaste = async (e: React.ClipboardEvent) => {
if (pendingFiles) return;
const clipboardData = e.clipboardData;
if (!clipboardData) return;
......@@ -115,9 +133,7 @@ export function useAttachments() {
}
if (imageFiles.length > 0) {
addAttachments(imageFiles, "chat-context");
// Show a brief toast or indication that image was pasted
console.log(`Pasted ${imageFiles.length} image(s) from clipboard`);
setPendingFiles(imageFiles);
}
}
};
......@@ -126,6 +142,7 @@ export function useAttachments() {
attachments,
fileInputRef,
isDraggingOver,
pendingFiles,
handleAttachmentClick,
handleFileChange,
handleFileSelect,
......@@ -136,5 +153,7 @@ export function useAttachments() {
clearAttachments,
handlePaste,
addAttachments,
confirmPendingFiles,
cancelPendingFiles,
};
}
......@@ -126,6 +126,12 @@
"uploadFileCodebase": "Upload file to codebase",
"uploadFileCodebaseExample": "Example use case: add an image to use for your app",
"dropFilesToAttach": "Drop files to attach",
"attachmentTypeDialog": {
"titleSingular": "How would you like to attach this file?",
"titlePlural": "How would you like to attach these {{count}} files?",
"descriptionSingular": "Choose how the file should be used.",
"descriptionPlural": "Choose how the files should be used."
},
"selectedComponents": "Selected Components ({{count}})",
"clearAllComponents": "Clear all selected components",
"deselectComponent": "Deselect component",
......
......@@ -126,6 +126,12 @@
"uploadFileCodebase": "Enviar arquivo para a base de código",
"uploadFileCodebaseExample": "Exemplo de uso: adicionar uma imagem para usar no seu app",
"dropFilesToAttach": "Solte os arquivos para anexar",
"attachmentTypeDialog": {
"titleSingular": "Como você gostaria de anexar este arquivo?",
"titlePlural": "Como você gostaria de anexar estes {{count}} arquivos?",
"descriptionSingular": "Escolha como o arquivo deve ser usado.",
"descriptionPlural": "Escolha como os arquivos devem ser usados."
},
"selectedComponents": "Componentes Selecionados ({{count}})",
"clearAllComponents": "Limpar todos os componentes selecionados",
"deselectComponent": "Desmarcar componente",
......
......@@ -126,6 +126,12 @@
"uploadFileCodebase": "上传文件到代码库",
"uploadFileCodebaseExample": "示例用途:添加图片供应用使用",
"dropFilesToAttach": "拖放文件以附加",
"attachmentTypeDialog": {
"titleSingular": "您想如何附加此文件?",
"titlePlural": "您想如何附加这 {{count}} 个文件?",
"descriptionSingular": "选择文件的使用方式。",
"descriptionPlural": "选择文件的使用方式。"
},
"selectedComponents": "已选择的组件 ({{count}})",
"clearAllComponents": "清除所有已选择的组件",
"deselectComponent": "取消选择组件",
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论