diff --git a/apps/mongo-feed/.gitignore b/apps/mongo-feed/.gitignore new file mode 100644 index 0000000..7920021 --- /dev/null +++ b/apps/mongo-feed/.gitignore @@ -0,0 +1,5 @@ +node_modules +.env +.env.local +package-lock.json +.next diff --git a/apps/mongo-feed/README.md b/apps/mongo-feed/README.md new file mode 100644 index 0000000..95dd3d6 --- /dev/null +++ b/apps/mongo-feed/README.md @@ -0,0 +1,117 @@ +# MongoFeed + +MongoFeed is a comprehensive platform for product feedback analysis and sentiment tracking. It leverages MongoDB for data storage and Amazon Bedrock for AI-powered sentiment analysis, providing valuable insights into customer feedback and product reviews. + +> ♥️ Inpired by a customer success story : [Syncly](https://www.mongodb.com/customers/syncly) + +## Hosted Version + +https://mongo-feed.vercel.app + +## Features + +- File upload for product feedback analysis (JSON, HTML, images) +- Chat paste functionality for direct input of customer interactions +- Sentiment analysis using Amazon Bedrock AI +- Real-time processing queue for feedback analysis +- Interactive charts and visualizations: + - Feedback trends over time + - Sentiment distribution + - Top issues identification +- Agent performance tracking and sentiment analysis + +## Prerequisites + +Before you begin, ensure you have the following installed: +- Node.js (v14 or later) +- npm (v6 or later) +- MongoDB (6.0+) +- An AWS account with access to Amazon Bedrock and Claude 3.5 V2 model + +## Installation + +1. **Clone the repository:** + ```bash + git clone + cd mongo-feed + ``` + +2. **Install dependencies:** + ```bash + npm install + ``` + +3. **Configure environment variables:** + - Create a `.env.local` file in the root directory. + - Add your MongoDB connection string and AWS Bedrock credentials. + ```env + MONGODB_URI=your_mongodb_connection_string + AWS_REGION=your_aws_region + AWS_ACCESS_KEY_ID=your_aws_access_key_id + AWS_SECRET_ACCESS_KEY=your_aws_secret_access_key + ``` + **Note:** Ensure you have the necessary permissions for Amazon Bedrock and MongoDB. + +## Development + +1. **Run the development server:** + ```bash + npm run dev + ``` + Open [http://localhost:3000](http://localhost:3000) in your browser to view the application. + +## Building for Production + +1. **Build the application:** + ```bash + npm run build + ``` + +2. **Start the production server:** + ```bash + npm run start + ``` + +## Usage + +To use MongoFeed: + +1. **Access the application** in your browser at [http://localhost:3000](http://localhost:3000) after running the development or production server. +2. **Upload Feedback Files or Paste Chat Interactions:** + - Navigate to the feedback input section. + - Choose to upload files (JSON, HTML, images) or paste text from chat interactions. + - Follow the on-screen instructions to input your feedback data. +3. **View Sentiment Analysis Results and Visualizations:** + - Once the feedback is processed, navigate to the dashboard. + - Explore interactive charts and visualizations to understand: + - Feedback trends over time + - Sentiment distribution across feedback + - Top issues identified from the feedback +4. **Navigate the Dashboard:** + - Use the dashboard to access different features, such as: + - Real-time processing queue monitoring. + - Agent performance tracking and sentiment analysis (if applicable). + - Detailed views of individual feedback entries and their sentiment analysis. + +## Configuration + +- **Environment Variables:** + - `MONGODB_URI`: MongoDB connection string for your MongoDB database. + - `AWS_REGION`: AWS region where your Bedrock service is configured. + - `AWS_ACCESS_KEY_ID`: AWS access key ID for authentication. + - `AWS_SECRET_ACCESS_KEY`: AWS secret access key for authentication. + +- **Other configurations:** + - The application may have additional configurations that can be set in the `.env.local` file or through the application's settings panel. Refer to the application documentation for advanced configuration options. + +## Contributing + +If you'd like to contribute to MongoFeed, please follow these guidelines: +1. Fork the repository. +2. Create a branch for your feature or bug fix. +3. Ensure your code adheres to the project's coding standards. +4. Submit a pull request with a clear description of your changes. + +## License + +[Specify the project license, e.g., MIT License] diff --git a/apps/mongo-feed/app/api/agent-analysis/route.ts b/apps/mongo-feed/app/api/agent-analysis/route.ts new file mode 100644 index 0000000..a2108b0 --- /dev/null +++ b/apps/mongo-feed/app/api/agent-analysis/route.ts @@ -0,0 +1,45 @@ +import { NextResponse } from "next/server" +import clientPromise from "@/lib/mongodb" + +export const dynamic = "force-dynamic" +export const revalidate = 0 + +export async function GET() { + try { + const client = await clientPromise + const db = client.db("mongofeed") + + const agentAnalysis = await db + .collection("chat_analyses") + .aggregate([ + { $unwind: "$messages" }, + { $match: { "messages.role": "Agent" } }, + { + $group: { + _id: "$messages.agentName", + positiveSentiment: { $sum: { $cond: [{ $eq: ["$messages.sentiment", "positive"] }, 1, 0] } }, + neutralSentiment: { $sum: { $cond: [{ $eq: ["$messages.sentiment", "neutral"] }, 1, 0] } }, + negativeSentiment: { $sum: { $cond: [{ $eq: ["$messages.sentiment", "negative"] }, 1, 0] } }, + totalInteractions: { $sum: 1 }, + }, + }, + { + $project: { + agentName: "$_id", + positiveSentiment: 1, + neutralSentiment: 1, + negativeSentiment: 1, + totalInteractions: 1, + _id: 0, + }, + }, + { $sort: { totalInteractions: -1 } }, + ]) + .toArray() + + return NextResponse.json(agentAnalysis) + } catch (error) { + console.error("Error fetching agent analysis:", error) + return NextResponse.json({ error: "An error occurred while fetching agent analysis." }, { status: 500 }) + } +} diff --git a/apps/mongo-feed/app/api/agent-sentiment/route.ts b/apps/mongo-feed/app/api/agent-sentiment/route.ts new file mode 100644 index 0000000..33b6b92 --- /dev/null +++ b/apps/mongo-feed/app/api/agent-sentiment/route.ts @@ -0,0 +1,50 @@ +import { NextResponse } from "next/server" +import clientPromise from "@/lib/mongodb" + +export const dynamic = "force-dynamic" +export const revalidate = 0 + +export async function GET() { + try { + const client = await clientPromise + const db = client.db("mongofeed") + + // First get all agents with their sentiment data + const agentData = await db + .collection("agent_sentiment") + .aggregate([ + { + $lookup: { + from: "chat_analyses", + let: { agentName: "$agentName" }, + pipeline: [ + { + $match: { + $expr: { $eq: ["$analysis.agentName", "$$agentName"] }, + }, + }, + { $sort: { createdAt: -1 } }, + { $limit: 5 }, + { + $project: { + id: { $toString: "$_id" }, + date: "$createdAt", + summary: "$analysis.summary", + sentiment: "$analysis.overallSentiment", + issues: "$analysis.mainTopics", + }, + }, + ], + as: "recentChats", + }, + }, + { $sort: { totalInteractions: -1 } }, + ]) + .toArray() + + return NextResponse.json(agentData) + } catch (error) { + console.error("Error fetching agent sentiment:", error) + return NextResponse.json({ error: "An error occurred while fetching agent sentiment." }, { status: 500 }) + } +} diff --git a/apps/mongo-feed/app/api/analyze-feedback/route.ts b/apps/mongo-feed/app/api/analyze-feedback/route.ts new file mode 100644 index 0000000..7b5a24c --- /dev/null +++ b/apps/mongo-feed/app/api/analyze-feedback/route.ts @@ -0,0 +1,55 @@ +import { type NextRequest, NextResponse } from "next/server" +import { analyzeAgentFeedback, analyzeProductReview } from "@/lib/analyze-content" +import clientPromise from "@/lib/mongodb" + +export const dynamic = "force-dynamic" +export const runtime = "nodejs" + +export async function POST(req: NextRequest) { + try { + const formData = await req.formData() + const file = formData.get("file") as File + const type = formData.get("type") as string + + if (!file) { + return NextResponse.json({ error: "No file uploaded" }, { status: 400 }) + } + + const contentType = file.type + const fileName = file.name + + let content: string | ArrayBuffer + if (contentType.startsWith("image/")) { + content = await file.arrayBuffer() + } else { + const buffer = await file.arrayBuffer() + content = new TextDecoder().decode(buffer) + } + + let analysisResult + + if (type === "agent") { + analysisResult = await analyzeAgentFeedback(content as string) + } else if (type === "product") { + analysisResult = await analyzeProductReview(content, contentType, fileName) + } else { + return NextResponse.json({ error: "Invalid analysis type" }, { status: 400 }) + } + + // Store the analysis result in MongoDB + const client = await clientPromise + const db = client.db("mongofeed") + await db.collection("chat_analyses").insertOne({ + type, + contentType, + fileName, + analysis: analysisResult, + createdAt: new Date(), + }) + + return NextResponse.json({ sentiments: analysisResult }) + } catch (error) { + console.error("Error analyzing feedback:", error) + return NextResponse.json({ error: "An error occurred while analyzing feedback." }, { status: 500 }) + } +} diff --git a/apps/mongo-feed/app/api/analyze-sentiment/route.ts b/apps/mongo-feed/app/api/analyze-sentiment/route.ts new file mode 100644 index 0000000..1eaa3c6 --- /dev/null +++ b/apps/mongo-feed/app/api/analyze-sentiment/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { bedrock } from '@/lib/bedrock'; +import { generateText } from 'ai'; + +export async function POST(req: NextRequest) { + try { + const data = await req.json(); + const documents = data.documents; + + if (!Array.isArray(documents)) { + return NextResponse.json({ error: 'Invalid input. Expected an array of documents.' }, { status: 400 }); + } + + const sentiments = await Promise.all( + documents.map(async (doc) => { + try { + const { text } = await generateText({ + model: bedrock('aanthropic.claude-3-5-sonnet-20241022-v2:0'), + prompt: `Analyze the sentiment of the following text and respond with only one word: "positive", "negative", or "neutral". Text: "${doc}"`, + }); + return text.trim().toLowerCase(); + } catch (error) { + console.error('Error analyzing individual document:', error); + return 'error'; + } + }) + ); + + return NextResponse.json({ sentiments }); + } catch (error) { + console.error('Error analyzing sentiment:', error); + return NextResponse.json({ error: 'An error occurred while analyzing sentiment.' }, { status: 500 }); + } +} diff --git a/apps/mongo-feed/app/api/feedback/overview/route.ts b/apps/mongo-feed/app/api/feedback/overview/route.ts new file mode 100644 index 0000000..5bbe15f --- /dev/null +++ b/apps/mongo-feed/app/api/feedback/overview/route.ts @@ -0,0 +1,90 @@ +import { NextResponse } from "next/server" +import clientPromise from "@/lib/mongodb" + +export const dynamic = "force-dynamic" +export const revalidate = 0 + +export async function GET() { + try { + const client = await clientPromise + const db = client.db("mongofeed") + + // Get total feedback count + const totalFeedback = await db.collection("chat_analyses").countDocuments() + + // Calculate sentiment score + const sentimentAggregation = await db + .collection("chat_analyses") + .aggregate([ + { + $unwind: "$analysis", + }, + { + $group: { + _id: null, + positive: { + $sum: { $cond: [{ $eq: ["$analysis.sentiment", "positive"] }, 1, 0] }, + }, + total: { $sum: 1 }, + }, + }, + ]) + .toArray() + + const sentimentScore = + sentimentAggregation.length > 0 + ? Math.round((sentimentAggregation[0].positive / sentimentAggregation[0].total) * 100) + : 0 + + // Get trend data + const trendData = await db + .collection("chat_analyses") + .aggregate([ + { + $unwind: "$analysis", + }, + { + $group: { + _id: { + date: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } }, + sentiment: "$analysis.sentiment", + }, + count: { $sum: 1 }, + }, + }, + { + $group: { + _id: "$_id.date", + positive: { + $sum: { $cond: [{ $eq: ["$_id.sentiment", "positive"] }, "$count", 0] }, + }, + negative: { + $sum: { $cond: [{ $eq: ["$_id.sentiment", "negative"] }, "$count", 0] }, + }, + neutral: { + $sum: { $cond: [{ $eq: ["$_id.sentiment", "neutral"] }, "$count", 0] }, + }, + }, + }, + { $sort: { _id: 1 } }, + { $limit: 30 }, + ]) + .toArray() + + const formattedTrendData = trendData.map((item) => ({ + date: item._id, + positive: item.positive, + negative: item.negative, + neutral: item.neutral, + })) + + return NextResponse.json({ + totalFeedback, + sentimentScore, + trendData: formattedTrendData, + }) + } catch (error) { + console.error("Error fetching feedback overview:", error) + return NextResponse.json({ error: "Failed to fetch feedback overview" }, { status: 500 }) + } +} diff --git a/apps/mongo-feed/app/api/feedback/recent/route.ts b/apps/mongo-feed/app/api/feedback/recent/route.ts new file mode 100644 index 0000000..90512fd --- /dev/null +++ b/apps/mongo-feed/app/api/feedback/recent/route.ts @@ -0,0 +1,53 @@ +import { NextResponse } from "next/server" +import clientPromise from "@/lib/mongodb" + +export const dynamic = "force-dynamic" +export const revalidate = 0 + +function getTimeAgo(date: Date): string { + const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000) + + if (seconds < 60) return `${seconds} sec ago` + const minutes = Math.floor(seconds / 60) + if (minutes < 60) return `${minutes} min ago` + const hours = Math.floor(minutes / 60) + if (hours < 24) return `${hours} hr ago` + const days = Math.floor(hours / 24) + return `${days} days ago` +} + +export async function GET() { + try { + const client = await clientPromise + const db = client.db("mongofeed") + + const recentFeedback = await db.collection("chat_analyses").find({}).sort({ createdAt: -1 }).limit(5).toArray() + + const formattedFeedback = recentFeedback.map((feedback) => { + const analysis = feedback.analysis || [] + const mainTopic = Array.isArray(analysis) && analysis.length > 0 ? analysis[0].name : "General Feedback" + + // Calculate sentiment score based on analysis array + const sentimentScore = Array.isArray(analysis) + ? analysis.reduce((score, item) => { + if (item.sentiment === "positive") return score + 5 + if (item.sentiment === "negative") return score + 1 + return score + 3 + }, 0) / analysis.length + : 3.0 + + return { + id: feedback._id.toString(), + score: Number.parseFloat(sentimentScore.toFixed(1)), + title: mainTopic, + type: feedback.type || "agent", + timeAgo: getTimeAgo(new Date(feedback.createdAt)), + } + }) + + return NextResponse.json(formattedFeedback) + } catch (error) { + console.error("Error fetching recent feedback:", error) + return NextResponse.json({ error: "Failed to fetch recent feedback" }, { status: 500 }) + } +} diff --git a/apps/mongo-feed/app/api/feedback/sentiment-distribution/route.ts b/apps/mongo-feed/app/api/feedback/sentiment-distribution/route.ts new file mode 100644 index 0000000..60e6529 --- /dev/null +++ b/apps/mongo-feed/app/api/feedback/sentiment-distribution/route.ts @@ -0,0 +1,40 @@ +import { NextResponse } from "next/server" +import clientPromise from "@/lib/mongodb" + +export const dynamic = "force-dynamic" +export const revalidate = 0 + +export async function GET() { + try { + const client = await clientPromise + const db = client.db("mongofeed") + + const distribution = await db + .collection("chat_analyses") + .aggregate([ + { + $group: { + _id: { + type: { $ifNull: ["$type", "agent"] }, + sentiment: "$analysis.overallSentiment", + }, + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + name: "$_id.sentiment", + type: "$_id.type", + value: "$count", + }, + }, + ]) + .toArray() + + return NextResponse.json(distribution) + } catch (error) { + console.error("Error fetching sentiment distribution:", error) + return NextResponse.json({ error: "An error occurred while fetching sentiment distribution." }, { status: 500 }) + } +} diff --git a/apps/mongo-feed/app/api/feedback/top-issues/route.ts b/apps/mongo-feed/app/api/feedback/top-issues/route.ts new file mode 100644 index 0000000..eb6c523 --- /dev/null +++ b/apps/mongo-feed/app/api/feedback/top-issues/route.ts @@ -0,0 +1,59 @@ +import { NextResponse } from "next/server" +import clientPromise from "@/lib/mongodb" + +export const dynamic = "force-dynamic" +export const revalidate = 0 + +export async function GET() { + try { + const client = await clientPromise + const db = client.db("mongofeed") + + const topIssues = await db + .collection("chat_analyses") + .aggregate([ + { + $match: { + "analysis.overallSentiment": "negative", + }, + }, + { + $unwind: "$analysis.mainTopics", + }, + { + $group: { + _id: { + topic: "$analysis.mainTopics", + type: { $ifNull: ["$type", "agent"] }, + }, + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + name: "$_id.topic", + type: "$_id.type", + count: 1, + sentiment: { $literal: "negative" }, + }, + }, + { + $sort: { count: -1 }, + }, + { + $limit: 10, + }, + ]) + .toArray() + + if (!topIssues.length) { + return NextResponse.json([]) + } + + return NextResponse.json(topIssues) + } catch (error) { + console.error("Error fetching top issues:", error) + return NextResponse.json({ error: "An error occurred while fetching top issues." }, { status: 500 }) + } +} diff --git a/apps/mongo-feed/app/api/feedback/trend/route.ts b/apps/mongo-feed/app/api/feedback/trend/route.ts new file mode 100644 index 0000000..9c76857 --- /dev/null +++ b/apps/mongo-feed/app/api/feedback/trend/route.ts @@ -0,0 +1,71 @@ +import { NextResponse } from "next/server" +import clientPromise from "@/lib/mongodb" + +export const dynamic = "force-dynamic" +export const revalidate = 0 + +export async function GET() { + try { + const client = await clientPromise + const db = client.db("mongofeed") + + const trendData = await db + .collection("chat_analyses") + .aggregate([ + { + $unwind: "$analysis", + }, + { + $group: { + _id: { + date: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } }, + type: { $ifNull: ["$type", "agent"] }, + sentiment: "$analysis.sentiment", + }, + count: { $sum: 1 }, + }, + }, + { + $group: { + _id: { + date: "$_id.date", + type: "$_id.type", + }, + positive: { + $sum: { + $cond: [{ $eq: ["$_id.sentiment", "positive"] }, "$count", 0], + }, + }, + negative: { + $sum: { + $cond: [{ $eq: ["$_id.sentiment", "negative"] }, "$count", 0], + }, + }, + neutral: { + $sum: { + $cond: [{ $eq: ["$_id.sentiment", "neutral"] }, "$count", 0], + }, + }, + }, + }, + { + $sort: { "_id.date": 1 }, + }, + ]) + .toArray() + + // Transform the data for the chart + const formattedData = trendData.map((item) => ({ + date: item._id.date, + type: item._id.type, + positive: item.positive, + negative: item.negative, + neutral: item.neutral, + })) + + return NextResponse.json(formattedData) + } catch (error) { + console.error("Error fetching feedback trend:", error) + return NextResponse.json({ error: "An error occurred while fetching feedback trend." }, { status: 500 }) + } +} diff --git a/apps/mongo-feed/app/api/past-analysis/route.ts b/apps/mongo-feed/app/api/past-analysis/route.ts new file mode 100644 index 0000000..dc86ce4 --- /dev/null +++ b/apps/mongo-feed/app/api/past-analysis/route.ts @@ -0,0 +1,44 @@ +import { NextResponse } from "next/server" +import clientPromise from "@/lib/mongodb" + +export const dynamic = "force-dynamic" +export const revalidate = 0 + +export async function GET() { + try { + const client = await clientPromise + const db = client.db("mongofeed") + + const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000) + + const pastAnalysis = await db + .collection("chat_queue") + .find({ + status: "completed", + updatedAt: { $lt: fiveMinutesAgo }, + }) + .sort({ updatedAt: -1 }) + .limit(10) + .toArray() + + const formattedAnalysis = pastAnalysis.map((item) => ({ + id: item._id.toString(), + name: `Analysis ${item._id.toString().slice(-6)}`, + date: item.updatedAt.toISOString(), + duration: calculateDuration(item.createdAt, item.updatedAt), + status: item.status, + })) + + return NextResponse.json(formattedAnalysis) + } catch (error) { + console.error("Error fetching past analysis:", error) + return NextResponse.json({ error: "An error occurred while fetching past analysis." }, { status: 500 }) + } +} + +function calculateDuration(start: Date, end: Date): string { + const durationMs = end.getTime() - start.getTime() + const minutes = Math.floor(durationMs / 60000) + const seconds = Math.floor((durationMs % 60000) / 1000) + return `${minutes}m ${seconds}s` +} diff --git a/apps/mongo-feed/app/api/process-chat/route.ts b/apps/mongo-feed/app/api/process-chat/route.ts new file mode 100644 index 0000000..e2b2b36 --- /dev/null +++ b/apps/mongo-feed/app/api/process-chat/route.ts @@ -0,0 +1,143 @@ +import { type NextRequest, NextResponse } from "next/server" +import { generateObject } from "ai" +import { bedrock } from "@/lib/bedrock" +import clientPromise from "@/lib/mongodb" +import { z } from "zod" +import type { Db, ObjectId } from "mongodb" + +interface Message { + role: "Customer" | "Agent" + content: string +} + +const chatAnalysisSchema = z.object({ + summary: z.string(), + overallSentiment: z.enum(["positive", "negative", "neutral"]), + mainTopics: z.array(z.string()).max(5), + messages: z.array( + z.object({ + role: z.enum(["Customer", "Agent"]), + content: z.string(), + sentiment: z.enum(["positive", "negative", "neutral"]), + agentName: z.string().optional(), + }), + ), +}) + +const systemPrompt = `You are an AI assistant that analyzes customer service chat conversations for MongoFeed, a feedback analysis platform. +Your task is to analyze the provided chat messages and return a structured analysis including: +- summary: A brief summary of the conversation (max 3 sentences) +- overallSentiment: The overall sentiment of the conversation (positive, negative, or neutral) +- mainTopics: An array of main topics discussed (max 5 topics) +- messages: An array of analyzed messages, each containing: + - role: The role of the speaker (Customer or Agent) + - content: The content of the message + - sentiment: The sentiment of the individual message (positive, negative, or neutral) + - agentName: Try to identify the agent's name from the conversation. If found, include it here. + +Focus on identifying key issues, feature requests, or areas of improvement for MongoFeed.` + +async function updateProcessingLog(db: Db, chatId: ObjectId, message: string) { + await db.collection("processing_logs").updateOne( + { chatId }, + { + $push: { + logs: { timestamp: new Date(), message }, + }, + }, + ) +} + +export async function POST(req: NextRequest) { + try { + const { messages } = await req.json() + + if (!Array.isArray(messages) || messages.length === 0) { + return NextResponse.json({ error: "Invalid input. Expected an array of messages." }, { status: 400 }) + } + + const client = await clientPromise + const db = client.db("mongofeed") + + const chatQueue = await db + .collection("chat_queue") + .findOneAndUpdate( + { status: "pending" }, + { $set: { status: "processing" } }, + { sort: { createdAt: 1 }, returnDocument: "after" }, + ) + + if (!chatQueue) { + return NextResponse.json({ error: "No pending chats in the queue." }, { status: 404 }) + } + + const chatId = chatQueue._id + + await updateProcessingLog(db, chatId, "Starting chat analysis") + + await updateProcessingLog(db, chatId, "Chat retrieved from queue and processing started") + + const conversationText = messages.map((msg: Message) => `${msg.role}: ${msg.content}`).join("\n\n") + + await updateProcessingLog(db, chatId, "Generating analysis using Bedrock LLM") + + const result = await generateObject({ + model: bedrock("anthropic.claude-3-5-sonnet-20241022-v2:0"), + prompt: conversationText, + system: systemPrompt, + schema: chatAnalysisSchema, + }) + + console.log('chat analysis result'); + + await updateProcessingLog(db, chatId, `Analysis generated: ${result.summary}`) + + await updateProcessingLog(db, chatId, "Analysis generated successfully") + + await db.collection("chat_analyses").insertOne({ + chatId, + messages: result.messages, + analysis: { + summary: result.summary, + overallSentiment: result.overallSentiment, + mainTopics: result.mainTopics, + }, + createdAt: new Date(), + }) + + await updateProcessingLog(db, chatId, "Chat analysis stored in database") + + // Update agent sentiment data + const agentMessages = result.messages.filter((msg) => msg.role === "Agent" && msg.agentName) + for (const msg of agentMessages) { + await db.collection("agent_sentiment").updateOne( + { agentName: msg.agentName }, + { + $inc: { + [`sentiment.${msg.sentiment}`]: 1, + totalInteractions: 1, + }, + }, + { upsert: true }, + ) + } + + await updateProcessingLog(db, chatId, "Agent sentiment data updated") + + await updateProcessingLog( + db, + chatId, + `Processing completed. Overall sentiment: ${result.overallSentiment}. Main topics: ${result.mainTopics.join(", ")}`, + ) + + await db.collection("chat_queue").updateOne({ _id: chatId }, { $set: { status: "completed" } }) + + return NextResponse.json({ success: true }) + } catch (error) { + console.error("Error processing chat:", error) + if (chatId) { + await updateProcessingLog(db, chatId, `Error during processing: ${error.message}`) + } + return NextResponse.json({ error: "An error occurred while processing the chat." }, { status: 500 }) + } +} diff --git a/apps/mongo-feed/app/api/process-queue/route.ts b/apps/mongo-feed/app/api/process-queue/route.ts new file mode 100644 index 0000000..fac8b80 --- /dev/null +++ b/apps/mongo-feed/app/api/process-queue/route.ts @@ -0,0 +1,58 @@ +import { NextResponse } from "next/server" +import clientPromise from "@/lib/mongodb" + +export const dynamic = "force-dynamic" +export const revalidate = 0 + +export async function GET() { + try { + const client = await clientPromise + const db = client.db("mongofeed") + + const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000) + + const queue = await db + .collection("chat_queue") + .find({ + $or: [ + { status: { $in: ["pending", "processing"] } }, + { + status: "completed", + updatedAt: { $gte: fiveMinutesAgo }, + }, + ], + }) + .sort({ createdAt: -1 }) + .limit(10) + .toArray() + + const queueItems = await Promise.all( + queue.map(async (item) => { + const logs = await db.collection("processing_logs").findOne({ chatId: item._id }) + const progress = + item.status === "completed" + ? 100 + : item.status === "processing" + ? 50 + : item.status === "pending" + ? 25 + : item.status === "error" + ? 0 + : 25 + + return { + id: item._id.toString(), + name: `Analysis ${item._id.toString().slice(-6)}`, + progress, + status: item.status, + type: item.type || "agent", // Default to "agent" for backward compatibility + } + }), + ) + + return NextResponse.json(queueItems) + } catch (error) { + console.error("Error fetching process queue:", error) + return NextResponse.json({ error: "An error occurred while fetching the process queue." }, { status: 500 }) + } +} diff --git a/apps/mongo-feed/app/api/processing-logs/[id]/route.ts b/apps/mongo-feed/app/api/processing-logs/[id]/route.ts new file mode 100644 index 0000000..88bc90c --- /dev/null +++ b/apps/mongo-feed/app/api/processing-logs/[id]/route.ts @@ -0,0 +1,26 @@ +import { type NextRequest, NextResponse } from "next/server" +import clientPromise from "@/lib/mongodb" +import { ObjectId } from "mongodb" + +export const dynamic = "force-dynamic" +export const revalidate = 0 + +export async function GET(req: NextRequest, { params }: { params: { id: string } }) { + try { + const client = await clientPromise + const db = client.db("mongofeed") + + const logs = await db + .collection("processing_logs") + .findOne({ chatId: new ObjectId(params.id) }, { projection: { _id: 0, logs: 1 } }) + + if (!logs) { + return NextResponse.json({ error: "Processing logs not found" }, { status: 404 }) + } + + return NextResponse.json(logs) + } catch (error) { + console.error("Error fetching processing logs:", error) + return NextResponse.json({ error: "An error occurred while fetching processing logs." }, { status: 500 }) + } +} diff --git a/apps/mongo-feed/app/api/submit-chat/route.ts b/apps/mongo-feed/app/api/submit-chat/route.ts new file mode 100644 index 0000000..d313668 --- /dev/null +++ b/apps/mongo-feed/app/api/submit-chat/route.ts @@ -0,0 +1,115 @@ +import { type NextRequest, NextResponse } from "next/server" +import clientPromise from "@/lib/mongodb" +import type { ObjectId } from "mongodb" +import { analyzeEntireChat } from "@/lib/chat-processing" + +export async function POST(req: NextRequest) { + try { + const { messages } = await req.json() + + if (!Array.isArray(messages) || messages.length === 0) { + return NextResponse.json({ error: "Invalid input. Expected an array of messages." }, { status: 400 }) + } + + const client = await clientPromise + const db = client.db("mongofeed") + + const result = await db.collection("chat_queue").insertOne({ + messages, + status: "pending", + createdAt: new Date(), + updatedAt: new Date(), + }) + + const chatId = result.insertedId + + // Create initial processing log + await db.collection("processing_logs").insertOne({ + chatId: chatId, + logs: [{ timestamp: new Date(), message: "Chat submitted for processing" }], + status: "pending", + }) + + // Trigger immediate processing + await processChatAnalysis(db, chatId, messages) + + return NextResponse.json({ id: chatId.toString() }) + } catch (error) { + console.error("Error submitting chat:", error) + return NextResponse.json({ error: "An error occurred while submitting the chat." }, { status: 500 }) + } +} + +async function processChatAnalysis(db, chatId: ObjectId, messages) { + try { + // Update chat queue status to processing + await db + .collection("chat_queue") + .updateOne({ _id: chatId }, { $set: { status: "processing", updatedAt: new Date() } }) + + // Update processing log + await db.collection("processing_logs").updateOne( + { chatId }, + { + $push: { + logs: { timestamp: new Date(), message: "Analyzing entire chat" }, + }, + $set: { status: "processing" }, + }, + ) + + const analysis = await analyzeEntireChat(messages) + + // Create chat analysis document + await db.collection("chat_analyses").insertOne({ + chatId, + messages, + analysis, + createdAt: new Date(), + }) + + // Update agent sentiment data if an agent was identified + if (analysis.agentName) { + await db.collection("agent_sentiment").updateOne( + { agentName: analysis.agentName }, + { + $inc: { + [`sentiment.${analysis.overallSentiment}`]: 1, + totalInteractions: 1, + }, + }, + { upsert: true }, + ) + } + + // Update processing status + await db + .collection("chat_queue") + .updateOne({ _id: chatId }, { $set: { status: "completed", updatedAt: new Date() } }) + + await db.collection("processing_logs").updateOne( + { chatId }, + { + $push: { + logs: { + timestamp: new Date(), + message: `Sentiment analysis completed with overall ${analysis.overallSentiment}`, + }, + }, + $set: { status: "completed" }, + }, + ) + } catch (error) { + console.error("Error processing chat analysis:", error) + await db.collection("processing_logs").updateOne( + { chatId }, + { + $push: { + logs: { timestamp: new Date(), message: `Error during analysis: ${error.message}` }, + }, + $set: { status: "error" }, + }, + ) + await db.collection("chat_queue").updateOne({ _id: chatId }, { $set: { status: "error", updatedAt: new Date() } }) + } +} diff --git a/apps/mongo-feed/app/charts/page.tsx b/apps/mongo-feed/app/charts/page.tsx new file mode 100644 index 0000000..5b73ddd --- /dev/null +++ b/apps/mongo-feed/app/charts/page.tsx @@ -0,0 +1,42 @@ +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" +import { FeedbackTrendChart } from "@/components/charts/feedback-trend" +import { SentimentDistributionChart } from "@/components/charts/sentiment-distribution" +import { TopIssuesChart } from "@/components/charts/top-issues" + +export default function ChartsPage() { + return ( +
+ + + Feedback Trend + View feedback volume and sentiment over time + + + + + + +
+ + + Sentiment Distribution + Overall sentiment breakdown + + + + + + + + + Top Issues + Most frequently mentioned issues + + + + + +
+
+ ) +} diff --git a/apps/mongo-feed/app/feedback/loading.tsx b/apps/mongo-feed/app/feedback/loading.tsx new file mode 100644 index 0000000..20326a3 --- /dev/null +++ b/apps/mongo-feed/app/feedback/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingSpinner } from "@/components/ui/loading-spinner" + +export default function Loading() { + return +} diff --git a/apps/mongo-feed/app/feedback/page.tsx b/apps/mongo-feed/app/feedback/page.tsx new file mode 100644 index 0000000..b0eade3 --- /dev/null +++ b/apps/mongo-feed/app/feedback/page.tsx @@ -0,0 +1,30 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { FeedbackOverview } from "@/components/feedback/overview" +import { RecentFeedback } from "@/components/feedback/recent-feedback" +import { TopIssues } from "@/components/feedback/top-issues" + +export default async function FeedbackPage() { + return ( +
+
+ + + + Recent Feedback + + + + + +
+ + + Top Issues + + + + + +
+ ) +} diff --git a/apps/mongo-feed/app/globals.css b/apps/mongo-feed/app/globals.css new file mode 100644 index 0000000..d8e1ceb --- /dev/null +++ b/apps/mongo-feed/app/globals.css @@ -0,0 +1,76 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 142.1 76.2% 36.3%; + --primary-foreground: 355.7 100% 97.3%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 142.1 76.2% 36.3%; + + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 142.1 70.6% 45.3%; + --primary-foreground: 144.9 80.4% 10%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 142.1 76.2% 36.3%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/mongo-feed/app/layout.tsx b/apps/mongo-feed/app/layout.tsx new file mode 100644 index 0000000..46fe180 --- /dev/null +++ b/apps/mongo-feed/app/layout.tsx @@ -0,0 +1,37 @@ +import { Inter } from 'next/font/google' +import { Navigation } from '@/components/navigation' +import { ThemeProvider } from '@/components/theme-provider' +import './globals.css' + +const inter = Inter({ subsets: ['latin'] }) + +export const metadata = { + title: 'MongoFeed', + description: 'Feedback Analysis Platform', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + +
+ +
+ {children} +
+
+
+ + + ) +} diff --git a/apps/mongo-feed/app/page.tsx b/apps/mongo-feed/app/page.tsx new file mode 100644 index 0000000..f6af090 --- /dev/null +++ b/apps/mongo-feed/app/page.tsx @@ -0,0 +1,193 @@ +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Upload, MessageSquare, BarChart2, LineChart, PieChart, ArrowRight, ListChecks } from "lucide-react" +import Link from "next/link" + +export default function HomePage() { + return ( +
+
+

Welcome to MongoFeed

+

+ Your comprehensive platform for product feedback analysis and sentiment tracking +

+
+ + + + Input Sources + Analysis Tools + + +
+ + + + + File Upload + + Upload product feedback data files for analysis + + +

Supported formats:

+
    +
  • JSON data
  • +
  • HTML scrapes
  • +
  • Screenshots
  • +
+ +
+
+ + + + + + Chat Paste + + Paste product review logs directly + + +

Supported content:

+
    +
  • Customer reviews
  • +
  • Product feedback
  • +
  • Social media comments
  • +
  • Survey responses
  • +
+ +
+
+
+
+ + +
+ + + + + Feedback Analysis + + Analyze feedback patterns + + +

+ View comprehensive feedback analysis including sentiment trends, common issues, and customer + satisfaction metrics. +

+ +
+
+ + + + + + Sentiment Trends + + Track product sentiment + + +

+ Monitor product sentiment trends over time, identifying areas for improvement and success stories. +

+ +
+
+ + + + + + Charts & Reports + + Visualize your data + + +

+ Access detailed charts and reports to gain insights into product feedback trends and patterns over + time. +

+ +
+
+ + + + + Process Queue + + Monitor analysis progress + + +

+ View ongoing analysis tasks, track progress, and access past analysis results. +

+ +
+
+
+
+
+ + + + Getting Started + Follow these steps to begin analyzing your product feedback + + +
    +
  1. + Choose your input method: Upload files containing + product feedback data or paste review logs directly into the system. +
  2. +
  3. + Process your data: The system will automatically + analyze your input for sentiment and key patterns. +
  4. +
  5. + Explore insights: Use the analysis tools to view + feedback trends, product performance, and detailed reports. +
  6. +
  7. + Take action: Use the insights to improve product + features, marketing strategies, and overall customer satisfaction. +
  8. +
+
+
+
+ ) +} diff --git a/apps/mongo-feed/app/paste/page.tsx b/apps/mongo-feed/app/paste/page.tsx new file mode 100644 index 0000000..c153640 --- /dev/null +++ b/apps/mongo-feed/app/paste/page.tsx @@ -0,0 +1,18 @@ +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" +import { PasteForm } from "@/components/paste-form" + +export default function PastePage() { + return ( +
+ + + Paste Chat + Paste chat content for analysis + + + + + +
+ ) +} diff --git a/apps/mongo-feed/app/process-queue/page.tsx b/apps/mongo-feed/app/process-queue/page.tsx new file mode 100644 index 0000000..52dddfe --- /dev/null +++ b/apps/mongo-feed/app/process-queue/page.tsx @@ -0,0 +1,44 @@ +import { Suspense } from "react" +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" +import { ProcessingQueue } from "@/components/processing-queue" +import { PastAnalysis } from "@/components/past-analysis" +import { AgentAnalysis } from "@/components/agent-analysis" +import { LoadingSpinner } from "@/components/ui/loading-spinner" + +export default function ProcessQueuePage() { + return ( +
+ + + Processing Queue + View ongoing and completed analysis tasks + + + }> + + + + + + + + Past Analysis + Review previously completed analysis tasks + + + + + + + + + Agent Analysis + View aggregated sentiment analysis for agents + + + + + +
+ ) +} diff --git a/apps/mongo-feed/app/sentiment/page.tsx b/apps/mongo-feed/app/sentiment/page.tsx new file mode 100644 index 0000000..cb95614 --- /dev/null +++ b/apps/mongo-feed/app/sentiment/page.tsx @@ -0,0 +1,9 @@ +import { AgentList } from "@/components/sentiment/agent-list" + +export default function SentimentPage() { + return ( +
+ +
+ ) +} diff --git a/apps/mongo-feed/app/upload/page.tsx b/apps/mongo-feed/app/upload/page.tsx new file mode 100644 index 0000000..ef61043 --- /dev/null +++ b/apps/mongo-feed/app/upload/page.tsx @@ -0,0 +1,18 @@ +import { UploadForm } from "@/components/upload-form" +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" + +export default function UploadPage() { + return ( +
+ + + Upload Source File + Upload Files for Agent Feedback or Product Review Analysis + + + + + +
+ ) +} diff --git a/apps/mongo-feed/components.json b/apps/mongo-feed/components.json new file mode 100644 index 0000000..13f24bf --- /dev/null +++ b/apps/mongo-feed/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/apps/mongo-feed/components/agent-analysis.tsx b/apps/mongo-feed/components/agent-analysis.tsx new file mode 100644 index 0000000..8424fd5 --- /dev/null +++ b/apps/mongo-feed/components/agent-analysis.tsx @@ -0,0 +1,91 @@ +"use client" + +import { useState, useEffect } from "react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import { InfoIcon } from "lucide-react" +import { LoadingSpinner } from "@/components/ui/loading-spinner" + +type AgentSentiment = { + agentName: string + positiveSentiment: number + neutralSentiment: number + negativeSentiment: number + totalInteractions: number +} + +export function AgentAnalysis() { + const [agentSentiments, setAgentSentiments] = useState([]) + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + const fetchAgentAnalysis = async () => { + try { + const response = await fetch("/api/agent-analysis") + if (!response.ok) { + throw new Error("Failed to fetch agent analysis") + } + const data = await response.json() + setAgentSentiments(data) + } catch (error) { + console.error("Error fetching agent analysis:", error) + } finally { + setIsLoading(false) + } + } + + fetchAgentAnalysis() + }, []) + + if (isLoading) { + return + } + + return ( + + + + Agent Sentiment Analysis + + + + + + +

