Skip to content

Commit

Permalink
FE + BE for collecting user feedback
Browse files Browse the repository at this point in the history
1. Created FE component for user feedback
2. Created helper func for FE  component
3. Created Endpoint, Model, Service, Router for BE  portion
  • Loading branch information
kengboonang committed Oct 23, 2024
1 parent f637586 commit 8298a50
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 99 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ frontend/.env
frontend/package-lock.json
backend/package-lock.json
venv
.venv
.env
android
.idea
Expand Down
2 changes: 2 additions & 0 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import resultRouter from "./routes/resultRouter";
import sectionRouter from "./routes/sectionRouter";
import unitRouter from "./routes/unitRouter";
import accountsGamificationRouter from "./routes/accountsGamificationRouter";
import feedbackRouter from "./routes/feedbackRouter";

const app = express();
const port = 3000;
Expand All @@ -40,6 +41,7 @@ app.use("/lesson", lessonRouter);
app.use("/section", sectionRouter);
app.use("/clickstream", clickstreamRouter);
app.use("/accounts", accountsGamificationRouter);
app.use("/feedback", feedbackRouter);

// RabbitMQ Producer: Sends "timeTaken" data to RabbitMQ
app.post("/rabbitmq", (req, res) => {
Expand Down
8 changes: 4 additions & 4 deletions backend/src/chatbot/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
# dealing with relative / absolute imports
if __package__ is None or __package__ == '' or __name__ == '__main__':
from chatgpt import ChatGPT
from langchain_setup import full_chain, full_chain_w_history
# from langchain_setup import full_chain, full_chain_w_history
else:
from src.chatbot.chatgpt import ChatGPT
from src.chatbot.langchain_setup import full_chain, full_chain_w_history
# from src.chatbot.langchain_setup import full_chain, full_chain_w_history

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -55,8 +55,8 @@ async def generate_text(prompt: Prompt):
logger.error("Error in '/generate' endpoint: %s", str(e))
raise HTTPException(status_code=500, detail=str(e))

@app.post("/langchain")
async def langchain_text(prompt: Prompt):
# @app.post("/langchain")
# async def langchain_text(prompt: Prompt):
"""
Generate a response from Agent-integrated chain based on the role and prompt.
"""
Expand Down
7 changes: 4 additions & 3 deletions backend/src/chatbot/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# chromadb
fastapi
# langchain
# langchain-chroma
langchain-core
langchain-community
# langchain-core
# langchain-community
# langchain-huggingface
langchain-openai
# langchain-openai
# langchain-text-splitters
# llama-index
# llama-index-embeddings-huggingface
Expand Down
17 changes: 17 additions & 0 deletions backend/src/controllers/feedbackController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Request, Response } from "express";
import * as feedbackService from "../services/feedbackService";
import handleError from "../errors/errorHandling";

export const sendFeedback = async (req: Request, res: Response) => {
const messageBody = req.body;

try {
const message = await feedbackService.sendMessage(messageBody);
res.status(200).json({ message: 'Published message successfully' });
} catch (error: any) {
const errorResponse = handleError(error);
if (errorResponse) {
res.status(errorResponse.status).json(errorResponse);
}
}
}
7 changes: 7 additions & 0 deletions backend/src/models/feedbackModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Feedback {
userID: string;
timestamp: Date;
eventType: "feedback" | "bug" | "sugestion";
rating: number;
message: string;
}
9 changes: 9 additions & 0 deletions backend/src/rabbitmq/rabbitmq_logs.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,14 @@
},
{
"timeTaken": 1718
},
{
"feedback": 1618
},
{
"bug": 1518
},
{
"suggestion": 1418
}
]
9 changes: 9 additions & 0 deletions backend/src/routes/feedbackRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as feedbackController from '../controllers/feedbackController';
import { Router } from 'express';
import verifyToken from '../middleware/authMiddleware';

const router = Router();

router.post('/sendFeedback', verifyToken, feedbackController.sendFeedback);

