feat: drag files into the window (#2412)

This commit is contained in:
Allison Carter
2025-05-01 18:01:42 -04:00
committed by GitHub
parent 76cb77ebbb
commit 2591e9c98f
4 changed files with 43 additions and 2 deletions

View File

@@ -70,6 +70,7 @@ module.exports = [
HTMLTextAreaElement: 'readonly',
HTMLButtonElement: 'readonly',
HTMLDivElement: 'readonly',
File: 'readonly',
FileList: 'readonly',
FileReader: 'readonly',
DOMParser: 'readonly',

View File

@@ -93,6 +93,7 @@ function ChatContent({
const [showGame, setShowGame] = useState(false);
const [isGeneratingRecipe, setIsGeneratingRecipe] = useState(false);
const [sessionTokenCount, setSessionTokenCount] = useState<number>(0);
const [droppedFiles, setDroppedFiles] = useState<string[]>([]);
const scrollRef = useRef<ScrollAreaHandle>(null);
const {
@@ -402,6 +403,22 @@ function ChatContent({
}
}, [chat.id, messages]);
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
const files = e.dataTransfer.files;
if (files.length > 0) {
const paths: string[] = [];
for (let i = 0; i < files.length; i++) {
paths.push(window.electron.getPathForFile(files[i]));
}
setDroppedFiles(paths);
}
};
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
};
return (
<div className="flex flex-col w-full h-screen items-center justify-center">
{/* Loader when generating recipe */}
@@ -410,7 +427,11 @@ function ChatContent({
<MoreMenuLayout setView={setView} setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} />
</div>
<Card className="flex flex-col flex-1 rounded-none h-[calc(100vh-95px)] w-full bg-bgApp mt-0 border-none relative">
<Card
className="flex flex-col flex-1 rounded-none h-[calc(100vh-95px)] w-full bg-bgApp mt-0 border-none relative"
onDrop={handleDrop}
onDragOver={handleDragOver}
>
{recipeConfig?.title && messages.length > 0 && (
<AgentHeader
title={recipeConfig.title}
@@ -501,6 +522,7 @@ function ChatContent({
onStop={onStopGoose}
commandHistory={commandHistory}
initialValue={_input}
droppedFiles={droppedFiles}
/>
<BottomMenu hasMessages={hasMessages} setView={setView} numTokens={sessionTokenCount} />
</div>

View File

@@ -10,6 +10,7 @@ interface InputProps {
onStop?: () => void;
commandHistory?: string[];
initialValue?: string;
droppedFiles?: string[];
}
export default function Input({
@@ -18,6 +19,7 @@ export default function Input({
onStop,
commandHistory = [],
initialValue = '',
droppedFiles = [],
}: InputProps) {
const [_value, setValue] = useState(initialValue);
const [displayValue, setDisplayValue] = useState(initialValue); // For immediate visual feedback
@@ -35,6 +37,7 @@ export default function Input({
const [historyIndex, setHistoryIndex] = useState(-1);
const [savedInput, setSavedInput] = useState('');
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const [processedFilePaths, setProcessedFilePaths] = useState<string[]>([]);
useEffect(() => {
if (textAreaRef.current) {
@@ -45,6 +48,19 @@ export default function Input({
const minHeight = '1rem';
const maxHeight = 10 * 24;
// If we have dropped files, add them to the input and update our state.
if (processedFilePaths !== droppedFiles) {
// Append file paths that aren't in displayValue.
let joinedPaths =
displayValue.trim() +
' ' +
droppedFiles.filter((path) => !displayValue.includes(path)).join(' ');
setDisplayValue(joinedPaths);
setValue(joinedPaths);
textAreaRef.current?.focus();
setProcessedFilePaths(droppedFiles);
}
// Debounced function to update actual value
const debouncedSetValue = useCallback((val: string) => {
debounce((value: string) => {

View File

@@ -1,4 +1,4 @@
import Electron, { contextBridge, ipcRenderer } from 'electron';
import Electron, { contextBridge, ipcRenderer, webUtils } from 'electron';
interface RecipeConfig {
id: string;
@@ -50,6 +50,7 @@ type ElectronAPI = {
readFile: (directory: string) => Promise<FileResponse>;
writeFile: (directory: string, content: string) => Promise<boolean>;
getAllowedExtensions: () => Promise<string[]>;
getPathForFile: (file: File) => string;
on: (
channel: string,
callback: (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
@@ -101,6 +102,7 @@ const electronAPI: ElectronAPI = {
readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath),
writeFile: (filePath: string, content: string) =>
ipcRenderer.invoke('write-file', filePath, content),
getPathForFile: (file: File) => webUtils.getPathForFile(file),
getAllowedExtensions: () => ipcRenderer.invoke('get-allowed-extensions'),
on: (
channel: string,