Agent sentiment analysis is aggregated from LLM processing of customer interactions.

+

+ Potential query: "Which agent has the highest positive sentiment ratio?" +

+
+
+
+
+
+ + + + + Agent Name + Positive + Neutral + Negative + Total Interactions + + + + {agentSentiments.map((agent) => ( + + {agent.agentName} + {agent.positiveSentiment} + {agent.neutralSentiment} + {agent.negativeSentiment} + {agent.totalInteractions} + + ))} + +
+
+
+ ) +} diff --git a/apps/mongo-feed/components/charts/feedback-trend.tsx b/apps/mongo-feed/components/charts/feedback-trend.tsx new file mode 100644 index 0000000..da872c4 --- /dev/null +++ b/apps/mongo-feed/components/charts/feedback-trend.tsx @@ -0,0 +1,100 @@ +"use client" + +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts" +import { Tooltip as UITooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import { InfoIcon } from "lucide-react" +import { useState, useEffect } from "react" +import { LoadingSpinner } from "@/components/ui/loading-spinner" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { ErrorMessage } from "@/components/ui/error-message" + +interface TrendData { + date: string + type: "agent" | "product" + positive: number + negative: number + neutral: number +} + +export function FeedbackTrendChart() { + const [trendData, setTrendData] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const [selectedType, setSelectedType] = useState<"all" | "agent" | "product">("all") + + useEffect(() => { + const fetchTrendData = async () => { + try { + const response = await fetch("/api/feedback/trend") + if (!response.ok) { + throw new Error("Failed to fetch trend data") + } + const data = await response.json() + setTrendData(data) + } catch (error) { + console.error("Error fetching trend data:", error) + setError("Failed to load trend data") + } finally { + setIsLoading(false) + } + } + + fetchTrendData() + }, []) + + if (isLoading) { + return + } + + if (error) { + return + } + + const filteredData = selectedType === "all" ? trendData : trendData.filter((item) => item.type === selectedType) + + return ( +
+
+ + + +
+

