diff --git a/backend/app.py b/backend/app.py index 903354e5..1caf1634 100644 --- a/backend/app.py +++ b/backend/app.py @@ -139,6 +139,8 @@ def chatgpt(): start_time = time.time() # Start the timer conversation_id = request.json["conversation_id"] question = request.json["query"] + file = request.json["file"] + logging.info(f"[webbackend] from orchestrator file : {file}") logging.info("[webbackend] conversation_id: " + conversation_id) logging.info("[webbackend] question: " + question) @@ -217,6 +219,38 @@ def getGptSpeechToken(): logging.exception("[webbackend] exception in /api/get-speech-token") return jsonify({"error": str(e)}), 500 +@app.route("/api/upload-blob", methods=["POST"]) +def uploadBlob(): + try: + # Retrieve the file from the request + uploaded_file = request.files['file'] + if not uploaded_file: + return jsonify({"error": "No file provided."}), 400 + + # Generate a blob name (you can customize this) + blob_name = uploaded_file.filename + + # Authenticate with Azure Blob Storage + client_credential = DefaultAzureCredential() + blob_service_client = BlobServiceClient( + f"https://{STORAGE_ACCOUNT}.blob.core.windows.net", + client_credential + ) + + # Get a blob client + blob_client = blob_service_client.get_blob_client(container='attachments', blob=blob_name) + + # Upload the file + blob_client.upload_blob(uploaded_file.read(), overwrite=True) + logging.info(f"Successfully uploaded blob: {blob_name}") + + # Return the blob name + return jsonify({"blob_name": blob_name}), 200 + + except Exception as e: + logging.exception("[webbackend] exception in /api/upload-blob") + return jsonify({"error": str(e)}), 500 + @app.route("/api/get-storage-account", methods=["GET"]) def getStorageAccount(): if not STORAGE_ACCOUNT: @@ -230,6 +264,8 @@ def getStorageAccount(): @app.route("/api/get-blob", methods=["POST"]) def getBlob(): blob_name = unquote(request.json["blob_name"]) + container = request.json["container"] + logging.info(f"Starting getBlob function for blob: {blob_name}") try: client_credential = DefaultAzureCredential() @@ -237,7 +273,9 @@ def getBlob(): f"https://{STORAGE_ACCOUNT}.blob.core.windows.net", client_credential ) - blob_client = blob_service_client.get_blob_client(container='documents', blob=blob_name) + if not container : + container = 'documents' + blob_client = blob_service_client.get_blob_client(container=container, blob=blob_name) blob_data = blob_client.download_blob() blob_text = blob_data.readall() logging.info(f"Successfully fetched blob: {blob_name}") diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 5851e351..90482f73 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -13,6 +13,7 @@ export async function chatApiGpt(options: ChatRequestGpt): Promise void; + onSend: (question: string, file?: File) => void; disabled: boolean; placeholder?: string; clearOnSend?: boolean; @@ -15,16 +16,18 @@ interface Props { export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Props) => { const [question, setQuestion] = useState(""); + const [selectedFile, setSelectedFile] = useState(null); const sendQuestion = () => { if (disabled || !question.trim()) { return; } - onSend(question); + onSend(question, selectedFile || undefined); if (clearOnSend) { setQuestion(""); + setSelectedFile(null); } }; @@ -66,6 +69,12 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr } }; + const onFileChange = (ev: React.ChangeEvent) => { + if (ev.target.files && ev.target.files.length > 0) { + setSelectedFile(ev.target.files[0]); + } + }; + const sendQuestionDisabled = disabled || !question.trim(); return ( @@ -80,6 +89,19 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr onChange={onQuestionChange} onKeyDown={onEnterPress} /> + {selectedFile && ( +
+ + {selectedFile.name} + setSelectedFile(null)} + title="Remove attached file" + > + ✕ + +
+ )}
+ +
); + }; diff --git a/frontend/src/components/UserChatMessage/UserChatMessage.module.css b/frontend/src/components/UserChatMessage/UserChatMessage.module.css index c80bea6f..f8dd91c5 100644 --- a/frontend/src/components/UserChatMessage/UserChatMessage.module.css +++ b/frontend/src/components/UserChatMessage/UserChatMessage.module.css @@ -13,3 +13,17 @@ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); outline: transparent solid 1px; } + +.file { + margin-top: 5px; + text-align: left; +} + +.image { + max-width: 100%; + max-height: 200px; + width: auto; + height: auto; + border-radius: 5px; + object-fit: contain; +} diff --git a/frontend/src/components/UserChatMessage/UserChatMessage.tsx b/frontend/src/components/UserChatMessage/UserChatMessage.tsx index 2f5e8d3d..4fec5476 100644 --- a/frontend/src/components/UserChatMessage/UserChatMessage.tsx +++ b/frontend/src/components/UserChatMessage/UserChatMessage.tsx @@ -1,13 +1,69 @@ import styles from "./UserChatMessage.module.css"; +import { useEffect, useState } from "react"; + interface Props { message: string; + file: string | null; // Blob name } -export const UserChatMessage = ({ message }: Props) => { +export const UserChatMessage = ({ message, file }: Props) => { + const [blobUrl, setBlobUrl] = useState(null); + + useEffect(() => { + const fetchBlobFile = async () => { + if (file) { + try { + const response = await fetch("/api/get-blob", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ blob_name: file,container:'attachments' }), + }); + + if (!response.ok) { + throw new Error(`Error fetching blob: ${response.statusText}`); + } + + const blob = await response.blob(); + const url = URL.createObjectURL(blob); // Create a temporary URL for the blob + setBlobUrl(url); + } catch (error) { + console.error("Failed to fetch blob file:", error); + } + } + }; + + fetchBlobFile(); + + return () => { + if (blobUrl) { + URL.revokeObjectURL(blobUrl); + } + }; + }, [file]); + return (
-
{message}
+
{message} + {file && blobUrl && ( +
+ {file.endsWith(".png") || file.endsWith(".jpg") || file.endsWith(".jpeg") ? ( + Blob Content + ) : ( + + {file.split("/").pop() || "Download File"} + + )} +
+ )} +
); -}; + +}; \ No newline at end of file diff --git a/frontend/src/pages/chat/Chat.module.css b/frontend/src/pages/chat/Chat.module.css index 91c9818a..c156dc92 100644 --- a/frontend/src/pages/chat/Chat.module.css +++ b/frontend/src/pages/chat/Chat.module.css @@ -113,3 +113,10 @@ margin-right: 20px; margin-bottom: 20px; } +.imagePreview { + max-width: 100%; /* Ensure it fits within the container */ + max-height: 400px; /* Limit the height */ + object-fit: contain; /* Preserve aspect ratio */ + display: block; /* Avoid inline-block space issues */ + margin: 0 auto; /* Center the image horizontally */ +} \ No newline at end of file diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 5c26b508..8eb62f68 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -48,8 +48,10 @@ const Chat = () => { const [userId, setUserId] = useState(""); const triggered = useRef(false); + const [filePreview, setFilePreview] = useState(null); - const makeApiRequestGpt = async (question: string) => { + + const makeApiRequestGpt = async (question: string,selectedFile?: File) => { lastQuestionRef.current = question; error && setError(undefined); @@ -58,12 +60,43 @@ const Chat = () => { setActiveAnalysisPanelTab(undefined); try { + let fileUrl = null; + if (selectedFile) { + const formData = new FormData(); + formData.append("file", selectedFile); + + // Extract MIME type for file + const fileMimeType = selectedFile.type; + setFileType(fileMimeType); + console.log('error {',fileMimeType); + + // Preview the file based on its type + const previewUrl = URL.createObjectURL(selectedFile); + setFilePreview(previewUrl); + + try { + const uploadResponse = await fetch("/api/upload-blob", { + method: "POST", + body: formData, + }); + if (!uploadResponse.ok) { + throw new Error("Failed to upload file"); + } + const uploadResult = await uploadResponse.json(); + fileUrl = uploadResult.blob_name; // Assuming the API returns the file URL + } catch (uploadError) { + console.error("File upload error:", uploadError); + setError(uploadError); + return; + } + } const history: ChatTurn[] = answers.map(a => ({ user: a[0], bot: a[1].answer })); const request: ChatRequestGpt = { history: [...history, { user: question, bot: undefined }], approach: Approaches.ReadRetrieveRead, conversation_id: userId, query: question, + file: fileUrl, overrides: { promptTemplate: promptTemplate.length === 0 ? undefined : promptTemplate, excludeCategory: excludeCategory.length === 0 ? undefined : excludeCategory, @@ -261,7 +294,6 @@ const Chat = () => { console.log('activeAnalysisPanelTab is now:', activeAnalysisPanelTab); }; - return (
@@ -276,7 +308,7 @@ const Chat = () => {
{answers.map((answer, index) => (
- +
{ ))} {isLoading && ( <> - +
@@ -302,7 +334,7 @@ const Chat = () => { )} {error ? ( <> - +
{ )}
- makeApiRequestGpt(question)} /> + makeApiRequestGpt(question, selectedFile)} />