diff --git a/application/agents/classic_agent.py b/application/agents/classic_agent.py index f0915747..8446347c 100644 --- a/application/agents/classic_agent.py +++ b/application/agents/classic_agent.py @@ -44,10 +44,13 @@ class ClassicAgent(BaseAgent): ): yield {"answer": resp.message.content} else: - completion = self.llm.gen_stream( - model=self.gpt_model, messages=messages, tools=self.tools - ) - for line in completion: + # completion = self.llm.gen_stream( + # model=self.gpt_model, messages=messages, tools=self.tools + # ) + # log type of resp + logger.info(f"Response type: {type(resp)}") + logger.info(f"Response: {resp}") + for line in resp: if isinstance(line, str): yield {"answer": line} diff --git a/application/agents/llm_handler.py b/application/agents/llm_handler.py index 21d972d9..7fe794f8 100644 --- a/application/agents/llm_handler.py +++ b/application/agents/llm_handler.py @@ -15,7 +15,7 @@ class LLMHandler(ABC): @abstractmethod def handle_response(self, agent, resp, tools_dict, messages, attachments=None, **kwargs): pass - + def prepare_messages_with_attachments(self, agent, messages, attachments=None): """ Prepare messages with attachment content if available. @@ -33,15 +33,53 @@ class LLMHandler(ABC): logger.info(f"Preparing messages with {len(attachments)} attachments") - # Check if the LLM has its own custom attachment handling implementation - if hasattr(agent.llm, "prepare_messages_with_attachments") and agent.llm.__class__.__name__ != "BaseLLM": - logger.info(f"Using {agent.llm.__class__.__name__}'s own prepare_messages_with_attachments method") - return agent.llm.prepare_messages_with_attachments(messages, attachments) + supported_types = agent.llm.get_supported_attachment_types() - # Otherwise, append attachment content to the system prompt + supported_attachments = [] + unsupported_attachments = [] + + for attachment in attachments: + mime_type = attachment.get('mime_type') + if not mime_type: + import mimetypes + file_path = attachment.get('path') + if file_path: + mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + else: + unsupported_attachments.append(attachment) + continue + + if mime_type in supported_types: + supported_attachments.append(attachment) + else: + unsupported_attachments.append(attachment) + + # Process supported attachments with the LLM's custom method + prepared_messages = messages + if supported_attachments: + logger.info(f"Processing {len(supported_attachments)} supported attachments with {agent.llm.__class__.__name__}'s method") + prepared_messages = agent.llm.prepare_messages_with_attachments(messages, supported_attachments) + + # Process unsupported attachments with the default method + if unsupported_attachments: + logger.info(f"Processing {len(unsupported_attachments)} unsupported attachments with default method") + prepared_messages = self._append_attachment_content_to_system(prepared_messages, unsupported_attachments) + + return prepared_messages + + def _append_attachment_content_to_system(self, messages, attachments): + """ + Default method to append attachment content to the system prompt. + + Args: + messages (list): List of message dictionaries. + attachments (list): List of attachment dictionaries with content. + + Returns: + list: Messages with attachment context added to the system prompt. + """ prepared_messages = messages.copy() - # Build attachment content string attachment_texts = [] for attachment in attachments: logger.info(f"Adding attachment {attachment.get('id')} to context") @@ -122,12 +160,13 @@ class OpenAILLMHandler(LLMHandler): return resp else: - + text_buffer = "" while True: tool_calls = {} for chunk in resp: if isinstance(chunk, str) and len(chunk) > 0: - return + yield chunk + continue elif hasattr(chunk, "delta"): chunk_delta = chunk.delta @@ -206,12 +245,17 @@ class OpenAILLMHandler(LLMHandler): } ) tool_calls = {} + if hasattr(chunk_delta, "content") and chunk_delta.content: + # Add to buffer or yield immediately based on your preference + text_buffer += chunk_delta.content + yield text_buffer + text_buffer = "" if ( hasattr(chunk, "finish_reason") and chunk.finish_reason == "stop" ): - return + return resp elif isinstance(chunk, str) and len(chunk) == 0: continue @@ -227,7 +271,7 @@ class GoogleLLMHandler(LLMHandler): from google.genai import types messages = self.prepare_messages_with_attachments(agent, messages, attachments) - + while True: if not stream: response = agent.llm.gen( @@ -298,6 +342,9 @@ class GoogleLLMHandler(LLMHandler): "content": [function_response_part.to_json_dict()], } ) + else: + tool_call_found = False + yield result if not tool_call_found: return response diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py index 2f158b5b..ef9b7381 100644 --- a/application/api/answer/routes.py +++ b/application/api/answer/routes.py @@ -835,12 +835,7 @@ def get_attachments_content(attachment_ids, user): }) if attachment_doc: - attachments.append({ - "id": str(attachment_doc["_id"]), - "content": attachment_doc["content"], - "token_count": attachment_doc.get("token_count", 0), - "path": attachment_doc.get("path", "") - }) + attachments.append(attachment_doc) except Exception as e: logger.error(f"Error retrieving attachment {attachment_id}: {e}") diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 8f374aa7..91b028d5 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -2506,23 +2506,26 @@ class StoreAttachment(Resource): user = secure_filename(decoded_token.get("sub")) try: + attachment_id = ObjectId() original_filename = secure_filename(file.filename) - folder_name = original_filename - save_dir = os.path.join(current_dir, settings.UPLOAD_FOLDER, user, "attachments",folder_name) + + save_dir = os.path.join( + current_dir, + settings.UPLOAD_FOLDER, + user, + "attachments", + str(attachment_id) + ) os.makedirs(save_dir, exist_ok=True) - # Create directory structure: user/attachments/filename/ + file_path = os.path.join(save_dir, original_filename) - # Handle filename conflicts - if os.path.exists(file_path): - name_parts = os.path.splitext(original_filename) - timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") - new_filename = f"{name_parts[0]}_{timestamp}{name_parts[1]}" - file_path = os.path.join(save_dir, new_filename) - original_filename = new_filename file.save(file_path) - file_info = {"folder": folder_name, "filename": original_filename} + file_info = { + "filename": original_filename, + "attachment_id": str(attachment_id) + } current_app.logger.info(f"Saved file: {file_path}") # Start async task to process single file diff --git a/application/llm/base.py b/application/llm/base.py index 0fce208c..0607159d 100644 --- a/application/llm/base.py +++ b/application/llm/base.py @@ -55,3 +55,12 @@ class BaseLLM(ABC): def _supports_tools(self): raise NotImplementedError("Subclass must implement _supports_tools method") + + def get_supported_attachment_types(self): + """ + Return a list of MIME types supported by this LLM for file uploads. + + Returns: + list: List of supported MIME types + """ + return [] # Default: no attachments supported diff --git a/application/llm/google_ai.py b/application/llm/google_ai.py index 5e33550c..c049eaa2 100644 --- a/application/llm/google_ai.py +++ b/application/llm/google_ai.py @@ -1,5 +1,9 @@ from google import genai from google.genai import types +import os +import logging +import mimetypes +import json from application.llm.base import BaseLLM @@ -9,6 +13,138 @@ class GoogleLLM(BaseLLM): super().__init__(*args, **kwargs) self.api_key = api_key self.user_api_key = user_api_key + self.client = genai.Client(api_key=self.api_key) + + def get_supported_attachment_types(self): + """ + Return a list of MIME types supported by Google Gemini for file uploads. + + Returns: + list: List of supported MIME types + """ + return [ + 'application/pdf', + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/webp', + 'image/gif' + ] + + def prepare_messages_with_attachments(self, messages, attachments=None): + """ + Process attachments using Google AI's file API for more efficient handling. + + Args: + messages (list): List of message dictionaries. + attachments (list): List of attachment dictionaries with content and metadata. + + Returns: + list: Messages formatted with file references for Google AI API. + """ + if not attachments: + return messages + + prepared_messages = messages.copy() + + # Find the user message to attach files to the last one + user_message_index = None + for i in range(len(prepared_messages) - 1, -1, -1): + if prepared_messages[i].get("role") == "user": + user_message_index = i + break + + if user_message_index is None: + user_message = {"role": "user", "content": []} + prepared_messages.append(user_message) + user_message_index = len(prepared_messages) - 1 + + if isinstance(prepared_messages[user_message_index].get("content"), str): + text_content = prepared_messages[user_message_index]["content"] + prepared_messages[user_message_index]["content"] = [ + {"type": "text", "text": text_content} + ] + elif not isinstance(prepared_messages[user_message_index].get("content"), list): + prepared_messages[user_message_index]["content"] = [] + + files = [] + for attachment in attachments: + mime_type = attachment.get('mime_type') + if not mime_type: + file_path = attachment.get('path') + if file_path: + mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + + if mime_type in self.get_supported_attachment_types(): + try: + file_uri = self._upload_file_to_google(attachment) + logging.info(f"GoogleLLM: Successfully uploaded file, got URI: {file_uri}") + files.append({"file_uri": file_uri, "mime_type": mime_type}) + except Exception as e: + logging.error(f"GoogleLLM: Error uploading file: {e}") + if 'content' in attachment: + prepared_messages[user_message_index]["content"].append({ + "type": "text", + "text": f"[File could not be processed: {attachment.get('path', 'unknown')}]" + }) + + if files: + logging.info(f"GoogleLLM: Adding {len(files)} files to message") + prepared_messages[user_message_index]["content"].append({ + "files": files + }) + + return prepared_messages + + def _upload_file_to_google(self, attachment): + """ + Upload a file to Google AI and return the file URI. + + Args: + attachment (dict): Attachment dictionary with path and metadata. + + Returns: + str: Google AI file URI for the uploaded file. + """ + if 'google_file_uri' in attachment: + return attachment['google_file_uri'] + + file_path = attachment.get('path') + if not file_path: + raise ValueError("No file path provided in attachment") + + if not os.path.isabs(file_path): + current_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + file_path = os.path.join(current_dir, "application", file_path) + + if not os.path.exists(file_path): + raise FileNotFoundError(f"File not found: {file_path}") + + mime_type = attachment.get('mime_type') + if not mime_type: + mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + + try: + response = self.client.files.upload(file=file_path) + + file_uri = response.uri + + from application.core.mongo_db import MongoDB + mongo = MongoDB.get_client() + db = mongo["docsgpt"] + attachments_collection = db["attachments"] + if '_id' in attachment: + attachments_collection.update_one( + {"_id": attachment['_id']}, + {"$set": {"google_file_uri": file_uri}} + ) + + return file_uri + except Exception as e: + logging.error(f"Error uploading file to Google AI: {e}") + raise def _clean_messages_google(self, messages): cleaned_messages = [] @@ -26,7 +162,7 @@ class GoogleLLM(BaseLLM): elif isinstance(content, list): for item in content: if "text" in item: - parts.append(types.Part.from_text(item["text"])) + parts.append(types.Part.from_text(text=item["text"])) elif "function_call" in item: parts.append( types.Part.from_function_call( @@ -41,6 +177,14 @@ class GoogleLLM(BaseLLM): response=item["function_response"]["response"], ) ) + elif "files" in item: + for file_data in item["files"]: + parts.append( + types.Part.from_uri( + file_uri=file_data["file_uri"], + mime_type=file_data["mime_type"] + ) + ) else: raise ValueError( f"Unexpected content dictionary format:{item}" @@ -145,12 +289,26 @@ class GoogleLLM(BaseLLM): if tools: cleaned_tools = self._clean_tools_format(tools) config.tools = cleaned_tools - + + # Check if we have both tools and file attachments + has_attachments = False + for message in messages: + for part in message.parts: + if hasattr(part, 'file_data') and part.file_data is not None: + has_attachments = True + break + if has_attachments: + break + + logging.info(f"GoogleLLM: Starting stream generation. Model: {model}, Messages: {json.dumps(messages, default=str)}, Has attachments: {has_attachments}") + response = client.models.generate_content_stream( model=model, contents=messages, config=config, ) + + for chunk in response: if hasattr(chunk, "candidates") and chunk.candidates: for candidate in chunk.candidates: diff --git a/application/llm/openai.py b/application/llm/openai.py index 5e11a072..75bd37e0 100644 --- a/application/llm/openai.py +++ b/application/llm/openai.py @@ -1,4 +1,8 @@ import json +import base64 +import os +import mimetypes +import logging from application.core.settings import settings from application.llm.base import BaseLLM @@ -65,6 +69,15 @@ class OpenAILLM(BaseLLM): ), } ) + elif isinstance(item, dict): + content_parts = [] + if "text" in item: + content_parts.append({"type": "text", "text": item["text"]}) + elif "type" in item and item["type"] == "text" and "text" in item: + content_parts.append(item) + elif "type" in item and item["type"] == "file" and "file" in item: + content_parts.append(item) + cleaned_messages.append({"role": role, "content": content_parts}) else: raise ValueError( f"Unexpected content dictionary format: {item}" @@ -133,6 +146,183 @@ class OpenAILLM(BaseLLM): def _supports_tools(self): return True + def get_supported_attachment_types(self): + """ + Return a list of MIME types supported by OpenAI for file uploads. + + Returns: + list: List of supported MIME types + """ + return [ + 'application/pdf', + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/webp', + 'image/gif' + ] + + def prepare_messages_with_attachments(self, messages, attachments=None): + """ + Process attachments using OpenAI's file API for more efficient handling. + + Args: + messages (list): List of message dictionaries. + attachments (list): List of attachment dictionaries with content and metadata. + + Returns: + list: Messages formatted with file references for OpenAI API. + """ + if not attachments: + return messages + + prepared_messages = messages.copy() + + # Find the user message to attach file_id to the last one + user_message_index = None + for i in range(len(prepared_messages) - 1, -1, -1): + if prepared_messages[i].get("role") == "user": + user_message_index = i + break + + if user_message_index is None: + user_message = {"role": "user", "content": []} + prepared_messages.append(user_message) + user_message_index = len(prepared_messages) - 1 + + if isinstance(prepared_messages[user_message_index].get("content"), str): + text_content = prepared_messages[user_message_index]["content"] + prepared_messages[user_message_index]["content"] = [ + {"type": "text", "text": text_content} + ] + elif not isinstance(prepared_messages[user_message_index].get("content"), list): + prepared_messages[user_message_index]["content"] = [] + + for attachment in attachments: + mime_type = attachment.get('mime_type') + if not mime_type: + file_path = attachment.get('path') + if file_path: + mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + + if mime_type and mime_type.startswith('image/'): + try: + base64_image = self._get_base64_image(attachment) + prepared_messages[user_message_index]["content"].append({ + "type": "image_url", + "image_url": { + "url": f"data:{mime_type};base64,{base64_image}" + } + }) + except Exception as e: + logging.error(f"Error processing image attachment: {e}") + if 'content' in attachment: + prepared_messages[user_message_index]["content"].append({ + "type": "text", + "text": f"[Image could not be processed: {attachment.get('path', 'unknown')}]" + }) + # Handle PDFs using the file API + elif mime_type == 'application/pdf': + try: + file_id = self._upload_file_to_openai(attachment) + + prepared_messages[user_message_index]["content"].append({ + "type": "file", + "file": {"file_id": file_id} + }) + except Exception as e: + logging.error(f"Error uploading PDF to OpenAI: {e}") + if 'content' in attachment: + prepared_messages[user_message_index]["content"].append({ + "type": "text", + "text": f"File content:\n\n{attachment['content']}" + }) + + return prepared_messages + + def _get_base64_image(self, attachment): + """ + Convert an image file to base64 encoding. + + Args: + attachment (dict): Attachment dictionary with path and metadata. + + Returns: + str: Base64-encoded image data. + """ + file_path = attachment.get('path') + if not file_path: + raise ValueError("No file path provided in attachment") + + if not os.path.isabs(file_path): + current_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + file_path = os.path.join(current_dir, "application", file_path) + + if not os.path.exists(file_path): + raise FileNotFoundError(f"File not found: {file_path}") + + with open(file_path, "rb") as image_file: + return base64.b64encode(image_file.read()).decode('utf-8') + + def _upload_file_to_openai(self, attachment): ##pdfs + """ + Upload a file to OpenAI and return the file_id. + + Args: + attachment (dict): Attachment dictionary with path and metadata. + Expected keys: + - path: Path to the file + - id: Optional MongoDB ID for caching + + Returns: + str: OpenAI file_id for the uploaded file. + """ + import os + import logging + + if 'openai_file_id' in attachment: + return attachment['openai_file_id'] + + file_path = attachment.get('path') + if not file_path: + raise ValueError("No file path provided in attachment") + + if not os.path.isabs(file_path): + current_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + file_path = os.path.join(current_dir,"application", file_path) + + if not os.path.exists(file_path): + raise FileNotFoundError(f"File not found: {file_path}") + + + try: + with open(file_path, 'rb') as file: + response = self.client.files.create( + file=file, + purpose="assistants" + ) + + file_id = response.id + + from application.core.mongo_db import MongoDB + mongo = MongoDB.get_client() + db = mongo["docsgpt"] + attachments_collection = db["attachments"] + if '_id' in attachment: + attachments_collection.update_one( + {"_id": attachment['_id']}, + {"$set": {"openai_file_id": file_id}} + ) + + return file_id + except Exception as e: + logging.error(f"Error uploading file to OpenAI: {e}") + raise + class AzureOpenAILLM(OpenAILLM): diff --git a/application/worker.py b/application/worker.py index 23ff0422..bbd422ac 100755 --- a/application/worker.py +++ b/application/worker.py @@ -328,34 +328,30 @@ def attachment_worker(self, directory, file_info, user): """ import datetime import os + import mimetypes from application.utils import num_tokens_from_string mongo = MongoDB.get_client() db = mongo["docsgpt"] attachments_collection = db["attachments"] - job_name = file_info["folder"] - logging.info(f"Processing attachment: {job_name}", extra={"user": user, "job": job_name}) + filename = file_info["filename"] + attachment_id = file_info["attachment_id"] + + logging.info(f"Processing attachment: {attachment_id}/{filename}", extra={"user": user}) self.update_state(state="PROGRESS", meta={"current": 10}) - folder_name = file_info["folder"] - filename = file_info["filename"] - file_path = os.path.join(directory, filename) - - logging.info(f"Processing file: {file_path}", extra={"user": user, "job": job_name}) - if not os.path.exists(file_path): - logging.warning(f"File not found: {file_path}", extra={"user": user, "job": job_name}) - return {"error": "File not found"} + logging.warning(f"File not found: {file_path}", extra={"user": user}) + raise FileNotFoundError(f"File not found: {file_path}") try: reader = SimpleDirectoryReader( input_files=[file_path] ) - documents = reader.load_data() self.update_state(state="PROGRESS", meta={"current": 50}) @@ -364,33 +360,37 @@ def attachment_worker(self, directory, file_info, user): content = documents[0].text token_count = num_tokens_from_string(content) - file_path_relative = f"{user}/attachments/{folder_name}/{filename}" + file_path_relative = f"{settings.UPLOAD_FOLDER}/{user}/attachments/{attachment_id}/{filename}" - attachment_id = attachments_collection.insert_one({ + mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + + doc_id = ObjectId(attachment_id) + attachments_collection.insert_one({ + "_id": doc_id, "user": user, "path": file_path_relative, "content": content, "token_count": token_count, + "mime_type": mime_type, "date": datetime.datetime.now(), - }).inserted_id + }) logging.info(f"Stored attachment with ID: {attachment_id}", - extra={"user": user, "job": job_name}) + extra={"user": user}) self.update_state(state="PROGRESS", meta={"current": 100}) return { - "attachment_id": str(attachment_id), "filename": filename, - "folder": folder_name, "path": file_path_relative, - "token_count": token_count + "token_count": token_count, + "attachment_id": attachment_id, + "mime_type": mime_type } else: logging.warning("No content was extracted from the file", - extra={"user": user, "job": job_name}) - return {"error": "No content was extracted from the file"} + extra={"user": user}) + raise ValueError("No content was extracted from the file") except Exception as e: - logging.error(f"Error processing file {filename}: {e}", - extra={"user": user, "job": job_name}, exc_info=True) - return {"error": f"Error processing file: {str(e)}"} + logging.error(f"Error processing file {filename}: {e}", extra={"user": user}, exc_info=True) + raise diff --git a/frontend/src/components/MessageInput.tsx b/frontend/src/components/MessageInput.tsx index 1e3576ee..10ea6738 100644 --- a/frontend/src/components/MessageInput.tsx +++ b/frontend/src/components/MessageInput.tsx @@ -1,21 +1,30 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef,useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDarkTheme } from '../hooks'; import { useSelector, useDispatch } from 'react-redux'; import userService from '../api/services/userService'; import endpoints from '../api/endpoints'; +import { getOS, isTouchDevice } from '../utils/browserUtils'; import PaperPlane from '../assets/paper_plane.svg'; import SourceIcon from '../assets/source.svg'; import ToolIcon from '../assets/tool.svg'; import SpinnerDark from '../assets/spinner-dark.svg'; import Spinner from '../assets/spinner.svg'; +import ExitIcon from '../assets/exit.svg'; +import AlertIcon from '../assets/alert.svg'; import SourcesPopup from './SourcesPopup'; import ToolsPopup from './ToolsPopup'; import { selectSelectedDocs, selectToken } from '../preferences/preferenceSlice'; import { ActiveState } from '../models/misc'; import Upload from '../upload/Upload'; import ClipIcon from '../assets/clip.svg'; -import { setAttachments } from '../conversation/conversationSlice'; +import { + addAttachment, + updateAttachment, + removeAttachment, + selectAttachments +} from '../conversation/conversationSlice'; + interface MessageInputProps { value: string; @@ -47,13 +56,33 @@ export default function MessageInput({ const [isSourcesPopupOpen, setIsSourcesPopupOpen] = useState(false); const [isToolsPopupOpen, setIsToolsPopupOpen] = useState(false); const [uploadModalState, setUploadModalState] = useState('INACTIVE'); - const [uploads, setUploads] = useState([]); const selectedDocs = useSelector(selectSelectedDocs); const token = useSelector(selectToken); + const attachments = useSelector(selectAttachments); const dispatch = useDispatch(); + const browserOS = getOS(); + const isTouch = isTouchDevice(); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + ((browserOS === 'win' || browserOS === 'linux') && event.ctrlKey && event.key === 'k') || + (browserOS === 'mac' && event.metaKey && event.key === 'k') + ) { + event.preventDefault(); + setIsSourcesPopupOpen(!isSourcesPopupOpen); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [browserOS]); + const handleFileAttachment = (e: React.ChangeEvent) => { if (!e.target.files || e.target.files.length === 0) return; @@ -64,56 +93,51 @@ export default function MessageInput({ const apiHost = import.meta.env.VITE_API_HOST; const xhr = new XMLHttpRequest(); - const uploadState: UploadState = { - taskId: '', + const newAttachment = { fileName: file.name, progress: 0, - status: 'uploading' + status: 'uploading' as const, + taskId: '', }; - setUploads(prev => [...prev, uploadState]); - const uploadIndex = uploads.length; + dispatch(addAttachment(newAttachment)); xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable) { const progress = Math.round((event.loaded / event.total) * 100); - setUploads(prev => prev.map((upload, index) => - index === uploadIndex - ? { ...upload, progress } - : upload - )); + dispatch(updateAttachment({ + taskId: newAttachment.taskId, + updates: { progress } + })); } }); xhr.onload = () => { if (xhr.status === 200) { const response = JSON.parse(xhr.responseText); - console.log('File uploaded successfully:', response); - if (response.task_id) { - setUploads(prev => prev.map((upload, index) => - index === uploadIndex - ? { ...upload, taskId: response.task_id, status: 'processing' } - : upload - )); + dispatch(updateAttachment({ + taskId: newAttachment.taskId, + updates: { + taskId: response.task_id, + status: 'processing', + progress: 10 + } + })); } } else { - setUploads(prev => prev.map((upload, index) => - index === uploadIndex - ? { ...upload, status: 'failed' } - : upload - )); - console.error('Error uploading file:', xhr.responseText); + dispatch(updateAttachment({ + taskId: newAttachment.taskId, + updates: { status: 'failed' } + })); } }; xhr.onerror = () => { - setUploads(prev => prev.map((upload, index) => - index === uploadIndex - ? { ...upload, status: 'failed' } - : upload - )); - console.error('Network error during file upload'); + dispatch(updateAttachment({ + taskId: newAttachment.taskId, + updates: { status: 'failed' } + })); }; xhr.open('POST', `${apiHost}${endpoints.USER.STORE_ATTACHMENT}`); @@ -123,64 +147,55 @@ export default function MessageInput({ }; useEffect(() => { - let timeoutIds: number[] = []; - const checkTaskStatus = () => { - const processingUploads = uploads.filter(upload => - upload.status === 'processing' && upload.taskId + const processingAttachments = attachments.filter(att => + att.status === 'processing' && att.taskId ); - processingUploads.forEach(upload => { + processingAttachments.forEach(attachment => { userService - .getTaskStatus(upload.taskId, null) + .getTaskStatus(attachment.taskId!, null) .then((data) => data.json()) .then((data) => { - console.log('Task status:', data); - - setUploads(prev => prev.map(u => { - if (u.taskId !== upload.taskId) return u; - - if (data.status === 'SUCCESS') { - return { - ...u, + if (data.status === 'SUCCESS') { + dispatch(updateAttachment({ + taskId: attachment.taskId!, + updates: { status: 'completed', progress: 100, - attachment_id: data.result?.attachment_id, + id: data.result?.attachment_id, token_count: data.result?.token_count - }; - } else if (data.status === 'FAILURE') { - return { ...u, status: 'failed' }; - } else if (data.status === 'PROGRESS' && data.result?.current) { - return { ...u, progress: data.result.current }; - } - return u; - })); - - if (data.status !== 'SUCCESS' && data.status !== 'FAILURE') { - const timeoutId = window.setTimeout(() => checkTaskStatus(), 2000); - timeoutIds.push(timeoutId); + } + })); + } else if (data.status === 'FAILURE') { + dispatch(updateAttachment({ + taskId: attachment.taskId!, + updates: { status: 'failed' } + })); + } else if (data.status === 'PROGRESS' && data.result?.current) { + dispatch(updateAttachment({ + taskId: attachment.taskId!, + updates: { progress: data.result.current } + })); } }) - .catch((error) => { - console.error('Error checking task status:', error); - setUploads(prev => prev.map(u => - u.taskId === upload.taskId - ? { ...u, status: 'failed' } - : u - )); + .catch(() => { + dispatch(updateAttachment({ + taskId: attachment.taskId!, + updates: { status: 'failed' } + })); }); }); }; - if (uploads.some(upload => upload.status === 'processing')) { - const timeoutId = window.setTimeout(checkTaskStatus, 2000); - timeoutIds.push(timeoutId); - } + const interval = setInterval(() => { + if (attachments.some(att => att.status === 'processing')) { + checkTaskStatus(); + } + }, 2000); - return () => { - timeoutIds.forEach(id => clearTimeout(id)); - }; - }, [uploads]); + return () => clearInterval(interval); + }, [attachments, dispatch]); const handleInput = () => { if (inputRef.current) { @@ -215,39 +230,53 @@ export default function MessageInput({ const handleSubmit = () => { - const completedAttachments = uploads - .filter(upload => upload.status === 'completed' && upload.attachment_id) - .map(upload => ({ - fileName: upload.fileName, - id: upload.attachment_id as string - })); - - dispatch(setAttachments(completedAttachments)); - onSubmit(); }; return (
- {uploads.map((upload, index) => ( + {attachments.map((attachment, index) => (
- {upload.fileName} + {attachment.fileName} - {upload.status === 'completed' && ( - + {attachment.status === 'completed' && ( + )} - - {upload.status === 'failed' && ( - + + {attachment.status === 'failed' && ( + Upload failed )} - - {(upload.status === 'uploading' || upload.status === 'processing') && ( + + {(attachment.status === 'uploading' || attachment.status === 'processing') && (
+ {/* Background circle */} @@ -298,15 +327,21 @@ export default function MessageInput({
)} -
+ ); -} \ No newline at end of file +} diff --git a/frontend/src/conversation/ConversationBubble.tsx b/frontend/src/conversation/ConversationBubble.tsx index ec244087..a241b2d3 100644 --- a/frontend/src/conversation/ConversationBubble.tsx +++ b/frontend/src/conversation/ConversationBubble.tsx @@ -57,7 +57,6 @@ const ConversationBubble = forwardRef< updated?: boolean, index?: number, ) => void; - attachments?: { fileName: string; id: string }[]; } >(function ConversationBubble( { @@ -72,7 +71,6 @@ const ConversationBubble = forwardRef< retryBtn, questionNumber, handleUpdatedQuestionSubmission, - attachments, }, ref, ) { @@ -99,36 +97,6 @@ const ConversationBubble = forwardRef< handleUpdatedQuestionSubmission?.(editInputBox, true, questionNumber); }; let bubble; - const renderAttachments = () => { - if (!attachments || attachments.length === 0) return null; - - return ( -
- {attachments.map((attachment, index) => ( -
- - - - {attachment.fileName} -
- ))} -
- ); - }; if (type === 'QUESTION') { bubble = (
{message}
- {renderAttachments()}