Feedback Trend

+ +
+
+ +

This chart shows the trend of feedback sentiment over time for both agent and product feedback.

+
+
+
+ + +
+ +
+ + + + + + + + + + + + +
+
+ ) +} diff --git a/apps/mongo-feed/components/charts/sentiment-distribution.tsx b/apps/mongo-feed/components/charts/sentiment-distribution.tsx new file mode 100644 index 0000000..d81449e --- /dev/null +++ b/apps/mongo-feed/components/charts/sentiment-distribution.tsx @@ -0,0 +1,133 @@ +"use client" + +import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts" +import { Tooltip as UITooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import { InfoIcon } from "lucide-react" +import { useState, useEffect } from "react" +import { LoadingSpinner } from "@/components/ui/loading-spinner" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { ErrorMessage } from "@/components/ui/error-message" + +interface SentimentData { + name: string + value: number + type: "agent" | "product" +} + +const COLORS = { + positive: "hsl(var(--primary))", + negative: "hsl(var(--destructive))", + neutral: "hsl(var(--muted-foreground))", + unknown: "hsl(var(--muted))", +} + +const SENTIMENT_LABELS = { + positive: "Positive", + negative: "Negative", + neutral: "Neutral", + unknown: "Unknown", +} + +export function SentimentDistributionChart() { + const [data, setData] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const [selectedType, setSelectedType] = useState<"all" | "agent" | "product">("all") + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetch("/api/feedback/sentiment-distribution") + if (!response.ok) { + throw new Error("Failed to fetch sentiment distribution") + } + const result = await response.json() + // Ensure the data is in the correct format + const formattedData = result.map((item: any) => ({ + name: item.name ? item.name.toLowerCase() : "unknown", + value: item.value || 0, + type: item.type || "unknown", + })) + setData(formattedData) + } catch (error) { + console.error("Error:", error) + setError("Failed to load sentiment distribution data") + } finally { + setIsLoading(false) + } + } + + fetchData() + }, []) + + if (isLoading) return + if (error) return + + const filteredData = selectedType === "all" ? data : data.filter((item) => item.type === selectedType) + + // Aggregate data by sentiment + const aggregatedData = filteredData.reduce( + (acc, item) => { + if (!acc[item.name]) { + acc[item.name] = 0 + } + acc[item.name] += item.value + return acc + }, + {} as Record, + ) + + // Convert aggregated data back to array format + const chartData = Object.entries(aggregatedData).map(([name, value]) => ({ name, value })) + + return ( +
+
+ + + +
+

