diff --git a/.gitignore b/.gitignore
index fae21c2..f55f526 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,4 +41,5 @@ yarn-error.log*
next-env.d.ts
#cloudflare
-.open-next
+.open-next
+package-lock.json
\ No newline at end of file
diff --git a/frontend/components/ChatInput.tsx b/frontend/components/ChatInput.tsx
index 0630f3b..84f77e5 100644
--- a/frontend/components/ChatInput.tsx
+++ b/frontend/components/ChatInput.tsx
@@ -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,
- ]);
+ ]);
+
if (!canChat) {
return ;
@@ -137,6 +143,14 @@ function PureChatInput({
+
+
+
@@ -163,12 +180,18 @@ function PureChatInput({
+
+
+
- {status === 'submitted' || status === 'streaming' ? (
-
- ) : (
-
- )}
+ {status === 'submitted' || status === 'streaming' ? (
+
+ ) : (
+
+ )}
+
diff --git a/frontend/components/Message.tsx b/frontend/components/Message.tsx
index b32629a..550df14 100644
--- a/frontend/components/Message.tsx
+++ b/frontend/components/Message.tsx
@@ -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' &&
{part.text}
}
+ {mode === 'view' && (
+
+ {message.experimental_attachments
+ ?.map((attachment, index) => (
+
+ {attachment.contentType?.startsWith("image/") ?
+
+ : attachment.contentType?.startsWith("text/") ?
+ :
+
+
+ }
+
+ ))}
+
{part.text}
+
+ )}
{mode === 'view' && (
void
+}
+
+export function TextFilePreview({ fileList }: { fileList: FileList | undefined }) {
+ const [content, setContent] = useState("");
+
+ 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);
+ }, [fileList]);
+
+ return (
+
+
+ {content}
+ {content.length >= 100 && "..."}
+
+
+ );
+}
+
+
+function PreviewFile({ fileUrls, clearFiles, fileList, contentTypes }: FileInputProps) {
+ return (
+ <>
+ {fileUrls?.length > 0 &&
+
+
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" ?
+
+ :
+ contentTypes[0] === "text/plain" ?
+
+ :
+
+ }
+
+
+ }
+ >
+ );
+}
+
+
+
+const FilePreview = memo(PreviewFile, (prevProps, nextProps) => {
+ if (prevProps.fileUrls !== nextProps.fileUrls) return false;
+ return true;
+});
+
+
+export default FilePreview;
\ No newline at end of file
diff --git a/frontend/hooks/useFileHandler.tsx b/frontend/hooks/useFileHandler.tsx
new file mode 100644
index 0000000..d799fcf
--- /dev/null
+++ b/frontend/hooks/useFileHandler.tsx
@@ -0,0 +1,84 @@
+import { useState, useCallback } from 'react';
+
+export function useFileHandler() {
+ const [fileUrls, setFileUrls] = useState([]);
+ const [fileList, setFileList] = useState(undefined);
+ const [contentTypes, setContentTypes] = useState([]);
+
+ const processFiles = useCallback((files: FileList) => {
+ const urls = Array.from(files).map((file) => URL.createObjectURL(file));
+ const types = Array.from(files).map((file) => file.type);
+
+ setFileUrls(urls);
+ setFileList(files);
+ setContentTypes(types);
+ console.log("type" , types , fileUrls , fileList);
+
+ }, []);
+
+ 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);
+ }
+ }, [processFiles]);
+
+ const handleFileChange = useCallback((event: React.ChangeEvent) => {
+ 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
+ };
+}