export default router;
133 changes: 44 additions & 89 deletions backend/src/services/feedbackService.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import amqp from "amqplib";
import bodyParser from "body-parser";
import express from "express";
import { Feedback } from "../models/feedbackModel";
import { s3 } from "../config/awsConfig"; // Assuming you already have an AWS config for S3
import { v4 as uuidv4 } from "uuid"; // For unique file naming

async function uploadToS3(queue: string, newClickstream: Clickstream) {
const key = `${queue}/${newClickstream.userID}.json`;
// Create upload function
async function uploadToS3(queue: string, newFeedback: Feedback) {
const key = `${queue}/${newFeedback.userID}.json`;
const params = {
Bucket: "isb-raw-data-athena",
Key: key,
};
let existingClickstream: any[] = [];
let existingFeedback: any[] = [];

try {
const existingData = await s3.getObject(params).promise();
let fileContent = existingData.Body!.toString("utf-8");
existingClickstream = fileContent
existingFeedback = fileContent
.split("\n")
.filter((line: string) => line.trim().length > 0)
.map((line: string) => JSON.parse(line));
Expand All @@ -27,8 +26,8 @@ async function uploadToS3(queue: string, newClickstream: Clickstream) {
}
}

existingClickstream.push(newClickstream);
const lineDelimitedJson = existingClickstream
existingFeedback.push(newFeedback);
const lineDelimitedJson = existingFeedback
.map((item) => JSON.stringify(item))
.join("\n");
s3.putObject({
Expand All @@ -39,85 +38,41 @@ async function uploadToS3(queue: string, newClickstream: Clickstream) {
}

// Define your queues (you can add more if necessary)
const FEEDBACK_QUEUE = "feedback";

// Initialize Express app
const app = express();

// Middleware to parse JSON request bodies
app.use(bodyParser.json());

// Helper function to send feedback to RabbitMQ
async function sendFeedbackToQueue(feedback: any) {
const conn = await amqp.connect(process.env.RABBITMQ_URL!);
const channel = await conn.createChannel();
await channel.assertQueue(FEEDBACK_QUEUE);
channel.sendToQueue(FEEDBACK_QUEUE, Buffer.from(JSON.stringify(feedback)));
const QUEUE_NAMES = ["feedback", "bug", "suggestion"];

// Create a function to consume messages
async function consumeMessage() {
try {
const conn = await amqp.connect(process.env.RABBITMQ_URL!);
const channel = await conn.createChannel();

for (const queue of QUEUE_NAMES) {
await channel.assertQueue(queue);
channel.consume(queue, async (message) => {
if (message !== null) {
const data = message.content.toString();
let parsedData: Feedback = JSON.parse(data);
try {
await uploadToS3(queue, parsedData);
channel.ack(message);
console.log(message)
} catch (error) {
console.error(`Error processing message from ${QUEUE_NAMES[0]}: `, error);
channel.nack(message);
}
}
})
}
} catch (err) {
console.error(`Error: ${err}`);
}
}

// POST endpoint to receive feedback
app.post("/submit-feedback", async (req, res) => {
const { name, feedbackType, message, rating } = req.body;

if (!name || !feedbackType || !message || !rating) {
return res.status(400).json({ error: "Missing required fields" });
}

// Create feedback object
const feedback = {
id: uuidv4(), // Generate a unique ID for the feedback
name,
feedbackType,
message,
rating,
timestamp: new Date().toISOString(),
};

try {
// Send feedback to RabbitMQ queue
await sendFeedbackToQueue(feedback);
res.status(200).json({ message: "Feedback submitted successfully!" });
} catch (error) {
console.error("Error submitting feedback:", error);
res.status(500).json({ error: "Failed to submit feedback" });
}
});

// RabbitMQ consumer to handle feedback from the queue
async function consumeFeedbackMessages() {
try {
const conn = await amqp.connect(process.env.RABBITMQ_URL!);
const channel = await conn.createChannel();

// Assert feedback queue
await channel.assertQueue(FEEDBACK_QUEUE);

// Consume messages from the feedback queue
channel.consume(FEEDBACK_QUEUE, async (message) => {
if (message !== null) {
const data = message.content.toString();
const parsedData = JSON.parse(data);

try {
// Upload feedback to S3 (reusing uploadToS3 function)
await uploadToS3(FEEDBACK_QUEUE, parsedData);
channel.ack(message);
} catch (error) {
console.error(`Error uploading feedback to S3:`, error);
channel.nack(message); // Nack if something goes wrong
}
}
});
} catch (err) {
console.error("Error:", err);
}
}

// Start consuming messages
consumeFeedbackMessages();

// Start the Express server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
export async function sendMessage(feedback: Feedback) {
const queue = feedback.eventType;
const conn = await amqp.connect(process.env.RABBITMQ_URL!);
const channel = await conn.createChannel();
await channel.assertQueue(queue);
channel.sendToQueue(queue, Buffer.from(JSON.stringify(feedback)));
await consumeMessage();
}
3 changes: 2 additions & 1 deletion frontend/iQMA-Skills-Builder/app/screens/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import SectionCard from '@/components/SectionCard';
import TopStats from '@/components/TopStats';
import {router} from 'expo-router';
import {useContext} from 'react';
import { packageFeedback } from '@/helpers/feedbackEndpoints';

const HomeScreen: React.FC = () => {
const {currentUser, isLoading} = useContext(AuthContext);
Expand Down Expand Up @@ -461,7 +462,7 @@ const HomeScreen: React.FC = () => {
<Ionicons name="arrow-up" size={24} color="#7654F2" />
</TouchableOpacity>
)}
<FeedbackComponent />
<FeedbackComponent userID={currentUser.sub}/>
</SafeAreaView>
);
};
Expand Down
42 changes: 40 additions & 2 deletions frontend/iQMA-Skills-Builder/components/Feedback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,45 @@ import React, {useState} from 'react';
import {Colors} from '@/constants/Colors';
import {Picker} from '@react-native-picker/picker';

import { packageFeedback, sendFeedback } from '@/helpers/feedbackEndpoints';

// Define types for the component
type FeedbackComponentProps = {};
type FeedbackComponentProps = {
userID: string;
};

// onpress function
const handleFeedbackSubmit = async (
userID: string,
eventType: string,
selectedRating: number | null,
userFeedback: string
) => {
try {
// Step 1: Call the packageFeedback function to package the data
const feedbackData = await packageFeedback(userID, eventType, selectedRating, userFeedback);

// Step 2: Send the packaged feedback using the sendFeedback function
const status = await sendFeedback(feedbackData);

// Step 3: Check response status
if (status === 200) {
console.log('Feedback sent successfully!');
} else {
console.log('Failed to send feedback.');
}
} catch (error) {
console.error('Error while submitting feedback:', error);
}
};


const FeedbackComponent: React.FC<FeedbackComponentProps> = () => {
const FeedbackComponent: React.FC<FeedbackComponentProps> = ({ userID }) => {
const [visible, setVisible] = useState<boolean>(false); // To toggle form visibility
const [selectedOption, setSelectedOption] = useState<string>(''); // Dropdown state
const [selectedRating, setSelectedRating] = useState<number | null>(null); // Rating state
const [message, setMessage] = useState<string>(''); // Message based on dropdown
const [userFeedback, setUserFeedback] = useState<string>(''); // Feedback state

// Messages corresponding to dropdown options
const customMessages: {[key: string]: string} = {
Expand Down Expand Up @@ -105,7 +136,14 @@ const FeedbackComponent: React.FC<FeedbackComponentProps> = () => {
style={styles.textInput}
placeholder="We value your inputs! Please share your feedback here."
multiline
onChangeText={(text) => setUserFeedback(text)}
/>
{/* Submit Button */}
<TouchableOpacity
style={styles.closeButton}
onPress={async () => {await handleFeedbackSubmit(userID, selectedOption, selectedRating, userFeedback); setVisible(false);}}>
<Text style={styles.closeButtonText}>Submit</Text>
</TouchableOpacity>

{/* Close Form Button */}
<TouchableOpacity
Expand Down
Loading

0 comments on commit 8298a50

Please sign in to comment.