Skip to content

Commit fe0e809

Browse files
committed
Include Flask-Mail package for python backend; Create simple function for sending transcript to server and send email via smtp with gmail; Create modal for users to send feedback
1 parent a470015 commit fe0e809

File tree

7 files changed

+199
-4
lines changed

7 files changed

+199
-4
lines changed

backend/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,7 @@ DB_USE_SSL=false
2121
# Not used for local dev
2222
#DB_USERNAME=default
2323
#DB_PASSWORD=password
24+
25+
# SMTP setup
26+
#MAIL_USERNAME="[email protected]"
27+
#APP_PASSWORD="app_specific_password"

backend/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ version = "0.2.0"
44
requires-python = ">=3.12"
55
dependencies = [
66
"flask",
7+
"Flask-Mail",
78
"valkey",
89
"gunicorn",
910
"openai==1.89",

backend/tenantfirstaid/app.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from pathlib import Path
2-
from flask import Flask, jsonify, session
2+
from flask import Flask, jsonify, session, request
3+
from flask_mail import Mail, Message
4+
from werkzeug.utils import secure_filename
35
import os
46
import secrets
57

@@ -15,13 +17,57 @@
1517
from .citations import get_citation
1618

1719
app = Flask(__name__)
20+
mail = Mail(app)
1821

1922
# Configure Flask sessions
2023
app.secret_key = os.getenv("FLASK_SECRET_KEY", secrets.token_hex(32))
2124
app.config["SESSION_COOKIE_HTTPONLY"] = True
2225
app.config["SESSION_COOKIE_SECURE"] = os.getenv("ENV", "dev") == "prod"
2326
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
2427

28+
# Configure Flask Mail
29+
app.config.update(
30+
MAIL_SERVER="smtp.gmail.com",
31+
MAIL_PORT=587,
32+
MAIL_USE_TLS=True,
33+
MAIL_USERNAME=os.getenv("MAIL_USERNAME"),
34+
MAIL_PASSWORD=os.getenv("APP_PASSWORD"),
35+
)
36+
37+
mail.init_app(app)
38+
39+
40+
def send_feedback():
41+
feedback = request.form.get("feedback")
42+
file = request.files.get("transcript")
43+
44+
if not file:
45+
return "No file provided", 400
46+
47+
filename = secure_filename(file.filename)
48+
filepath = os.path.join("/tmp", filename)
49+
file.save(filepath)
50+
51+
with open(filepath, "r", encoding="utf-8") as f:
52+
html_content = f.read()
53+
54+
msg = Message(
55+
subject="New Feedback with Transcript",
56+
sender=os.getenv("MAIL_USERNAME"),
57+
recipients=["[email protected]"],
58+
body=f"User feedback:\n\n{feedback}",
59+
)
60+
msg.attach(
61+
filename="transcript.html",
62+
content_type="text/html",
63+
data=html_content.encode("utf-8"),
64+
)
65+
66+
mail.send(msg)
67+
os.remove(filepath)
68+
69+
return "Email sent", 200
70+
2571

2672
tenant_session = TenantSession()
2773

@@ -52,5 +98,9 @@ def clear_session():
5298
"/api/citation", endpoint="citation", view_func=get_citation, methods=["GET"]
5399
)
54100

101+
app.add_url_rule(
102+
"/api/feedback", endpoint="feedback", view_func=send_feedback, methods=["POST"]
103+
)
104+
55105
if __name__ == "__main__":
56106
app.run(debug=True, host="0.0.0.0", port=5001)

backend/uv.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useState } from "react";
2+
import sendFeedback from "../utils/feedbackHelper";
3+
import { IMessage } from "../../../hooks/useMessages";
4+
5+
interface Props {
6+
messages: IMessage[];
7+
setOpenFeedback: React.Dispatch<React.SetStateAction<boolean>>;
8+
}
9+
10+
export default function FeedbackModal({ messages, setOpenFeedback }: Props) {
11+
const [feedback, setFeedback] = useState("");
12+
13+
const handleModalClose = () => {
14+
setOpenFeedback(false);
15+
setFeedback("");
16+
};
17+
18+
return (
19+
<dialog
20+
open
21+
className="absolute top-[50%] left-[50%] -translate-x-[50%] -translate-y-[50%] flex flex-col gap-4 items-center justify-center w-[300px] sm:w-[500px] h-[300px] rounded-lg p-4"
22+
>
23+
<textarea
24+
className="resize-none h-[80%] w-full px-3 py-2 border-1 border-[#ddd] rounded-md box-border transition-colors duration-300 focus:outline-0 focus:border-[#4a90e2] focus:shadow-[0_0_0_2px_rgba(74,144,226,0.2)]"
25+
placeholder="Please enter your feedback with regards to the chatbot here..."
26+
onChange={(event) => setFeedback(event.target.value)}
27+
/>
28+
<div className="flex gap-4">
29+
<button
30+
className="border rounded-full px-4 py-1 cursor-pointer"
31+
onClick={() => {
32+
if (feedback.trim() === "") handleModalClose();
33+
setTimeout(() => {
34+
sendFeedback(messages, feedback);
35+
handleModalClose();
36+
}, 1000);
37+
}}
38+
>
39+
Send
40+
</button>
41+
<button
42+
className="border rounded-full px-4 py-1 cursor-pointer"
43+
onClick={handleModalClose}
44+
>
45+
Close
46+
</button>
47+
</div>
48+
</dialog>
49+
);
50+
}