Sentiment Distribution

+ +
+
+ +

Distribution of sentiment across all feedback types.

+
+
+
+ + +
+ + {chartData.length > 0 ? ( +
+ + + + {chartData.map((entry, index) => ( + + ))} + + + SENTIMENT_LABELS[value as keyof typeof SENTIMENT_LABELS] || value} + /> + + +
+ ) : ( +
No data available for the selected type.
+ )} +
+ ) +} diff --git a/apps/mongo-feed/components/charts/top-issues.tsx b/apps/mongo-feed/components/charts/top-issues.tsx new file mode 100644 index 0000000..9326510 --- /dev/null +++ b/apps/mongo-feed/components/charts/top-issues.tsx @@ -0,0 +1,90 @@ +"use client" + +import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts" +import { Tooltip as UITooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import { InfoIcon } from "lucide-react" +import { useState, useEffect } from "react" +import { LoadingSpinner } from "@/components/ui/loading-spinner" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { ErrorMessage } from "@/components/ui/error-message" + +interface TopIssue { + name: string + count: number + type: "agent" | "product" + sentiment: "positive" | "negative" | "neutral" +} + +export function TopIssuesChart() { + const [data, setData] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const [selectedType, setSelectedType] = useState<"all" | "agent" | "product">("all") + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetch("/api/feedback/top-issues") + if (!response.ok) throw new Error("Failed to fetch top issues") + const result = await response.json() + setData(result) + } catch (error) { + console.error("Error:", error) + setError("Failed to load top issues data") + } finally { + setIsLoading(false) + } + } + + fetchData() + }, []) + + if (isLoading) return + if (error) return + + const filteredData = selectedType === "all" ? data : data.filter((item) => item.type === selectedType) + + return ( +
+
+ + + +
+

