-
-
Notifications
You must be signed in to change notification settings - Fork 95
Pull Request: Implement File Handling Capabilities for chat0 #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,4 +41,5 @@ yarn-error.log* | |
| next-env.d.ts | ||
|
|
||
| #cloudflare | ||
| .open-next | ||
| .open-next | ||
| package-lock.json | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,5 @@ | ||||||||||||||||||||||||||||||
| import { ChevronDown, Check, ArrowUpIcon } from 'lucide-react'; | ||||||||||||||||||||||||||||||
| import { memo, useCallback, useMemo } from 'react'; | ||||||||||||||||||||||||||||||
| import { ChevronDown, Check, ArrowUpIcon, Plus, CircleX } from 'lucide-react'; | ||||||||||||||||||||||||||||||
| import { memo, useCallback, useMemo, useRef, useState } from 'react'; | ||||||||||||||||||||||||||||||
| import { Textarea } from '@/frontend/components/ui/textarea'; | ||||||||||||||||||||||||||||||
| import { cn } from '@/lib/utils'; | ||||||||||||||||||||||||||||||
| import { Button } from '@/frontend/components/ui/button'; | ||||||||||||||||||||||||||||||
|
|
@@ -18,11 +18,12 @@ import { useAPIKeyStore } from '@/frontend/stores/APIKeyStore'; | |||||||||||||||||||||||||||||
| import { useModelStore } from '@/frontend/stores/ModelStore'; | ||||||||||||||||||||||||||||||
| import { AI_MODELS, AIModel, getModelConfig } from '@/lib/models'; | ||||||||||||||||||||||||||||||
| import KeyPrompt from '@/frontend/components/KeyPrompt'; | ||||||||||||||||||||||||||||||
| import { UIMessage } from 'ai'; | ||||||||||||||||||||||||||||||
| import { UIMessage } from 'ai'; | ||||||||||||||||||||||||||||||
| import { v4 as uuidv4 } from 'uuid'; | ||||||||||||||||||||||||||||||
| import { StopIcon } from './ui/icons'; | ||||||||||||||||||||||||||||||
| import { toast } from 'sonner'; | ||||||||||||||||||||||||||||||
| import { StopIcon } from './ui/icons'; | ||||||||||||||||||||||||||||||
| import { useMessageSummary } from '../hooks/useMessageSummary'; | ||||||||||||||||||||||||||||||
| import { useFileHandler } from '../hooks/useFileHandler'; | ||||||||||||||||||||||||||||||
| import FilePreview from './filePreview'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| interface ChatInputProps { | ||||||||||||||||||||||||||||||
| threadId: string; | ||||||||||||||||||||||||||||||
|
|
@@ -65,6 +66,9 @@ function PureChatInput({ | |||||||||||||||||||||||||||||
| maxHeight: 200, | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const { fileList, fileUrls, contentTypes, handlePaste, handleFileChange, clearFiles , handleDragOver , handleDrop} = useFileHandler(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const navigate = useNavigate(); | ||||||||||||||||||||||||||||||
| const { id } = useParams(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -99,10 +103,11 @@ function PureChatInput({ | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const userMessage = createUserMessage(messageId, currentInput.trim()); | ||||||||||||||||||||||||||||||
| await createMessage(threadId, userMessage); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| append(userMessage); | ||||||||||||||||||||||||||||||
| append(userMessage, { experimental_attachments: fileList }) | ||||||||||||||||||||||||||||||
| setInput(''); | ||||||||||||||||||||||||||||||
| clearFiles(); | ||||||||||||||||||||||||||||||
| adjustHeight(true); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| }, [ | ||||||||||||||||||||||||||||||
| input, | ||||||||||||||||||||||||||||||
| status, | ||||||||||||||||||||||||||||||
|
|
@@ -113,7 +118,8 @@ function PureChatInput({ | |||||||||||||||||||||||||||||
| textareaRef, | ||||||||||||||||||||||||||||||
| threadId, | ||||||||||||||||||||||||||||||
| complete, | ||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add missing dependencies to useCallback. The -]);
+ navigate,
+ fileList,
+ clearFiles,
+]);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (!canChat) { | ||||||||||||||||||||||||||||||
| return <KeyPrompt />; | ||||||||||||||||||||||||||||||
|
|
@@ -137,6 +143,14 @@ function PureChatInput({ | |||||||||||||||||||||||||||||
| <div className="relative"> | ||||||||||||||||||||||||||||||
| <div className="flex flex-col"> | ||||||||||||||||||||||||||||||
| <div className="bg-secondary overflow-y-auto max-h-[300px]"> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <FilePreview | ||||||||||||||||||||||||||||||
| clearFiles={clearFiles} | ||||||||||||||||||||||||||||||
| contentTypes={contentTypes} | ||||||||||||||||||||||||||||||
| fileList={fileList} | ||||||||||||||||||||||||||||||
| fileUrls={fileUrls} | ||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <Textarea | ||||||||||||||||||||||||||||||
| id="chat-input" | ||||||||||||||||||||||||||||||
| value={input} | ||||||||||||||||||||||||||||||
|
|
@@ -152,6 +166,9 @@ function PureChatInput({ | |||||||||||||||||||||||||||||
| ref={textareaRef} | ||||||||||||||||||||||||||||||
| onKeyDown={handleKeyDown} | ||||||||||||||||||||||||||||||
| onChange={handleInputChange} | ||||||||||||||||||||||||||||||
| onPaste={handlePaste} | ||||||||||||||||||||||||||||||
| onDrop={handleDrop} | ||||||||||||||||||||||||||||||
| onDragOver={handleDragOver} | ||||||||||||||||||||||||||||||
| aria-label="Chat message input" | ||||||||||||||||||||||||||||||
| aria-describedby="chat-input-description" | ||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||
|
|
@@ -163,12 +180,18 @@ function PureChatInput({ | |||||||||||||||||||||||||||||
| <div className="h-14 flex items-center px-2"> | ||||||||||||||||||||||||||||||
| <div className="flex items-center justify-between w-full"> | ||||||||||||||||||||||||||||||
| <ChatModelDropdown /> | ||||||||||||||||||||||||||||||
| <div className="flex gap-2 items-center"> | ||||||||||||||||||||||||||||||
| <input onChange={handleFileChange} className='hidden' type="file" name="" id="fileChange" /> | ||||||||||||||||||||||||||||||
| <label htmlFor="fileChange"> | ||||||||||||||||||||||||||||||
| <Plus /> | ||||||||||||||||||||||||||||||
| </label> | ||||||||||||||||||||||||||||||
|
Comment on lines
+183
to
+187
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Improve file input UI accessibility and styling. The Plus icon button lacks proper styling and accessibility attributes. -<label htmlFor="fileChange">
- <Plus />
-</label>
+<label
+ htmlFor="fileChange"
+ className="cursor-pointer p-2 hover:bg-muted rounded-md transition-colors"
+ aria-label="Attach file"
+>
+ <Plus className="w-5 h-5" />
+</label>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {status === 'submitted' || status === 'streaming' ? ( | ||||||||||||||||||||||||||||||
| <StopButton stop={stop} /> | ||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||
| <SendButton onSubmit={handleSubmit} disabled={isDisabled} /> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| {status === 'submitted' || status === 'streaming' ? ( | ||||||||||||||||||||||||||||||
| <StopButton stop={stop} /> | ||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||
| <SendButton onSubmit={handleSubmit} disabled={isDisabled} /> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,12 +1,13 @@ | ||||||||||||||||||
| import { memo, useState } from 'react'; | ||||||||||||||||||
| import MarkdownRenderer from '@/frontend/components/MemoizedMarkdown'; | ||||||||||||||||||
| import { cn } from '@/lib/utils'; | ||||||||||||||||||
| import { UIMessage } from 'ai'; | ||||||||||||||||||
| import { Attachment, UIMessage } from 'ai'; | ||||||||||||||||||
| import equal from 'fast-deep-equal'; | ||||||||||||||||||
| import MessageControls from './MessageControls'; | ||||||||||||||||||
| import { UseChatHelpers } from '@ai-sdk/react'; | ||||||||||||||||||
| import MessageEditor from './MessageEditor'; | ||||||||||||||||||
| import MessageReasoning from './MessageReasoning'; | ||||||||||||||||||
| import { TextFilePreview } from './filePreview'; | ||||||||||||||||||
|
|
||||||||||||||||||
| function PureMessage({ | ||||||||||||||||||
| threadId, | ||||||||||||||||||
|
|
@@ -67,7 +68,27 @@ function PureMessage({ | |||||||||||||||||
| stop={stop} | ||||||||||||||||||
| /> | ||||||||||||||||||
| )} | ||||||||||||||||||
| {mode === 'view' && <p>{part.text}</p>} | ||||||||||||||||||
| {mode === 'view' && ( | ||||||||||||||||||
| <div className='flex flex-col gap-2'> | ||||||||||||||||||
| {message.experimental_attachments | ||||||||||||||||||
| ?.map((attachment, index) => ( | ||||||||||||||||||
| <section key={attachment.name}> | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Use a unique key for attachment rendering. Using -<section key={attachment.name}>
+<section key={`${message.id}-attachment-${index}`}>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| {attachment.contentType?.startsWith("image/") ? | ||||||||||||||||||
| <img className='w-12 h-12 rounded-md' | ||||||||||||||||||
| key={`${message.id}-${index}`} | ||||||||||||||||||
| src={attachment.url} | ||||||||||||||||||
| alt={attachment.name} | ||||||||||||||||||
| /> | ||||||||||||||||||
| : attachment.contentType?.startsWith("text/") ? | ||||||||||||||||||
| <TextFilePreview fileList={undefined} /> : | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix TextFilePreview receiving undefined fileList. The The attachment object likely contains the file data or URL that should be used here. Consider either:
🤖 Prompt for AI Agents |
||||||||||||||||||
| <iframe src={attachment.url} className="w-20 h-20 overflow-hidden" scrolling="no"></iframe> | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Security concern: Validate iframe source URLs. Rendering arbitrary URLs in an iframe without validation could pose security risks. Consider implementing a whitelist of allowed domains or file types. Add proper sandboxing attributes to the iframe and validate the URL source: -<iframe src={attachment.url} className="w-20 h-20 overflow-hidden" scrolling="no"></iframe>
+<iframe
+ src={attachment.url}
+ className="w-20 h-20 overflow-hidden"
+ scrolling="no"
+ sandbox="allow-scripts allow-same-origin"
+ title={attachment.name}
+></iframe>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| } | ||||||||||||||||||
| </section> | ||||||||||||||||||
| ))} | ||||||||||||||||||
| <p>{part.text}</p> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| )} | ||||||||||||||||||
|
|
||||||||||||||||||
| {mode === 'view' && ( | ||||||||||||||||||
| <MessageControls | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,72 @@ | ||||||||||||||||||||||||||||
| import { CircleX } from "lucide-react"; | ||||||||||||||||||||||||||||
| import { memo, useEffect, useState } from "react"; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| interface FileInputProps { | ||||||||||||||||||||||||||||
| fileUrls: string[], | ||||||||||||||||||||||||||||
| fileList: FileList | undefined, | ||||||||||||||||||||||||||||
| contentTypes: string[], | ||||||||||||||||||||||||||||
| clearFiles: () => void | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| export function TextFilePreview({ fileList }: { fileList: FileList | undefined }) { | ||||||||||||||||||||||||||||
| const [content, setContent] = useState<string>(""); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||
| if (!fileList || fileList.length === 0) { | ||||||||||||||||||||||||||||
| setContent(""); | ||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| const file = fileList[0]; | ||||||||||||||||||||||||||||
| const reader = new FileReader(); | ||||||||||||||||||||||||||||
| reader.onload = (e) => { | ||||||||||||||||||||||||||||
| const text = e.target?.result; | ||||||||||||||||||||||||||||
| setContent(typeof text === "string" ? text.slice(0, 100) : ""); | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
| reader.readAsText(file); | ||||||||||||||||||||||||||||
|
Comment on lines
+22
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add error handling for FileReader operations. The FileReader operations could fail and should include error handling. reader.onload = (e) => {
const text = e.target?.result;
setContent(typeof text === "string" ? text.slice(0, 100) : "");
};
+reader.onerror = () => {
+ setContent("Error reading file");
+};
reader.readAsText(file);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| }, [fileList]); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||
| <div className="text-[5px] leading-1 w-15 h-14 overflow-hidden text-zinc-500 border p-2 rounded-lg bg-white dark:bg-zinc-800 dark:border-zinc-700 dark:text-zinc-400"> | ||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Text preview font size is too small. The font size of 5px is likely unreadable. Consider using a larger, more accessible size. -<div className="text-[5px] leading-1 w-15 h-14 overflow-hidden text-zinc-500 border p-2 rounded-lg bg-white dark:bg-zinc-800 dark:border-zinc-700 dark:text-zinc-400">
+<div className="text-[10px] leading-tight w-20 h-20 overflow-hidden text-zinc-500 border p-2 rounded-lg bg-white dark:bg-zinc-800 dark:border-zinc-700 dark:text-zinc-400">📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| <p className=""> | ||||||||||||||||||||||||||||
| {content} | ||||||||||||||||||||||||||||
| {content.length >= 100 && "..."} | ||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| function PreviewFile({ fileUrls, clearFiles, fileList, contentTypes }: FileInputProps) { | ||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||
| {fileUrls?.length > 0 && | ||||||||||||||||||||||||||||
| <div className="relative overflow-hidden rounded-md mb-2"> | ||||||||||||||||||||||||||||
| <CircleX | ||||||||||||||||||||||||||||
| onClick={() => clearFiles()} | ||||||||||||||||||||||||||||
| className={`absolute top-2 ${contentTypes[0] === "image/jpeg" ? 'w-5 h-5 left-14' : 'w-4 h-4 left-10'} hover:stroke-red-400`} | ||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||
| {contentTypes[0] === "image/jpeg" ? | ||||||||||||||||||||||||||||
| <img src={fileUrls[0]} className='w-20 h-20 rounded-md' alt="attached_image" /> | ||||||||||||||||||||||||||||
|
Comment on lines
+47
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Support all image types, not just JPEG. The code only checks for "image/jpeg" but should support all common image formats. -${contentTypes[0] === "image/jpeg" ? 'w-5 h-5 left-14' : 'w-4 h-4 left-10'}
+${contentTypes[0]?.startsWith("image/") ? 'w-5 h-5 left-14' : 'w-4 h-4 left-10'}-{contentTypes[0] === "image/jpeg" ?
+{contentTypes[0]?.startsWith("image/") ?🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| : | ||||||||||||||||||||||||||||
| contentTypes[0] === "text/plain" ? | ||||||||||||||||||||||||||||
| <TextFilePreview fileList={fileList} /> | ||||||||||||||||||||||||||||
| : | ||||||||||||||||||||||||||||
| <iframe src={fileUrls[0]} className="w-20 h-20 overflow-hidden" scrolling="no"></iframe> | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const FilePreview = memo(PreviewFile, (prevProps, nextProps) => { | ||||||||||||||||||||||||||||
| if (prevProps.fileUrls !== nextProps.fileUrls) return false; | ||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
|
Comment on lines
+66
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix memoization to check all relevant props. The memo comparison only checks -const FilePreview = memo(PreviewFile, (prevProps, nextProps) => {
- if (prevProps.fileUrls !== nextProps.fileUrls) return false;
- return true;
-});
+const FilePreview = memo(PreviewFile, (prevProps, nextProps) => {
+ return prevProps.fileUrls === nextProps.fileUrls &&
+ prevProps.fileList === nextProps.fileList &&
+ prevProps.contentTypes === nextProps.contentTypes;
+});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| export default FilePreview; | ||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,84 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState, useCallback } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function useFileHandler() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [fileUrls, setFileUrls] = useState<string[]>([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [fileList, setFileList] = useState<FileList | undefined>(undefined); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [contentTypes, setContentTypes] = useState<string[]>([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const processFiles = useCallback((files: FileList) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const urls = Array.from(files).map((file) => URL.createObjectURL(file)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prevent memory leaks by revoking object URLs. Object URLs created with Store the URLs and revoke them when clearing or before creating new ones: const processFiles = useCallback((files: FileList) => {
+ // Revoke previous URLs
+ fileUrls.forEach(url => URL.revokeObjectURL(url));
+
const urls = Array.from(files).map((file) => URL.createObjectURL(file));Also update clearFiles: const clearFiles = useCallback(() =>{
+ fileUrls.forEach(url => URL.revokeObjectURL(url));
setFileList(undefined);
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const types = Array.from(files).map((file) => file.type); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setFileUrls(urls); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setFileList(files); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setContentTypes(types); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log("type" , types , fileUrls , fileList); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove console.log statement. Debug logging should not be included in production code. -console.log("type" , types , fileUrls , fileList);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix useCallback dependency array. The empty dependency array is incorrect since the callback uses -}, []);
+}, [fileUrls]);Note: After removing the console.log, the empty array would be correct.
🤖 Prompt for AI Agents
Comment on lines
+8
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add file validation for size and quantity. The file processing should validate file size and limit the number of files to prevent performance issues. const processFiles = useCallback((files: FileList) => {
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
+ const MAX_FILES = 5;
+
+ if (files.length > MAX_FILES) {
+ console.error(`Maximum ${MAX_FILES} files allowed`);
+ return;
+ }
+
+ for (const file of Array.from(files)) {
+ if (file.size > MAX_FILE_SIZE) {
+ console.error(`File ${file.name} exceeds maximum size of 10MB`);
+ return;
+ }
+ }
+
const urls = Array.from(files).map((file) => URL.createObjectURL(file));📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handlePaste = useCallback((event: React.ClipboardEvent) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| event.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const items = event.clipboardData?.items; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!items) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = 0; i < items.length; i++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| items[i].type.startsWith('image') || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| items[i].type === 'application/pdf' || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| items[i].type === 'text/plain' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const file = items[i].getAsFile(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (file) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dt = new DataTransfer(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dt.items.add(file); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| processFiles(dt.files); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Optional: handle pasted text if needed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const text = event.clipboardData.getData('text/plain'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (text) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.execCommand('insertText', false, text); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Replace deprecated
-document.execCommand('insertText', false, text);
+// Let the default paste behavior handle text insertion
+// or use: navigator.clipboard.writeText(text);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [processFiles]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleFileChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const files = event.target.files; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (files && files.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| processFiles(files); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [processFiles]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleDrop = useCallback((event: React.DragEvent) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| event.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const files = event.dataTransfer.files; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (files && files.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| processFiles(files); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [processFiles]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleDragOver = useCallback((event: React.DragEvent) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| event.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const clearFiles = useCallback(() =>{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setFileList(undefined); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setFileUrls([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setContentTypes([]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } , []) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fileUrls, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fileList, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contentTypes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handlePaste, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handleFileChange, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handleDragOver, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handleDrop, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clearFiles | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add file validation before appending message.
Consider validating file size and type before sending to prevent issues with large files or unsupported formats.
📝 Committable suggestion
🤖 Prompt for AI Agents