Skip to content
Open
40 changes: 39 additions & 1 deletion backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -230,14 +264,18 @@ 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()
blob_service_client = BlobServiceClient(
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}")
Expand Down
1 change: 1 addition & 0 deletions frontend/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export async function chatApiGpt(options: ChatRequestGpt): Promise<AskResponseGp
approach: options.approach,
conversation_id: options.conversation_id,
query: options.query,
file: options.file,
overrides: {
semantic_ranker: options.overrides?.semanticRanker,
semantic_captions: options.overrides?.semanticCaptions,
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/api/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type AskRequest = {

export type AskResponse = {
answer: string;
file: string | null;
thoughts: string | null;
data_points: string[];
error?: string;
Expand All @@ -38,6 +39,7 @@ export type TransactionData = {
export type AskResponseGpt= {
conversation_id: string;
answer: string;
file : string | null;
current_state: string;
thoughts: string | null;
data_points: string[];
Expand All @@ -61,6 +63,7 @@ export type ChatRequestGpt = {
approach: Approaches;
conversation_id: string;
query: string;
file: string;
overrides?: AskRequestOverrides;
};

41 changes: 37 additions & 4 deletions frontend/src/components/QuestionInput/QuestionInput.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
import { useState } from "react";
import { Stack, TextField } from "@fluentui/react";
import { getTokenOrRefresh } from './token_util';
import { Send28Filled, BookOpenMicrophone28Filled, SlideMicrophone32Filled } from "@fluentui/react-icons";
import { Send28Filled, BookOpenMicrophone28Filled, SlideMicrophone32Filled, AttachFilled } from "@fluentui/react-icons";
import { ResultReason, SpeechConfig, AudioConfig, SpeechRecognizer } from 'microsoft-cognitiveservices-speech-sdk';
import { getLanguageText } from '../../utils/languageUtils';

import styles from "./QuestionInput.module.css";

interface Props {
onSend: (question: string) => void;
onSend: (question: string, file?: File) => void;
disabled: boolean;
placeholder?: string;
clearOnSend?: boolean;
}

export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Props) => {
const [question, setQuestion] = useState<string>("");
const [selectedFile, setSelectedFile] = useState<File | null>(null);

const sendQuestion = () => {
if (disabled || !question.trim()) {
return;
}

onSend(question);
onSend(question, selectedFile || undefined);

if (clearOnSend) {
setQuestion("");
setSelectedFile(null);
}
};

Expand Down Expand Up @@ -66,6 +69,12 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr
}
};

const onFileChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
if (ev.target.files && ev.target.files.length > 0) {
setSelectedFile(ev.target.files[0]);
}
};

const sendQuestionDisabled = disabled || !question.trim();

return (
Expand All @@ -80,6 +89,19 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr
onChange={onQuestionChange}
onKeyDown={onEnterPress}
/>
{selectedFile && (
<div className={styles.fileAttachmentIndicator}>
<AttachFilled primaryFill="rgba(115, 118, 225, 1)" />
<span className={styles.fileName}>{selectedFile.name}</span>
<span
className={styles.clearFile}
onClick={() => setSelectedFile(null)}
title="Remove attached file"
>
</span>
</div>
)}
<div className={styles.questionInputButtonsContainer}>
<div
className={`${styles.questionInputSendButton} ${sendQuestionDisabled ? styles.questionInputSendButtonDisabled : ""}`}
Expand All @@ -89,13 +111,24 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr
<Send28Filled primaryFill="rgba(115, 118, 225, 1)" />
</div>
<div
className={`${styles.questionInputSendButton}}`}
className={`${styles.questionInputSendButton}`}
aria-label="Talk"
onClick={sttFromMic}
>
<SlideMicrophone32Filled primaryFill="rgba(115, 118, 225, 1)" />
</div>
<label htmlFor="file-upload" className={styles.questionInputFileButton}>
<AttachFilled primaryFill="rgba(115, 118, 225, 1)" />
</label>
<input
id="file-upload"
type="file"
style={{ display: "none" }}
onChange={onFileChange}
disabled={disabled}
/>
</div>
</Stack>
);

};
14 changes: 14 additions & 0 deletions frontend/src/components/UserChatMessage/UserChatMessage.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
62 changes: 59 additions & 3 deletions frontend/src/components/UserChatMessage/UserChatMessage.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(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 (
<div className={styles.container}>
<div className={styles.message}>{message}</div>
<div className={styles.message}>{message}
{file && blobUrl && (
<div className={styles.file}>
{file.endsWith(".png") || file.endsWith(".jpg") || file.endsWith(".jpeg") ? (
<img
src={blobUrl}
alt="Blob Content"
className={styles.image} // Add this class for styling
/>
) : (
<a href={blobUrl} download={file} rel="noopener noreferrer">
{file.split("/").pop() || "Download File"}
</a>
)}
</div>
)}
</div>
</div>
);
};

};
7 changes: 7 additions & 0 deletions frontend/src/pages/chat/Chat.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
}
44 changes: 38 additions & 6 deletions frontend/src/pages/chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ const Chat = () => {
const [userId, setUserId] = useState<string>("");
const triggered = useRef(false);

const [filePreview, setFilePreview] = useState<string | null>(null);

const makeApiRequestGpt = async (question: string) => {

const makeApiRequestGpt = async (question: string,selectedFile?: File) => {
lastQuestionRef.current = question;

error && setError(undefined);
Expand All @@ -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,
Expand Down Expand Up @@ -261,7 +294,6 @@ const Chat = () => {
console.log('activeAnalysisPanelTab is now:', activeAnalysisPanelTab);
};


return (
<div className={styles.container}>
<div className={styles.commandsContainer}>
Expand All @@ -276,7 +308,7 @@ const Chat = () => {
<div className={styles.chatMessageStream}>
{answers.map((answer, index) => (
<div key={index}>
<UserChatMessage message={answer[0]} />
<UserChatMessage message={answer[0]} file={answer[1].file}/>
<div className={styles.chatMessageGpt}>
<Answer
key={index}
Expand All @@ -294,15 +326,15 @@ const Chat = () => {
))}
{isLoading && (
<>
<UserChatMessage message={lastQuestionRef.current} />
<UserChatMessage message={lastQuestionRef.current} file={null} />
<div className={styles.chatMessageGptMinWidth}>
<AnswerLoading />
</div>
</>
)}
{error ? (
<>
<UserChatMessage message={lastQuestionRef.current} />
<UserChatMessage message={lastQuestionRef.current} file={null}/>
<div className={styles.chatMessageGptMinWidth}>
<AnswerError
error={error.toString() === "SyntaxError: Unexpected end of JSON input"
Expand All @@ -318,7 +350,7 @@ const Chat = () => {
)}

<div className={styles.chatInput}>
<QuestionInput clearOnSend placeholder={placeholderText} disabled={isLoading} onSend={question => makeApiRequestGpt(question)} />
<QuestionInput clearOnSend placeholder={placeholderText} disabled={isLoading} onSend={(question, selectedFile) => makeApiRequestGpt(question, selectedFile)} />
</div>
</div>

Expand Down