Top Issues

+ +
+
+ +

Most frequently mentioned issues across feedback types.

+
+
+
+ + +
+ +
+ + + + + + + + + + +
+
+ ) +} diff --git a/apps/mongo-feed/components/feedback/feedback-chart.tsx b/apps/mongo-feed/components/feedback/feedback-chart.tsx new file mode 100644 index 0000000..d61854e --- /dev/null +++ b/apps/mongo-feed/components/feedback/feedback-chart.tsx @@ -0,0 +1,34 @@ +"use client" + +import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts" + +interface FeedbackChartProps { + data: Array<{ + date: string + positive: number + negative: number + neutral: number + }> +} + +export function FeedbackChart({ data }: FeedbackChartProps) { + return ( + + + + + + + + + + + + ) +} diff --git a/apps/mongo-feed/components/feedback/list.tsx b/apps/mongo-feed/components/feedback/list.tsx new file mode 100644 index 0000000..21cb9a0 --- /dev/null +++ b/apps/mongo-feed/components/feedback/list.tsx @@ -0,0 +1,62 @@ +"use client" + +import { useState, useEffect } from "react" +import { LoadingSpinner } from "@/components/ui/loading-spinner" +import { ErrorMessage } from "@/components/ui/error-message" + +export function FeedbackList() { + const [recentChats, setRecentChats] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + async function fetchData() { + try { + const response = await fetch("/api/feedback/recent") + if (!response.ok) { + throw new Error("Failed to fetch recent chats") + } + const data = await response.json() + setRecentChats(data) + } catch (err) { + setError("Failed to load recent feedback") + } finally { + setIsLoading(false) + } + } + + fetchData() + }, []) + + if (isLoading) return + if (error) return + + return ( +
+ {recentChats.map((chat: any) => ( +
+
+ {chat.overallSentiment.charAt(0).toUpperCase()} +
+
+

{chat.mainTopics.join(", ")}

+
+ {new Date(chat.createdAt).toLocaleDateString()} + + {chat.messages.length} messages +
+
+
+ ))} +
+ ) +} diff --git a/apps/mongo-feed/components/feedback/overview.tsx b/apps/mongo-feed/components/feedback/overview.tsx new file mode 100644 index 0000000..ff0fbb7 --- /dev/null +++ b/apps/mongo-feed/components/feedback/overview.tsx @@ -0,0 +1,87 @@ +"use client" + +import { useState, useEffect } from "react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import { InfoIcon } from "lucide-react" +import { FeedbackChart } from "./feedback-chart" +import { LoadingSpinner } from "@/components/ui/loading-spinner" +import { ErrorMessage } from "@/components/ui/error-message" + +interface FeedbackStats { + totalFeedback: number + sentimentScore: number + trendData: Array<{ + date: string + positive: number + negative: number + neutral: number + }> +} + +export function FeedbackOverview() { + const [stats, setStats] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + async function fetchData() { + try { + const response = await fetch("/api/feedback/overview") + if (!response.ok) { + throw new Error("Failed to fetch feedback overview") + } + const data = await response.json() + setStats(data) + } catch (err) { + setError("Failed to load feedback overview") + console.error("Error fetching feedback overview:", err) + } finally { + setIsLoading(false) + } + } + + fetchData() + }, []) + + if (isLoading) return + if (error) return + if (!stats) return null + + return ( + + + + Feedback Overview + + + + + + +

