Upload image via chat (#686)
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { FileText, X } from "lucide-react";
|
||||
import { FileText, X, MessageSquare, Upload } from "lucide-react";
|
||||
import type { FileAttachment } from "@/ipc/ipc_types";
|
||||
|
||||
interface AttachmentsListProps {
|
||||
attachments: File[];
|
||||
attachments: FileAttachment[];
|
||||
onRemove: (index: number) => void;
|
||||
}
|
||||
|
||||
@@ -13,40 +14,50 @@ export function AttachmentsList({
|
||||
|
||||
return (
|
||||
<div className="px-2 pt-2 flex flex-wrap gap-1">
|
||||
{attachments.map((file, index) => (
|
||||
{attachments.map((attachment, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center bg-muted rounded-md px-2 py-1 text-xs gap-1"
|
||||
title={`${file.name} (${(file.size / 1024).toFixed(1)}KB)`}
|
||||
title={`${attachment.file.name} (${(attachment.file.size / 1024).toFixed(1)}KB)`}
|
||||
>
|
||||
{file.type.startsWith("image/") ? (
|
||||
<div className="relative group">
|
||||
<img
|
||||
src={URL.createObjectURL(file)}
|
||||
alt={file.name}
|
||||
className="w-5 h-5 object-cover rounded"
|
||||
onLoad={(e) =>
|
||||
URL.revokeObjectURL((e.target as HTMLImageElement).src)
|
||||
}
|
||||
onError={(e) =>
|
||||
URL.revokeObjectURL((e.target as HTMLImageElement).src)
|
||||
}
|
||||
/>
|
||||
<div className="absolute hidden group-hover:block top-6 left-0 z-10">
|
||||
<div className="flex items-center gap-1">
|
||||
{attachment.type === "upload-to-codebase" ? (
|
||||
<Upload size={12} className="text-blue-600" />
|
||||
) : (
|
||||
<MessageSquare size={12} className="text-green-600" />
|
||||
)}
|
||||
{attachment.file.type.startsWith("image/") ? (
|
||||
<div className="relative group">
|
||||
<img
|
||||
src={URL.createObjectURL(file)}
|
||||
alt={file.name}
|
||||
className="max-w-[200px] max-h-[200px] object-contain bg-white p-1 rounded shadow-lg"
|
||||
src={URL.createObjectURL(attachment.file)}
|
||||
alt={attachment.file.name}
|
||||
className="w-5 h-5 object-cover rounded"
|
||||
onLoad={(e) =>
|
||||
URL.revokeObjectURL((e.target as HTMLImageElement).src)
|
||||
}
|
||||
onError={(e) =>
|
||||
URL.revokeObjectURL((e.target as HTMLImageElement).src)
|
||||
}
|
||||
/>
|
||||
<div className="absolute hidden group-hover:block top-6 left-0 z-10">
|
||||
<img
|
||||
src={URL.createObjectURL(attachment.file)}
|
||||
alt={attachment.file.name}
|
||||
className="max-w-[200px] max-h-[200px] object-contain bg-white p-1 rounded shadow-lg"
|
||||
onLoad={(e) =>
|
||||
URL.revokeObjectURL((e.target as HTMLImageElement).src)
|
||||
}
|
||||
onError={(e) =>
|
||||
URL.revokeObjectURL((e.target as HTMLImageElement).src)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<FileText size={12} />
|
||||
)}
|
||||
<span className="truncate max-w-[120px]">{file.name}</span>
|
||||
) : (
|
||||
<FileText size={12} />
|
||||
)}
|
||||
</div>
|
||||
<span className="truncate max-w-[120px]">{attachment.file.name}</span>
|
||||
<button
|
||||
onClick={() => onRemove(index)}
|
||||
className="hover:bg-muted-foreground/20 rounded-full p-0.5"
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
Database,
|
||||
ChevronsUpDown,
|
||||
ChevronsDownUp,
|
||||
Paperclip,
|
||||
ChartColumnIncreasing,
|
||||
SendHorizontalIcon,
|
||||
} from "lucide-react";
|
||||
@@ -58,6 +57,7 @@ import { useVersions } from "@/hooks/useVersions";
|
||||
import { useAttachments } from "@/hooks/useAttachments";
|
||||
import { AttachmentsList } from "./AttachmentsList";
|
||||
import { DragDropOverlay } from "./DragDropOverlay";
|
||||
import { FileAttachmentDropdown } from "./FileAttachmentDropdown";
|
||||
import { showError, showExtraFilesToast } from "@/lib/toast";
|
||||
import { ChatInputControls } from "../ChatInputControls";
|
||||
import { ChatErrorBox } from "./ChatErrorBox";
|
||||
@@ -89,10 +89,8 @@ export function ChatInput({ chatId }: { chatId?: number }) {
|
||||
// Use the attachments hook
|
||||
const {
|
||||
attachments,
|
||||
fileInputRef,
|
||||
isDraggingOver,
|
||||
handleAttachmentClick,
|
||||
handleFileChange,
|
||||
handleFileSelect,
|
||||
removeAttachment,
|
||||
handleDragOver,
|
||||
handleDragLeave,
|
||||
@@ -342,29 +340,10 @@ export function ChatInput({ chatId }: { chatId?: number }) {
|
||||
<div className="pl-2 pr-1 flex items-center justify-between pb-2">
|
||||
<div className="flex items-center">
|
||||
<ChatInputControls showContextFilesPicker={true} />
|
||||
{/* File attachment button */}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={handleAttachmentClick}
|
||||
title="Attach files"
|
||||
size="sm"
|
||||
>
|
||||
<Paperclip size={20} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Attach files</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
multiple
|
||||
accept=".jpg,.jpeg,.png,.gif,.webp,.txt,.md,.js,.ts,.html,.css,.json,.csv"
|
||||
{/* File attachment dropdown */}
|
||||
<FileAttachmentDropdown
|
||||
onFileSelect={handleFileSelect}
|
||||
disabled={isStreaming}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
133
src/components/chat/FileAttachmentDropdown.tsx
Normal file
133
src/components/chat/FileAttachmentDropdown.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { Paperclip, MessageSquare, Upload } from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useRef } from "react";
|
||||
|
||||
interface FileAttachmentDropdownProps {
|
||||
onFileSelect: (
|
||||
files: FileList,
|
||||
type: "chat-context" | "upload-to-codebase",
|
||||
) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function FileAttachmentDropdown({
|
||||
onFileSelect,
|
||||
disabled,
|
||||
className,
|
||||
}: FileAttachmentDropdownProps) {
|
||||
const chatContextFileInputRef = useRef<HTMLInputElement>(null);
|
||||
const uploadToCodebaseFileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleChatContextClick = () => {
|
||||
chatContextFileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleUploadToCodebaseClick = () => {
|
||||
uploadToCodebaseFileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleFileChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
type: "chat-context" | "upload-to-codebase",
|
||||
) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
onFileSelect(e.target.files, type);
|
||||
// Clear the input value so the same file can be selected again
|
||||
e.target.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
title="Attach files"
|
||||
className={className}
|
||||
>
|
||||
<Paperclip size={20} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
onClick={handleChatContextClick}
|
||||
className="py-3 px-4"
|
||||
>
|
||||
<MessageSquare size={16} className="mr-2" />
|
||||
Attach file as chat context
|
||||
</DropdownMenuItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
Example use case: screenshot of the app to point out a UI
|
||||
issue
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
onClick={handleUploadToCodebaseClick}
|
||||
className="py-3 px-4"
|
||||
>
|
||||
<Upload size={16} className="mr-2" />
|
||||
Upload file to codebase
|
||||
</DropdownMenuItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
Example use case: add an image to use for your app
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<TooltipContent>Attach files</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
{/* Hidden file inputs */}
|
||||
<input
|
||||
type="file"
|
||||
data-testid="chat-context-file-input"
|
||||
ref={chatContextFileInputRef}
|
||||
onChange={(e) => handleFileChange(e, "chat-context")}
|
||||
className="hidden"
|
||||
multiple
|
||||
accept=".jpg,.jpeg,.png,.gif,.webp,.txt,.md,.js,.ts,.html,.css,.json,.csv"
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
data-testid="upload-to-codebase-file-input"
|
||||
ref={uploadToCodebaseFileInputRef}
|
||||
onChange={(e) => handleFileChange(e, "upload-to-codebase")}
|
||||
className="hidden"
|
||||
multiple
|
||||
accept=".jpg,.jpeg,.png,.gif,.webp,.txt,.md,.js,.ts,.html,.css,.json,.csv"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SendIcon, StopCircleIcon, Paperclip } from "lucide-react";
|
||||
import { SendIcon, StopCircleIcon } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useStreamChat } from "@/hooks/useStreamChat";
|
||||
import { useAttachments } from "@/hooks/useAttachments";
|
||||
import { AttachmentsList } from "./AttachmentsList";
|
||||
import { DragDropOverlay } from "./DragDropOverlay";
|
||||
import { FileAttachmentDropdown } from "./FileAttachmentDropdown";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { HomeSubmitOptions } from "@/pages/home";
|
||||
import { ChatInputControls } from "../ChatInputControls";
|
||||
@@ -28,10 +29,8 @@ export function HomeChatInput({
|
||||
// Use the attachments hook
|
||||
const {
|
||||
attachments,
|
||||
fileInputRef,
|
||||
isDraggingOver,
|
||||
handleAttachmentClick,
|
||||
handleFileChange,
|
||||
handleFileSelect,
|
||||
removeAttachment,
|
||||
handleDragOver,
|
||||
handleDragLeave,
|
||||
@@ -111,22 +110,11 @@ export function HomeChatInput({
|
||||
disabled={isStreaming} // Should ideally reflect if *any* stream is happening
|
||||
/>
|
||||
|
||||
{/* File attachment button */}
|
||||
<button
|
||||
onClick={handleAttachmentClick}
|
||||
className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50"
|
||||
{/* File attachment dropdown */}
|
||||
<FileAttachmentDropdown
|
||||
className="mt-1 mr-1"
|
||||
onFileSelect={handleFileSelect}
|
||||
disabled={isStreaming}
|
||||
title="Attach files"
|
||||
>
|
||||
<Paperclip size={20} />
|
||||
</button>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
multiple
|
||||
accept=".jpg,.jpeg,.png,.gif,.webp,.txt,.md,.js,.ts,.html,.css,.json,.csv"
|
||||
/>
|
||||
|
||||
{isStreaming ? (
|
||||
|
||||
Reference in New Issue
Block a user