frontend/src/pages/Chat/components/MessageWindow.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import useSession from "../../../hooks/useSession";
66
import ExportMessagesButton from "./ExportMessagesButton";
77
import CitySelectField from "./CitySelectField";
88
import SuggestedPrompts from "./SuggestedPrompts";
9+
import FeedbackModal from "./FeedbackModal";
910

1011
interface Props {
1112
messages: IMessage[];
@@ -24,6 +25,7 @@ export default function MessageWindow({
2425
}: Props) {
2526
const [isLoading, setIsLoading] = useState(false);
2627
const [inputValue, setInputValue] = useState("");
28+
const [openFeedback, setOpenFeedback] = useState(false);
2729
const { handleNewSession } = useSession();
2830
const inputRef = useRef<HTMLTextAreaElement | null>(null);
2931
const messagesRef = useRef<HTMLDivElement | null>(null);
@@ -99,6 +101,9 @@ export default function MessageWindow({
99101
</div>
100102
)}
101103
</div>
104+
{openFeedback && (
105+
<FeedbackModal messages={messages} setOpenFeedback={setOpenFeedback} />
106+
)}
102107
<div>
103108
{messages.length > 0 ? (
104109
<>
@@ -121,9 +126,15 @@ export default function MessageWindow({
121126
>
122127
Clear Chat
123128
</button>
124-
<div className="">
125-
<ExportMessagesButton messages={messages} />
126-
</div>
129+
<ExportMessagesButton messages={messages} />
130+
<button
131+
className="py-2 px-4 border rounded-md font-semibold hover:bg-gray-200 transition-colors cursor-pointer opacity-70"
132+
onClick={() => {
133+
setOpenFeedback(true);
134+
}}
135+
>
136+
Feedback
137+
</button>
127138
</div>
128139
</>
129140
) : (
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { IMessage } from "../../../hooks/useMessages";
2+
3+
function sanitizeText(str: string) {
4+
// Strips anchor tags
5+
str = str.replace(/<a\b[^>]*>(.*?)<\/a>/gi, "$1");
6+
7+
return str
8+
.replace(/&/g, "&amp;")
9+
.replace(/</g, "&lt;")
10+
.replace(/>/g, "&gt;")
11+
.replace(/"/g, "&quot;")
12+
.replace(/'/g, "&#039;");
13+
}
14+
15+
export default async function sendFeedback(
16+
messages: IMessage[],
17+
userFeedback: string
18+
) {
19+
if (messages.length < 2) return;
20+
21+
const messageChain = messages
22+
.map(
23+
({ role, content }) =>
24+
`<p><strong>${
25+
role.charAt(0).toUpperCase() + role.slice(1)
26+
}</strong>: ${sanitizeText(content)}</p>`
27+
)
28+
.join("");
29+
30+
const htmlContent = `
31+
<html>
32+
<head>
33+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'none'; object-src 'none'; base-uri 'none'; style-src 'self'; img-src 'self' data:; font-src 'self'; form-action 'none';">
34+
<title>Conversation History</title>
35+
<style>
36+
body {
37+
font-family: sans-serif;
38+
padding: 20px;
39+
}
40+
strong {
41+
font-weight: bold;
42+
}
43+
p {
44+
margin: 12px;
45+
}
46+
</style>
47+
</head>
48+
<body>
49+
${messageChain}
50+
</body>
51+
</html>
52+
`;
53+
54+
const blob = new Blob([htmlContent], { type: "text/html" });
55+
const formData = new FormData();
56+
57+
formData.append("feedback", userFeedback);
58+
formData.append("transcript", blob, "transcript.html");
59+
60+
await fetch("/api/feedback", {
61+
method: "POST",
62+
body: formData,
63+
});
64+
}

0 commit comments

Comments
 (0)