Overview of all feedback data aggregated from MongoDB

+
+
+
+
+
+ +
+
+
+
Total Feedback
+
{stats.totalFeedback}
+
+
+
Sentiment
+
{stats.sentimentScore}%
+
+
+
+ +
+
+
+
+ ) +} diff --git a/apps/mongo-feed/components/feedback/recent-feedback.tsx b/apps/mongo-feed/components/feedback/recent-feedback.tsx new file mode 100644 index 0000000..3c16658 --- /dev/null +++ b/apps/mongo-feed/components/feedback/recent-feedback.tsx @@ -0,0 +1,69 @@ +"use client" + +import { useState, useEffect } from "react" +import { LoadingSpinner } from "@/components/ui/loading-spinner" +import { ErrorMessage } from "@/components/ui/error-message" +import { Badge } from "@/components/ui/badge" + +interface FeedbackItem { + id: string + score: number + title: string + type: "agent" | "product" + timeAgo: string +} + +export function RecentFeedback() { + const [feedback, setFeedback] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + async function fetchData() { + try { + const response = await fetch("/api/feedback/recent") + if (!response.ok) { + throw new Error("Failed to fetch recent feedback") + } + const data = await response.json() + setFeedback(data) + } catch (err) { + setError("Failed to load recent feedback") + } finally { + setIsLoading(false) + } + } + + fetchData() + }, []) + + if (isLoading) return + if (error) return + + return ( +
+ {feedback.map((item) => ( +
+
= 4 + ? "bg-emerald-100 text-emerald-700" + : item.score <= 2 + ? "bg-red-100 text-red-700" + : "bg-gray-100 text-gray-700" + }`} + > + {item.score} +
+
+
+

{item.title}

+ {item.type} +
+
{item.timeAgo}
+
+
+ ))} +
+ ) +} diff --git a/apps/mongo-feed/components/feedback/top-issues.tsx b/apps/mongo-feed/components/feedback/top-issues.tsx new file mode 100644 index 0000000..189167e --- /dev/null +++ b/apps/mongo-feed/components/feedback/top-issues.tsx @@ -0,0 +1,60 @@ +"use client" + +import { useState, useEffect } from "react" +import { Badge } from "@/components/ui/badge" +import { LoadingSpinner } from "@/components/ui/loading-spinner" +import { ErrorMessage } from "@/components/ui/error-message" +import { Card } from "@/components/ui/card" + +interface TopIssue { + name: string + count: number + type: "agent" | "product" + sentiment: "positive" | "negative" | "neutral" +} + +export function TopIssues() { + const [issues, setIssues] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + async function fetchData() { + try { + const response = await fetch("/api/feedback/top-issues?sentiment=negative") + if (!response.ok) { + throw new Error("Failed to fetch top issues") + } + const data = await response.json() + setIssues(data) + } catch (err) { + setError("Failed to load top issues") + } finally { + setIsLoading(false) + } + } + + fetchData() + }, []) + + if (isLoading) return + if (error) return + if (!issues.length) return
No issues found
+ + return ( +
+ {issues.map((issue, index) => ( + +
+

{issue.name}

+ {issue.count} +
+
+ {issue.type} + negative +
+
+ ))} +
+ ) +} diff --git a/apps/mongo-feed/components/navigation.tsx b/apps/mongo-feed/components/navigation.tsx new file mode 100644 index 0000000..7150d88 --- /dev/null +++ b/apps/mongo-feed/components/navigation.tsx @@ -0,0 +1,113 @@ +'use client' + +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { ScrollArea } from '@/components/ui/scroll-area' +import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet' +import { Upload, MessageSquare, BarChart2, LineChart, PieChart, Lock, Menu, ListChecks } from 'lucide-react' + +export function Navigation() { + const pathname = usePathname() + + const NavContent = () => ( + +
+
+

+ Input Sources +

+ + Upload Files + + + Paste Chat + +
+
+

+ Processing +

+ + Process Queue + +
+
+

+ Insights +

+ + Feedback Analysis + + + Agent Sentiment + + + Charts + +
+
+
+ ) + + const NavItem = ({ + href, + icon: Icon, + children + }: { + href: string + icon: React.ComponentType<{ className?: string }> + children: React.ReactNode + }) => { + const isActive = pathname === href + return ( + + + + ) + } + + return ( + <> + {/* Mobile Navigation */} + + + + + +
+
+ + MONGOFEED +
+
+ +
+
+ + {/* Desktop Navigation */} +
+
+
+ + MONGOFEED +
+
+ +
+ + ) +} diff --git a/apps/mongo-feed/components/past-analysis.tsx b/apps/mongo-feed/components/past-analysis.tsx new file mode 100644 index 0000000..2280349 --- /dev/null +++ b/apps/mongo-feed/components/past-analysis.tsx @@ -0,0 +1,94 @@ +"use client" + +import { useState, useEffect } from "react" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Button } from "@/components/ui/button" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import { InfoIcon, Download } from "lucide-react" +import { LoadingSpinner } from "@/components/ui/loading-spinner" + +type AnalysisItem = { + id: string + name: string + date: string + duration: string + status: "completed" | "failed" +} + +export function PastAnalysis() { + const [pastAnalysis, setPastAnalysis] = useState([]) + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + const fetchPastAnalysis = async () => { + try { + const response = await fetch("/api/past-analysis") + if (!response.ok) { + throw new Error("Failed to fetch past analysis") + } + const data = await response.json() + setPastAnalysis(data) + } catch (error) { + console.error("Error fetching past analysis:", error) + } finally { + setIsLoading(false) + } + } + + fetchPastAnalysis() + }, []) + + if (isLoading) { + return + } + + return ( +
+
+

Past Analysis

+ + + + + + +

Past analysis results are stored in MongoDB and can be quickly retrieved using Atlas vector search.

+

+ Potential query: "Find similar analyses to the most recent customer feedback report" +

+
+
+
+
+ + + + Name + Date + Duration + Status + Actions + + + + {pastAnalysis.map((item) => ( + + {item.name} + {item.date} + {item.duration} + + {item.status} + + + + + + ))} + +
+
+ ) +} diff --git a/apps/mongo-feed/components/paste-form.tsx b/apps/mongo-feed/components/paste-form.tsx new file mode 100644 index 0000000..33e5a04 --- /dev/null +++ b/apps/mongo-feed/components/paste-form.tsx @@ -0,0 +1,182 @@ +"use client" + +import { useState } from "react" +import { useRouter } from "next/navigation" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import { InfoIcon } from "lucide-react" +import { LoadingSpinner } from "@/components/ui/loading-spinner" +import { Badge } from "@/components/ui/badge" + +const exampleChats = { + neutral: `Customer: Hello, I have a question about my recent order. +Agent: Hello, this is Sarah. I'd be happy to help. Can you please provide your order number? +Customer: My order number is 12345. +Agent: Thank you. I've located your order. What specific information do you need? +Customer: I was wondering about the estimated delivery date. +Agent: According to our system, your order is scheduled to be delivered on June 15th. +Customer: Okay, that's what I thought. Thanks for confirming. +Agent: You're welcome. Is there anything else I can assist you with today? +Customer: No, that's all. Thank you. +Agent: Thank you for contacting us. Have a great day!`, + + positive: `Customer: Hi there! I just received my order and I'm absolutely thrilled! +Agent: Hello! This is Mike. I'm so glad to hear that you're happy with your order! What did you particularly like about it? +Customer: Everything! The quality is amazing and it arrived much faster than I expected. +Agent: That's wonderful to hear! We always strive to exceed our customers' expectations. Is there anything specific about the product or service you'd like to highlight? +Customer: Yes, the packaging was eco-friendly, which I really appreciate. And the product itself is even better than described on the website. +Agent: I'm delighted to hear that! We've been working hard on our eco-friendly packaging initiative, and it's great to know it's being noticed and appreciated. I'll make sure to pass your feedback about the product description to our team. +Customer: Please do! You've gained a loyal customer. Thank you so much, Mike! +Agent: You're very welcome! It's customers like you that make our job so rewarding. Is there anything else I can help you with today? +Customer: No, that's all. Thanks again for the great service! +Agent: It's been my pleasure. Thank you for your business and have a fantastic day!`, + + negative: `Customer: I'm extremely frustrated with your service! My order is late and no one seems to care! +Agent: I apologize for the inconvenience. My name is Alex, and I'm here to help. Can you please provide me with your order number? +Customer: It's order number 67890. It was supposed to arrive three days ago! +Agent: I'm sorry to hear that your order is delayed. Let me look into this for you right away. +Customer: This is unacceptable. I needed this for an important event! +Agent: I completely understand your frustration. I see that there was an unexpected delay in our shipping department. While this doesn't excuse the delay, I want to assure you that I'm going to do everything I can to resolve this for you. +Customer: Well, what can you do about it now? The event is tomorrow! +Agent: I sincerely apologize for this situation. Here's what I can do: I'll expedite your shipping to overnight delivery at no extra cost to you. Additionally, I'd like to offer you a 20% discount on your next purchase for the inconvenience caused. +Customer: I appreciate you trying to help, but this really messed up my plans. +Agent: I understand, and I'm truly sorry for the impact this has had on your plans. Is there anything else I can do to help make this situation better for you? +Customer: No, just make sure it arrives tomorrow. +Agent: Absolutely. I've personally flagged this for priority overnight shipping. You'll receive a tracking number within the hour. Again, I'm very sorry for this experience.`, +} + +interface Message { + role: "Customer" | "Agent" + content: string +} + +export function PasteForm() { + const [content, setContent] = useState(exampleChats.neutral) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const router = useRouter() + + const parseConversation = (text: string): Message[] => { + const lines = text.split("\n").filter((line) => line.trim() !== "") + const messages: Message[] = [] + let currentRole: "Customer" | "Agent" | null = null + let currentContent: string[] = [] + + for (const line of lines) { + if (line.startsWith("Customer:") || line.startsWith("Agent:")) { + if (currentRole && currentContent.length > 0) { + messages.push({ + role: currentRole, + content: currentContent.join(" ").trim(), + }) + currentContent = [] + } + currentRole = line.startsWith("Customer:") ? "Customer" : "Agent" + currentContent.push(line.substring(line.indexOf(":") + 1).trim()) + } else if (currentRole) { + currentContent.push(line.trim()) + } + } + + if (currentRole && currentContent.length > 0) { + messages.push({ + role: currentRole, + content: currentContent.join(" ").trim(), + }) + } + + return messages + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!content.trim()) { + setError("Please enter some chat content before submitting.") + return + } + + setIsLoading(true) + setError(null) + + const messages = parseConversation(content) + + try { + const response = await fetch("/api/submit-chat", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ messages }), + }) + + if (!response.ok) { + throw new Error("Failed to submit chat") + } + + const { id } = await response.json() + router.push(`/process-queue?id=${id}`) + } catch (err) { + setError("An error occurred while submitting the chat. Please try again.") + setIsLoading(false) + } + } + + return ( +
+
+
+ + + + + + + +

+ Paste your chat logs here. Our system will analyze the content using Bedrock LLMs and store the + results in MongoDB for quick retrieval. +

+

+ Format: Start each message with "Customer:" or "Agent:" +

+
+
+
+
+
+ setContent(exampleChats.neutral)}> + Neutral Example + + setContent(exampleChats.positive)}> + Positive Example + + setContent(exampleChats.negative)}> + Negative Example + +
+