Add Store Feedback service
This commit is contained in:
40
FeedbackService/build.gradle
Normal file
40
FeedbackService/build.gradle
Normal file
@@ -0,0 +1,40 @@
|
||||
import org.springframework.boot.gradle.plugin.SpringBootPlugin
|
||||
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'org.springframework.boot'
|
||||
apply plugin: 'io.spring.dependency-management'
|
||||
|
||||
description = "Feedback Service"
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom SpringBootPlugin.BOM_COORDINATES
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
implementation "me.paulschwarz:spring-dotenv:4.0.0"
|
||||
|
||||
implementation 'com.opencsv:opencsv:5.7.1'
|
||||
implementation 'edu.stanford.nlp:stanford-corenlp:4.5.9:models'
|
||||
implementation 'edu.stanford.nlp:stanford-corenlp:4.5.9'
|
||||
|
||||
implementation 'ch.qos.logback:logback-classic:1.5.13'
|
||||
implementation 'org.slf4j:slf4j-api:2.0.9'
|
||||
|
||||
runtimeOnly 'org.springframework.boot:spring-boot-devtools'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.retailstore.feedback;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class FeedbackServiceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
SentimentAnalyzer.main(args);
|
||||
|
||||
SpringApplication.run(FeedbackServiceApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
package com.retailstore.feedback;
|
||||
|
||||
import edu.stanford.nlp.pipeline.CoreDocument;
|
||||
import edu.stanford.nlp.pipeline.CoreSentence;
|
||||
import edu.stanford.nlp.pipeline.StanfordCoreNLP;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Analyzes the sentiment of customer feedback using Stanford CoreNLP.
|
||||
* This class reads feedback from a file, performs sentiment analysis,
|
||||
* and writes the results to an output file.
|
||||
*/
|
||||
public class SentimentAnalyzer {
|
||||
|
||||
public static final String OUTPUT_FILE = "FeedbackService/sentiment_feedback_output.txt";
|
||||
|
||||
// Regular expressions to extract feedback components
|
||||
private static final Pattern FEEDBACK_NUMBER_PATTERN = Pattern.compile("Feedback #(\\d+).*");
|
||||
private static final Pattern CUSTOMER_PATTERN = Pattern.compile("Customer:\\s*(.+)");
|
||||
private static final Pattern DEPARTMENT_PATTERN = Pattern.compile("Department:\\s*(.+)");
|
||||
private static final Pattern DATE_PATTERN = Pattern.compile("Date:\\s*(.+)");
|
||||
private static final Pattern COMMENT_PATTERN = Pattern.compile("Comment:\\s*(.+)");
|
||||
private static final Logger log = LoggerFactory.getLogger(SentimentAnalyzer.class);
|
||||
|
||||
// Stanford CoreNLP pipeline
|
||||
private final StanfordCoreNLP pipeline;
|
||||
|
||||
/**
|
||||
* Constructor initializes the Stanford CoreNLP pipeline with necessary properties.
|
||||
*/
|
||||
public SentimentAnalyzer() {
|
||||
// Set up pipeline properties
|
||||
Properties props = new Properties();
|
||||
// Set the list of annotators to run
|
||||
props.setProperty("annotators", "tokenize, ssplit, pos, lemma, parse, sentiment");
|
||||
// Build pipeline
|
||||
log.info("Initializing Stanford CoreNLP pipeline...");
|
||||
this.pipeline = new StanfordCoreNLP(props);
|
||||
log.info("Pipeline initialized successfully.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Main method to run the sentiment analysis
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
SentimentAnalyzer analyzer = new SentimentAnalyzer();
|
||||
|
||||
String inputFilePath = "FeedbackService/store_feedback.txt";
|
||||
String outputFilePath = OUTPUT_FILE;
|
||||
|
||||
try {
|
||||
log.info("Starting sentiment analysis...");
|
||||
log.info("Reading from: {}", inputFilePath);
|
||||
|
||||
// Process all feedback entries
|
||||
List<FeedbackEntry> feedbackEntries = analyzer.processFeedbackFile(inputFilePath);
|
||||
|
||||
log.info("Processed {} feedback entries.", feedbackEntries.size());
|
||||
|
||||
// Print first few entries for debugging
|
||||
if (!feedbackEntries.isEmpty()) {
|
||||
log.info("\nFirst entry processed:");
|
||||
FeedbackEntry first = feedbackEntries.getFirst();
|
||||
log.info("ID: {}", first.getId());
|
||||
log.info("Customer: {}", first.getCustomer());
|
||||
log.info("Comment: {}", first.getComment());
|
||||
log.info("Sentiment: {}", first.getSentiment());
|
||||
}
|
||||
|
||||
// Write results to output file
|
||||
analyzer.writeResults(feedbackEntries, outputFilePath);
|
||||
|
||||
log.info("\nSentiment analysis completed successfully. Results written to {}", outputFilePath);
|
||||
} catch (IOException e) {
|
||||
log.error("Error processing feedback: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the feedback file and returns a list of feedback entries with sentiment analysis
|
||||
*
|
||||
* @param filePath Path to the feedback file
|
||||
* @return List of feedback entries with sentiment analysis
|
||||
* @throws IOException If an I/O error occurs
|
||||
*/
|
||||
public List<FeedbackEntry> processFeedbackFile(String filePath) throws IOException {
|
||||
String content = new String(Files.readAllBytes(Paths.get(filePath)));
|
||||
|
||||
List<FeedbackEntry> feedbackEntries = new ArrayList<>();
|
||||
|
||||
// Split the content by the "Feedback #" pattern to get individual entries
|
||||
String[] rawEntries = content.split("(?=Feedback #)");
|
||||
|
||||
log.info("Found {} potential feedback entries.", rawEntries.length - 1); // -1 because first split might be empty
|
||||
|
||||
for (int i = 0; i < rawEntries.length; i++) {
|
||||
String rawEntry = rawEntries[i].trim();
|
||||
|
||||
// Skip empty entries
|
||||
if (!rawEntry.startsWith("Feedback #")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
FeedbackEntry feedbackEntry = parseEntry(rawEntry);
|
||||
|
||||
if (feedbackEntry != null) {
|
||||
// Analyze sentiment
|
||||
log.info("Analyzing sentiment for entry #{}", feedbackEntry.getId());
|
||||
String sentiment = analyzeSentiment(feedbackEntry.comment);
|
||||
feedbackEntry.setSentiment(sentiment);
|
||||
|
||||
feedbackEntries.add(feedbackEntry);
|
||||
} else {
|
||||
log.error("Failed to parse entry at position {}", i);
|
||||
}
|
||||
}
|
||||
|
||||
return feedbackEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a single feedback entry text into a FeedbackEntry object
|
||||
*
|
||||
* @param entryText The text of a single feedback entry
|
||||
* @return FeedbackEntry object
|
||||
*/
|
||||
private FeedbackEntry parseEntry(String entryText) {
|
||||
FeedbackEntry entry = new FeedbackEntry();
|
||||
|
||||
try {
|
||||
// Split into lines for easier parsing
|
||||
String[] lines = entryText.split("\n");
|
||||
|
||||
// Parse each line looking for specific patterns
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
|
||||
// Extract feedback number from first line
|
||||
if (line.startsWith("Feedback #")) {
|
||||
Matcher numberMatcher = FEEDBACK_NUMBER_PATTERN.matcher(line);
|
||||
if (numberMatcher.find()) {
|
||||
entry.setId(Integer.parseInt(numberMatcher.group(1)));
|
||||
}
|
||||
}
|
||||
|
||||
// Extract customer info
|
||||
else if (line.startsWith("Customer:")) {
|
||||
Matcher customerMatcher = CUSTOMER_PATTERN.matcher(line);
|
||||
if (customerMatcher.find()) {
|
||||
entry.setCustomer(customerMatcher.group(1).trim());
|
||||
}
|
||||
}
|
||||
|
||||
// Extract department
|
||||
else if (line.startsWith("Department:")) {
|
||||
Matcher departmentMatcher = DEPARTMENT_PATTERN.matcher(line);
|
||||
if (departmentMatcher.find()) {
|
||||
entry.setDepartment(departmentMatcher.group(1).trim());
|
||||
}
|
||||
}
|
||||
|
||||
// Extract date
|
||||
else if (line.startsWith("Date:")) {
|
||||
Matcher dateMatcher = DATE_PATTERN.matcher(line);
|
||||
if (dateMatcher.find()) {
|
||||
entry.setDate(dateMatcher.group(1).trim());
|
||||
}
|
||||
}
|
||||
|
||||
// Extract comment
|
||||
else if (line.startsWith("Comment:")) {
|
||||
Matcher commentMatcher = COMMENT_PATTERN.matcher(line);
|
||||
if (commentMatcher.find()) {
|
||||
entry.setComment(commentMatcher.group(1).trim());
|
||||
}
|
||||
}
|
||||
|
||||
// Note: We're ignoring the original sentiment line as we'll calculate it ourselves
|
||||
}
|
||||
|
||||
// Validate that we have all required fields
|
||||
if (entry.getId() > 0 && entry.getComment() != null) {
|
||||
return entry;
|
||||
} else {
|
||||
log.error("Invalid entry - missing ID or comment");
|
||||
log.error("ID: {}", entry.getId());
|
||||
log.error("Comment: {}", entry.getComment());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error parsing entry: {}", e.getMessage(), e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes the sentiment of a comment using Stanford CoreNLP
|
||||
*
|
||||
* @param comment The comment to analyze
|
||||
* @return Sentiment (VERY_POSITIVE, POSITIVE, NEUTRAL, NEGATIVE, VERY_NEGATIVE)
|
||||
*/
|
||||
public String analyzeSentiment(String comment) {
|
||||
// Create a document from the comment
|
||||
CoreDocument doc = new CoreDocument(comment);
|
||||
|
||||
// Annotate the document
|
||||
pipeline.annotate(doc);
|
||||
|
||||
// Get the sentiment scores for each sentence
|
||||
List<CoreSentence> sentences = doc.sentences();
|
||||
|
||||
if (sentences.isEmpty()) {
|
||||
return "NEUTRAL";
|
||||
}
|
||||
|
||||
// Calculate the average sentiment
|
||||
Map<String, Integer> sentimentCounts = new HashMap<>();
|
||||
for (CoreSentence sentence : sentences) {
|
||||
String sentiment = sentence.sentiment();
|
||||
sentimentCounts.put(sentiment, sentimentCounts.getOrDefault(sentiment, 0) + 1);
|
||||
}
|
||||
|
||||
// Determine the most common sentiment
|
||||
return sentimentCounts.entrySet().stream()
|
||||
.max(Map.Entry.comparingByValue())
|
||||
.map(Map.Entry::getKey)
|
||||
.orElse("NEUTRAL");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the results of sentiment analysis to an output file
|
||||
*
|
||||
* @param feedbackEntries List of feedback entries with sentiment analysis
|
||||
* @param outputFilePath Path to the output file
|
||||
* @throws IOException If an I/O error occurs
|
||||
*/
|
||||
public void writeResults(List<FeedbackEntry> feedbackEntries, String outputFilePath) throws IOException {
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFilePath))) {
|
||||
// Write header
|
||||
writer.write("# Sentiment Analysis Results\n\n");
|
||||
|
||||
// Write summary statistics
|
||||
writer.write("## Summary Statistics\n\n");
|
||||
|
||||
// Count sentiments
|
||||
Map<String, Long> sentimentCounts = feedbackEntries.stream()
|
||||
.collect(Collectors.groupingBy(FeedbackEntry::getSentiment, Collectors.counting()));
|
||||
|
||||
writer.write("Total Feedback Entries: " + feedbackEntries.size() + "\n");
|
||||
writer.write("Sentiment Distribution:\n");
|
||||
|
||||
for (Map.Entry<String, Long> entry : sentimentCounts.entrySet()) {
|
||||
double percentage = (double) entry.getValue() / feedbackEntries.size() * 100;
|
||||
writer.write(String.format("- %s: %d (%.1f%%)\n", entry.getKey(), entry.getValue(), percentage));
|
||||
}
|
||||
|
||||
writer.write("\n## Department Analysis\n\n");
|
||||
|
||||
// Group by department
|
||||
Map<String, List<FeedbackEntry>> byDepartment = feedbackEntries.stream()
|
||||
.collect(Collectors.groupingBy(FeedbackEntry::getDepartment));
|
||||
|
||||
for (Map.Entry<String, List<FeedbackEntry>> entry : byDepartment.entrySet()) {
|
||||
writer.write("### " + entry.getKey() + "\n\n");
|
||||
|
||||
// Count sentiments per department
|
||||
Map<String, Long> deptSentimentCounts = entry.getValue().stream()
|
||||
.collect(Collectors.groupingBy(FeedbackEntry::getSentiment, Collectors.counting()));
|
||||
|
||||
for (Map.Entry<String, Long> sentCount : deptSentimentCounts.entrySet()) {
|
||||
double percentage = (double) sentCount.getValue() / entry.getValue().size() * 100;
|
||||
writer.write(String.format("- %s: %d (%.1f%%)\n", sentCount.getKey(), sentCount.getValue(), percentage));
|
||||
}
|
||||
|
||||
writer.write("\n");
|
||||
}
|
||||
|
||||
// Write detailed entries
|
||||
writer.write("## Detailed Feedback Entries\n\n");
|
||||
|
||||
for (FeedbackEntry entry : feedbackEntries) {
|
||||
writer.write("Feedback #" + entry.getId() + "\n");
|
||||
writer.write("Customer: " + entry.getCustomer() + "\n");
|
||||
writer.write("Department: " + entry.getDepartment() + "\n");
|
||||
writer.write("Date: " + entry.getDate() + "\n");
|
||||
writer.write("Comment: " + entry.getComment() + "\n");
|
||||
writer.write("Sentiment: " + entry.getSentiment() + "\n\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner class representing a feedback entry with sentiment analysis
|
||||
*/
|
||||
public static class FeedbackEntry {
|
||||
private int id;
|
||||
private String customer;
|
||||
private String department;
|
||||
private String date;
|
||||
private String comment;
|
||||
private String sentiment;
|
||||
|
||||
// Default values to avoid null
|
||||
public FeedbackEntry() {
|
||||
this.customer = "";
|
||||
this.department = "";
|
||||
this.date = "";
|
||||
this.comment = "";
|
||||
this.sentiment = "";
|
||||
}
|
||||
|
||||
// Getters and setters
|
||||
public int getId() { return id; }
|
||||
public void setId(int id) { this.id = id; }
|
||||
|
||||
public String getCustomer() { return customer; }
|
||||
public void setCustomer(String customer) { this.customer = customer; }
|
||||
|
||||
public String getDepartment() { return department; }
|
||||
public void setDepartment(String department) { this.department = department; }
|
||||
|
||||
public String getDate() { return date; }
|
||||
public void setDate(String date) { this.date = date; }
|
||||
|
||||
public String getComment() { return comment; }
|
||||
public void setComment(String comment) { this.comment = comment; }
|
||||
|
||||
public String getSentiment() { return sentiment; }
|
||||
public void setSentiment(String sentiment) { this.sentiment = sentiment; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.retailstore.feedback.controller;
|
||||
|
||||
import com.retailstore.feedback.model.EnhancedFeedback;
|
||||
import com.retailstore.feedback.model.FeedbackSummary;
|
||||
import com.retailstore.feedback.service.FeedbackService;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Controller for feedback-related endpoints.
|
||||
*/
|
||||
@Controller
|
||||
public class FeedbackController {
|
||||
|
||||
@Autowired
|
||||
private FeedbackService feedbackService;
|
||||
|
||||
/**
|
||||
* Displays the dashboard page.
|
||||
*
|
||||
* @param model Model to add attributes to
|
||||
* @return The name of the view to render
|
||||
*/
|
||||
@GetMapping("/")
|
||||
public String dashboard(Model model) {
|
||||
try {
|
||||
FeedbackSummary summary = feedbackService.generateFeedbackSummary();
|
||||
model.addAttribute("summary", summary);
|
||||
return "dashboard";
|
||||
} catch (IOException e) {
|
||||
model.addAttribute("error", "Error loading feedback data: " + e.getMessage());
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all feedback data in JSON format.
|
||||
*
|
||||
* @return List of enhanced feedback entries
|
||||
*/
|
||||
@GetMapping("/getfeedback")
|
||||
@ResponseBody
|
||||
public ResponseEntity<List<EnhancedFeedback>> getFeedback() {
|
||||
try {
|
||||
List<EnhancedFeedback> feedback = feedbackService.getEnhancedFeedback();
|
||||
return ResponseEntity.ok(feedback);
|
||||
} catch (IOException e) {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.retailstore.feedback.model;
|
||||
|
||||
/**
|
||||
* Represents feedback enhanced with AI-generated category and actionable insights.
|
||||
*/
|
||||
public class EnhancedFeedback extends FeedbackEntry {
|
||||
private String category;
|
||||
private String actionableInsight;
|
||||
|
||||
// Default constructor
|
||||
public EnhancedFeedback() {
|
||||
super();
|
||||
}
|
||||
|
||||
// Constructor based on FeedbackEntry
|
||||
public EnhancedFeedback(FeedbackEntry entry) {
|
||||
super(entry.getId(), entry.getCustomer(), entry.getDepartment(),
|
||||
entry.getDate(), entry.getComment(), entry.getSentiment());
|
||||
}
|
||||
|
||||
// Getters and setters for additional fields
|
||||
public String getCategory() { return category; }
|
||||
public void setCategory(String category) { this.category = category; }
|
||||
|
||||
public String getActionableInsight() { return actionableInsight; }
|
||||
public void setActionableInsight(String actionableInsight) {
|
||||
this.actionableInsight = actionableInsight;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.retailstore.feedback.model;
|
||||
|
||||
/**
|
||||
* Represents a feedback entry with sentiment analysis.
|
||||
*/
|
||||
public class FeedbackEntry {
|
||||
private int id;
|
||||
private String customer;
|
||||
private String department;
|
||||
private String date;
|
||||
private String comment;
|
||||
private String sentiment;
|
||||
|
||||
// Default constructor
|
||||
public FeedbackEntry() {}
|
||||
|
||||
// Constructor with parameters
|
||||
public FeedbackEntry(int id, String customer, String department,
|
||||
String date, String comment, String sentiment) {
|
||||
this.id = id;
|
||||
this.customer = customer;
|
||||
this.department = department;
|
||||
this.date = date;
|
||||
this.comment = comment;
|
||||
this.sentiment = sentiment;
|
||||
}
|
||||
|
||||
// Getters and setters
|
||||
public int getId() { return id; }
|
||||
public void setId(int id) { this.id = id; }
|
||||
|
||||
public String getCustomer() { return customer; }
|
||||
public void setCustomer(String customer) { this.customer = customer; }
|
||||
|
||||
public String getDepartment() { return department; }
|
||||
public void setDepartment(String department) { this.department = department; }
|
||||
|
||||
public String getDate() { return date; }
|
||||
public void setDate(String date) { this.date = date; }
|
||||
|
||||
public String getComment() { return comment; }
|
||||
public void setComment(String comment) { this.comment = comment; }
|
||||
|
||||
public String getSentiment() { return sentiment; }
|
||||
public void setSentiment(String sentiment) { this.sentiment = sentiment; }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.retailstore.feedback.model;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Represents a summary of feedback data for the dashboard.
|
||||
*/
|
||||
public class FeedbackSummary {
|
||||
private int totalFeedback;
|
||||
private Map<String, Integer> sentimentCounts;
|
||||
private Map<String, Integer> categoryCounts;
|
||||
private Map<String, Integer> departmentCounts;
|
||||
private List<EnhancedFeedback> recentFeedback;
|
||||
|
||||
// Default constructor
|
||||
public FeedbackSummary() {}
|
||||
|
||||
// Getters and setters
|
||||
public int getTotalFeedback() { return totalFeedback; }
|
||||
public void setTotalFeedback(int totalFeedback) {
|
||||
this.totalFeedback = totalFeedback;
|
||||
}
|
||||
|
||||
public Map<String, Integer> getSentimentCounts() { return sentimentCounts; }
|
||||
public void setSentimentCounts(Map<String, Integer> sentimentCounts) {
|
||||
this.sentimentCounts = sentimentCounts;
|
||||
}
|
||||
|
||||
public Map<String, Integer> getCategoryCounts() { return categoryCounts; }
|
||||
public void setCategoryCounts(Map<String, Integer> categoryCounts) {
|
||||
this.categoryCounts = categoryCounts;
|
||||
}
|
||||
|
||||
public Map<String, Integer> getDepartmentCounts() { return departmentCounts; }
|
||||
public void setDepartmentCounts(Map<String, Integer> departmentCounts) {
|
||||
this.departmentCounts = departmentCounts;
|
||||
}
|
||||
|
||||
public List<EnhancedFeedback> getRecentFeedback() { return recentFeedback; }
|
||||
public void setRecentFeedback(List<EnhancedFeedback> recentFeedback) {
|
||||
this.recentFeedback = recentFeedback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package com.retailstore.feedback.service;
|
||||
|
||||
import com.retailstore.feedback.SentimentAnalyzer;
|
||||
import com.retailstore.feedback.model.FeedbackEntry;
|
||||
import com.retailstore.feedback.model.EnhancedFeedback;
|
||||
import com.retailstore.feedback.model.FeedbackSummary;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Service for processing feedback data and enhancing it with AI insights.
|
||||
*/
|
||||
@Service
|
||||
public class FeedbackService {
|
||||
|
||||
@Autowired
|
||||
private GeminiService geminiService;
|
||||
|
||||
// Path to sentiment analysis output file
|
||||
private static final String SENTIMENT_FILE_PATH = SentimentAnalyzer.OUTPUT_FILE;
|
||||
|
||||
// Patterns for parsing feedback file
|
||||
private static final Pattern FEEDBACK_PATTERN = Pattern.compile("Feedback #(\\d+)");
|
||||
private static final Pattern CUSTOMER_PATTERN = Pattern.compile("Customer:\\s*(.+)");
|
||||
private static final Pattern DEPARTMENT_PATTERN = Pattern.compile("Department:\\s*(.+)");
|
||||
private static final Pattern DATE_PATTERN = Pattern.compile("Date:\\s*(.+)");
|
||||
private static final Pattern COMMENT_PATTERN = Pattern.compile("Comment:\\s*(.+)");
|
||||
private static final Pattern SENTIMENT_PATTERN = Pattern.compile("Sentiment:\\s*(.+)");
|
||||
|
||||
private static final String PROMPT = """
|
||||
You are an AI assistant specialized in customer feedback analysis.
|
||||
Analyze the following customer feedback and:
|
||||
1. Categorize the feedback into one of these categories: Product Quality, Customer Service, Store Experience, Website/App, Delivery, Price/Value, Inventory/Stock, or Other.
|
||||
2. Provide a specific actionable insight or recommendation based on the feedback.
|
||||
|
||||
Format your response as JSON with two fields: "category" and "actionableInsight".
|
||||
Keep your response concise but insightful.
|
||||
|
||||
Customer Feedback:
|
||||
Comment: %s
|
||||
Department: %s
|
||||
Sentiment: %s
|
||||
|
||||
Provide the category and actionable insight as JSON:
|
||||
""";
|
||||
|
||||
// Cache for feedback data
|
||||
private List<EnhancedFeedback> enhancedFeedbackCache = null;
|
||||
|
||||
/**
|
||||
* Reads feedback data from the sentiment analysis output file.
|
||||
*
|
||||
* @return List of FeedbackEntry objects
|
||||
* @throws IOException If an I/O error occurs
|
||||
*/
|
||||
public List<FeedbackEntry> readFeedbackData() throws IOException {
|
||||
List<FeedbackEntry> entries = new ArrayList<>();
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(SENTIMENT_FILE_PATH))) {
|
||||
StringBuilder entryText = new StringBuilder();
|
||||
String line;
|
||||
|
||||
boolean inDetailedSection = false;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
// Check if we've reached the detailed feedback section
|
||||
if (line.contains("## Detailed Feedback Entries")) {
|
||||
inDetailedSection = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inDetailedSection) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process each line
|
||||
if (line.trim().isEmpty() && entryText.length() > 0) {
|
||||
// We've reached the end of an entry
|
||||
FeedbackEntry entry = parseFeedbackEntry(entryText.toString());
|
||||
if (entry != null) {
|
||||
entries.add(entry);
|
||||
}
|
||||
entryText = new StringBuilder();
|
||||
} else {
|
||||
entryText.append(line).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Process the last entry if there is one
|
||||
if (entryText.length() > 0) {
|
||||
FeedbackEntry entry = parseFeedbackEntry(entryText.toString());
|
||||
if (entry != null) {
|
||||
entries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a feedback entry from text.
|
||||
*
|
||||
* @param text Text containing a feedback entry
|
||||
* @return FeedbackEntry object or null if parsing fails
|
||||
*/
|
||||
private FeedbackEntry parseFeedbackEntry(String text) {
|
||||
FeedbackEntry entry = new FeedbackEntry();
|
||||
|
||||
// Extract feedback ID
|
||||
Matcher idMatcher = FEEDBACK_PATTERN.matcher(text);
|
||||
if (idMatcher.find()) {
|
||||
entry.setId(Integer.parseInt(idMatcher.group(1)));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract customer
|
||||
Matcher customerMatcher = CUSTOMER_PATTERN.matcher(text);
|
||||
if (customerMatcher.find()) {
|
||||
entry.setCustomer(customerMatcher.group(1));
|
||||
}
|
||||
|
||||
// Extract department
|
||||
Matcher departmentMatcher = DEPARTMENT_PATTERN.matcher(text);
|
||||
if (departmentMatcher.find()) {
|
||||
entry.setDepartment(departmentMatcher.group(1));
|
||||
}
|
||||
|
||||
// Extract date
|
||||
Matcher dateMatcher = DATE_PATTERN.matcher(text);
|
||||
if (dateMatcher.find()) {
|
||||
entry.setDate(dateMatcher.group(1));
|
||||
}
|
||||
|
||||
// Extract comment
|
||||
Matcher commentMatcher = COMMENT_PATTERN.matcher(text);
|
||||
if (commentMatcher.find()) {
|
||||
entry.setComment(commentMatcher.group(1));
|
||||
}
|
||||
|
||||
// Extract sentiment
|
||||
Matcher sentimentMatcher = SENTIMENT_PATTERN.matcher(text);
|
||||
if (sentimentMatcher.find()) {
|
||||
entry.setSentiment(sentimentMatcher.group(1));
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances feedback with AI-generated categories and actionable insights.
|
||||
*
|
||||
* @return List of EnhancedFeedback objects
|
||||
* @throws IOException If an I/O error occurs
|
||||
*/
|
||||
public synchronized List<EnhancedFeedback> getEnhancedFeedback() throws IOException {
|
||||
// Return cached data if available
|
||||
if (enhancedFeedbackCache != null) {
|
||||
return enhancedFeedbackCache;
|
||||
}
|
||||
|
||||
List<FeedbackEntry> entries = readFeedbackData();
|
||||
List<EnhancedFeedback> enhancedEntries = new ArrayList<>();
|
||||
|
||||
for (FeedbackEntry entry : entries) {
|
||||
EnhancedFeedback enhancedEntry = enhanceFeedback(entry);
|
||||
enhancedEntries.add(enhancedEntry);
|
||||
}
|
||||
|
||||
// Cache the enhanced feedback
|
||||
enhancedFeedbackCache = enhancedEntries;
|
||||
|
||||
return enhancedEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances a single feedback entry with AI-generated category and actionable insight.
|
||||
*
|
||||
* @param entry FeedbackEntry to enhance
|
||||
* @return EnhancedFeedback with AI-generated category and actionable insight
|
||||
*/
|
||||
private EnhancedFeedback enhanceFeedback(FeedbackEntry entry) {
|
||||
EnhancedFeedback enhancedEntry = new EnhancedFeedback(entry);
|
||||
|
||||
// Create a prompt for Gemini
|
||||
String prompt = PROMPT.formatted(entry.getComment(), entry.getDepartment(), entry.getSentiment());
|
||||
|
||||
try {
|
||||
// Call Gemini API and parse the response
|
||||
String response = geminiService.generateContent(prompt);
|
||||
|
||||
// Parse JSON response
|
||||
// This is a simple parsing approach - for production, use a proper JSON parser
|
||||
String jsonResponse = response.trim();
|
||||
|
||||
// Extract category
|
||||
Pattern categoryPattern = Pattern.compile("\"category\"\\s*:\\s*\"([^\"]+)\"");
|
||||
Matcher categoryMatcher = categoryPattern.matcher(jsonResponse);
|
||||
if (categoryMatcher.find()) {
|
||||
enhancedEntry.setCategory(categoryMatcher.group(1));
|
||||
} else {
|
||||
enhancedEntry.setCategory("Uncategorized");
|
||||
}
|
||||
|
||||
// Extract actionable insight
|
||||
Pattern insightPattern = Pattern.compile("\"actionableInsight\"\\s*:\\s*\"([^\"]+)\"");
|
||||
Matcher insightMatcher = insightPattern.matcher(jsonResponse);
|
||||
if (insightMatcher.find()) {
|
||||
enhancedEntry.setActionableInsight(insightMatcher.group(1));
|
||||
} else {
|
||||
enhancedEntry.setActionableInsight("No specific action recommended.");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
// Handle API errors gracefully
|
||||
enhancedEntry.setCategory("Error in processing");
|
||||
enhancedEntry.setActionableInsight("Could not generate insight due to API error: " + e.getMessage());
|
||||
}
|
||||
|
||||
return enhancedEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a summary of the feedback data for the dashboard.
|
||||
*
|
||||
* @return FeedbackSummary object
|
||||
* @throws IOException If an I/O error occurs
|
||||
*/
|
||||
public FeedbackSummary generateFeedbackSummary() throws IOException {
|
||||
List<EnhancedFeedback> allFeedback = getEnhancedFeedback();
|
||||
FeedbackSummary summary = new FeedbackSummary();
|
||||
|
||||
// Set total feedback count
|
||||
summary.setTotalFeedback(allFeedback.size());
|
||||
|
||||
// Count sentiments
|
||||
Map<String, Integer> sentimentCounts = new HashMap<>();
|
||||
for (EnhancedFeedback feedback : allFeedback) {
|
||||
String sentiment = feedback.getSentiment();
|
||||
sentimentCounts.put(sentiment, sentimentCounts.getOrDefault(sentiment, 0) + 1);
|
||||
}
|
||||
summary.setSentimentCounts(sentimentCounts);
|
||||
|
||||
// Count categories
|
||||
Map<String, Integer> categoryCounts = new HashMap<>();
|
||||
for (EnhancedFeedback feedback : allFeedback) {
|
||||
String category = feedback.getCategory();
|
||||
categoryCounts.put(category, categoryCounts.getOrDefault(category, 0) + 1);
|
||||
}
|
||||
summary.setCategoryCounts(categoryCounts);
|
||||
|
||||
// Count departments
|
||||
Map<String, Integer> departmentCounts = new HashMap<>();
|
||||
for (EnhancedFeedback feedback : allFeedback) {
|
||||
String department = feedback.getDepartment();
|
||||
departmentCounts.put(department, departmentCounts.getOrDefault(department, 0) + 1);
|
||||
}
|
||||
summary.setDepartmentCounts(departmentCounts);
|
||||
|
||||
// Get recent feedback (last 5 entries)
|
||||
List<EnhancedFeedback> recentFeedback = allFeedback.stream()
|
||||
.sorted(Comparator.comparing(EnhancedFeedback::getId).reversed())
|
||||
.limit(5)
|
||||
.collect(Collectors.toList());
|
||||
summary.setRecentFeedback(recentFeedback);
|
||||
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.retailstore.feedback.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.http.client.support.HttpRequestWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Service for interacting with Google's Gemini AI using REST API
|
||||
*/
|
||||
@Service
|
||||
public class GeminiService {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private final String serviceUri;
|
||||
|
||||
public GeminiService(@Value("${gemini.api.url}") String url, @Value("${gemini.api.key}") String apiKey) {
|
||||
this.objectMapper = new JsonMapper();
|
||||
serviceUri = UriComponentsBuilder
|
||||
.fromUriString(url)
|
||||
.queryParam("key", apiKey)
|
||||
.build().toUriString();
|
||||
restTemplate = new RestTemplateBuilder()
|
||||
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a prompt to Gemini and returns the response
|
||||
*
|
||||
* @param prompt The prompt to send to Gemini
|
||||
* @return The response from Gemini
|
||||
*/
|
||||
public String generateContent(String prompt) {
|
||||
try {
|
||||
// Create the request body using Jackson
|
||||
ObjectNode requestBody = objectMapper.createObjectNode();
|
||||
ArrayNode contents = objectMapper.createArrayNode();
|
||||
ObjectNode content = objectMapper.createObjectNode();
|
||||
ArrayNode parts = objectMapper.createArrayNode();
|
||||
ObjectNode textPart = objectMapper.createObjectNode();
|
||||
|
||||
textPart.put("text", prompt);
|
||||
parts.add(textPart);
|
||||
content.set("parts", parts);
|
||||
contents.add(content);
|
||||
requestBody.set("contents", contents);
|
||||
|
||||
// Add generation config for better JSON responses
|
||||
ObjectNode generationConfig = objectMapper.createObjectNode();
|
||||
generationConfig.put("temperature", 0.7);
|
||||
generationConfig.put("maxOutputTokens", 1024);
|
||||
requestBody.set("generationConfig", generationConfig);
|
||||
|
||||
// Create the request
|
||||
RequestEntity<ObjectNode> request = RequestEntity.post(serviceUri).body(requestBody);
|
||||
|
||||
// Send the request
|
||||
ResponseEntity<String> response = restTemplate.exchange(request, String.class);
|
||||
|
||||
// Parse the response
|
||||
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
||||
ObjectNode responseJson = (ObjectNode) objectMapper.readTree(response.getBody());
|
||||
ArrayNode candidates = (ArrayNode) responseJson.get("candidates");
|
||||
|
||||
if (candidates != null && !candidates.isEmpty()) {
|
||||
ObjectNode candidate = (ObjectNode) candidates.get(0);
|
||||
ObjectNode candidateContent = (ObjectNode) candidate.get("content");
|
||||
ArrayNode candidateParts = (ArrayNode) candidateContent.get("parts");
|
||||
|
||||
if (candidateParts != null && !candidateParts.isEmpty()) {
|
||||
return candidateParts.get(0).get("text").asText();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "No response from Gemini";
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("Error calling Gemini API: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return "Error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method to verify API connection
|
||||
*/
|
||||
public void testConnection() {
|
||||
String testPrompt = "Say 'Hello, World!' if you can hear me.";
|
||||
String response = generateContent(testPrompt);
|
||||
System.out.println("Gemini API Test Response: " + response);
|
||||
}
|
||||
}
|
||||
13
FeedbackService/src/main/resources/application.properties
Normal file
13
FeedbackService/src/main/resources/application.properties
Normal file
@@ -0,0 +1,13 @@
|
||||
server.port=8080
|
||||
spring.application.name=feedback-service
|
||||
|
||||
gemini.api.key=${GEMINI_API_KEY}
|
||||
gemini.api.url=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent
|
||||
|
||||
# Logging configuration
|
||||
logging.level.com.retailstore=DEBUG
|
||||
logging.level.org.springframework.web=INFO
|
||||
|
||||
# Thymeleaf configuration
|
||||
spring.thymeleaf.cache=false
|
||||
spring.thymeleaf.mode=HTML
|
||||
14
FeedbackService/src/main/resources/logback.xml
Normal file
14
FeedbackService/src/main/resources/logback.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</root>
|
||||
|
||||
<!-- Stanford CoreNLP specific logging (optional - reduce verbosity) -->
|
||||
<logger name="edu.stanford.nlp" level="WARN" />
|
||||
</configuration>
|
||||
327
FeedbackService/src/main/resources/templates/dashboard.html
Normal file
327
FeedbackService/src/main/resources/templates/dashboard.html
Normal file
@@ -0,0 +1,327 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Customer Feedback Dashboard</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
<style>
|
||||
.card {
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.sentiment-badge {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.sentiment-VERY_POSITIVE, .sentiment-POSITIVE {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
.sentiment-NEUTRAL {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
.sentiment-NEGATIVE, .sentiment-VERY_NEGATIVE {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
.insight-card {
|
||||
background-color: #f8f9fa;
|
||||
border-left: 4px solid #007bff;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-dark bg-primary mb-4">
|
||||
<div class="container">
|
||||
<span class="navbar-brand mb-0 h1">Retail Store Customer Feedback Dashboard</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<!-- Summary Section -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Feedback Summary</h5>
|
||||
<p class="card-text">Total Feedback: <span th:text="${summary.totalFeedback}">0</span></p>
|
||||
<div class="mt-3">
|
||||
<h6>Sentiment Distribution</h6>
|
||||
<div class="chart-container">
|
||||
<canvas id="sentimentChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Category Distribution</h5>
|
||||
<div class="chart-container">
|
||||
<canvas id="categoryChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Department Distribution</h5>
|
||||
<div class="chart-container">
|
||||
<canvas id="departmentChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Feedback Section -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">Recent Feedback</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Customer</th>
|
||||
<th>Department</th>
|
||||
<th>Date</th>
|
||||
<th>Comment</th>
|
||||
<th>Sentiment</th>
|
||||
<th>Category</th>
|
||||
<th>Insight</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="feedback : ${summary.recentFeedback}">
|
||||
<td th:text="${feedback.id}">1</td>
|
||||
<td th:text="${feedback.customer}">John Doe</td>
|
||||
<td th:text="${feedback.department}">Electronics</td>
|
||||
<td th:text="${feedback.date}">2023-02-15</td>
|
||||
<td th:text="${feedback.comment}">Great service!</td>
|
||||
<td>
|
||||
<span class="badge rounded-pill"
|
||||
th:text="${feedback.sentiment}"
|
||||
th:classappend="${'sentiment-' + feedback.sentiment}">
|
||||
POSITIVE
|
||||
</span>
|
||||
</td>
|
||||
<td th:text="${feedback.category}">Customer Service</td>
|
||||
<td>
|
||||
<div class="insight-card" th:text="${feedback.actionableInsight}">
|
||||
Recognize staff for excellent service.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Full Feedback Section -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">All Feedback</h5>
|
||||
<button id="refreshBtn" class="btn btn-sm btn-outline-primary">Refresh Data</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="allFeedbackContainer" class="table-responsive">
|
||||
<p class="text-center">Loading all feedback...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script th:inline="javascript">
|
||||
// Get summary data from Thymeleaf
|
||||
const summaryData = /*[[${summary}]]*/ {};
|
||||
|
||||
// Prepare chart data
|
||||
const prepareChartData = (dataObject) => {
|
||||
const labels = Object.keys(dataObject);
|
||||
const values = Object.values(dataObject);
|
||||
const backgroundColors = labels.map(() =>
|
||||
`rgba(${Math.floor(Math.random() * 155) + 100}, ${Math.floor(Math.random() * 155) + 100}, ${Math.floor(Math.random() * 155) + 100}, 0.6)`
|
||||
);
|
||||
|
||||
return {
|
||||
labels,
|
||||
datasets: [{
|
||||
data: values,
|
||||
backgroundColor: backgroundColors,
|
||||
borderWidth: 1
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
// Create sentiment chart
|
||||
const createSentimentChart = () => {
|
||||
const ctx = document.getElementById('sentimentChart').getContext('2d');
|
||||
const data = prepareChartData(summaryData.sentimentCounts);
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'pie',
|
||||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Sentiment Distribution'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Create category chart
|
||||
const createCategoryChart = () => {
|
||||
const ctx = document.getElementById('categoryChart').getContext('2d');
|
||||
const data = prepareChartData(summaryData.categoryCounts);
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Category Distribution'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Create department chart
|
||||
const createDepartmentChart = () => {
|
||||
const ctx = document.getElementById('departmentChart').getContext('2d');
|
||||
const data = prepareChartData(summaryData.departmentCounts);
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Department Distribution'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Fetch all feedback and update the table
|
||||
const fetchAllFeedback = () => {
|
||||
fetch('/getfeedback')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const container = document.getElementById('allFeedbackContainer');
|
||||
|
||||
// Create table
|
||||
let tableHtml = `
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Customer</th>
|
||||
<th>Department</th>
|
||||
<th>Date</th>
|
||||
<th>Sentiment</th>
|
||||
<th>Category</th>
|
||||
<th>Comment</th>
|
||||
<th>Insight</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
// Add rows
|
||||
data.forEach(feedback => {
|
||||
let sentimentClass = '';
|
||||
if (feedback.sentiment === 'VERY_POSITIVE' || feedback.sentiment === 'POSITIVE') {
|
||||
sentimentClass = 'sentiment-POSITIVE';
|
||||
} else if (feedback.sentiment === 'NEGATIVE' || feedback.sentiment === 'VERY_NEGATIVE') {
|
||||
sentimentClass = 'sentiment-NEGATIVE';
|
||||
} else {
|
||||
sentimentClass = 'sentiment-NEUTRAL';
|
||||
}
|
||||
|
||||
tableHtml += `
|
||||
<tr>
|
||||
<td>${feedback.id}</td>
|
||||
<td>${feedback.customer}</td>
|
||||
<td>${feedback.department}</td>
|
||||
<td>${feedback.date}</td>
|
||||
<td><span class="badge rounded-pill ${sentimentClass}">${feedback.sentiment}</span></td>
|
||||
<td>${feedback.category}</td>
|
||||
<td>${feedback.comment}</td>
|
||||
<td><div class="insight-card">${feedback.actionableInsight}</div></td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
tableHtml += `
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
container.innerHTML = tableHtml;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching feedback:', error);
|
||||
document.getElementById('allFeedbackContainer').innerHTML =
|
||||
'<div class="alert alert-danger">Error loading feedback data. Please try again later.</div>';
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize charts and data when the page loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
createSentimentChart();
|
||||
createCategoryChart();
|
||||
createDepartmentChart();
|
||||
fetchAllFeedback();
|
||||
|
||||
// Set up refresh button
|
||||
document.getElementById('refreshBtn').addEventListener('click', fetchAllFeedback);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
27
FeedbackService/src/main/resources/templates/error.html
Normal file
27
FeedbackService/src/main/resources/templates/error.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Error - Customer Feedback Dashboard</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h4 class="mb-0">Error</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text" th:text="${error}">An error occurred.</p>
|
||||
<p>Please ensure that the sentiment analysis has been run and the output file exists.</p>
|
||||
<a href="/" class="btn btn-primary">Try Again</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.retailstore.feedback;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class FeedbackServiceApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
349
FeedbackService/store_feedback.txt
Normal file
349
FeedbackService/store_feedback.txt
Normal file
@@ -0,0 +1,349 @@
|
||||
Feedback #1 [POSITIVE]
|
||||
Customer: Lisa Martinez, New Customer
|
||||
Department: Returns
|
||||
Date: 2025-04-24
|
||||
Comment: The staff was very helpful and friendly. The restrooms were clean and well-maintained.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #2 [NEGATIVE]
|
||||
Customer: Michael Johnson, Loyalty Member
|
||||
Department: Customer Service
|
||||
Date: 2025-05-10
|
||||
Comment: Staff seemed uninterested in helping me. The music playing in the store was too loud.
|
||||
Sentiment: NEGATIVE
|
||||
|
||||
Feedback #3 [POSITIVE]
|
||||
Customer: Emily Williams, Online Shopper
|
||||
Department: Returns
|
||||
Date: 2025-04-14
|
||||
Comment: Product quality exceeded my expectations.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #4 [POSITIVE]
|
||||
Customer: James Rodriguez, Returning Customer
|
||||
Department: Electronics
|
||||
Date: 2025-05-02
|
||||
Comment: I love the new store design! I had to travel across the store multiple times to find everything.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #5 [NEUTRAL]
|
||||
Customer: David Garcia, VIP Member
|
||||
Department: Checkout
|
||||
Date: 2025-05-03
|
||||
Comment: Prices are comparable to other stores.
|
||||
Sentiment: NEUTRAL
|
||||
|
||||
Feedback #6 [NEGATIVE]
|
||||
Customer: Lisa Martinez, New Customer
|
||||
Department: Returns
|
||||
Date: 2025-05-05
|
||||
Comment: I had to wait too long for assistance. The store was very crowded during my visit.
|
||||
Sentiment: NEGATIVE
|
||||
|
||||
Feedback #7 [NEUTRAL]
|
||||
Customer: Lisa Martinez, New Customer
|
||||
Department: Customer Service
|
||||
Date: 2025-04-13
|
||||
Comment: The shopping experience was average. Parking was difficult to find.
|
||||
Sentiment: NEUTRAL
|
||||
|
||||
Feedback #8 [POSITIVE]
|
||||
Customer: David Garcia, VIP Member
|
||||
Department: Product Selection
|
||||
Date: 2025-04-29
|
||||
Comment: Customer service representative solved my problem quickly. The music playing in the store was too loud.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #9 [POSITIVE]
|
||||
Customer: James Rodriguez, Returning Customer
|
||||
Department: Product Selection
|
||||
Date: 2025-04-24
|
||||
Comment: Customer service representative solved my problem quickly. The store app helped me locate products easily.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #10 [NEGATIVE]
|
||||
Customer: James Rodriguez, Returning Customer
|
||||
Department: Online Shopping
|
||||
Date: 2025-04-11
|
||||
Comment: My delivery arrived damaged.
|
||||
Sentiment: NEGATIVE
|
||||
|
||||
Feedback #11 [POSITIVE]
|
||||
Customer: Patricia Wilson, Weekend Shopper
|
||||
Department: Home Goods
|
||||
Date: 2025-04-17
|
||||
Comment: Store layout made it easy to find what I needed. I will definitely shop here again.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #12 [NEGATIVE]
|
||||
Customer: Jane Smith, First-time Shopper
|
||||
Department: Clothing
|
||||
Date: 2025-04-25
|
||||
Comment: Too few cashiers during peak hours. The store was very crowded during my visit.
|
||||
Sentiment: NEGATIVE
|
||||
|
||||
Feedback #13 [POSITIVE]
|
||||
Customer: Robert Brown, Frequent Buyer
|
||||
Department: Electronics
|
||||
Date: 2025-05-07
|
||||
Comment: Customer service representative solved my problem quickly. I appreciated the seasonal decorations.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #14 [NEUTRAL]
|
||||
Customer: John Doe, Regular Customer
|
||||
Department: Grocery
|
||||
Date: 2025-04-19
|
||||
Comment: Store layout is functional. The restrooms were clean and well-maintained.
|
||||
Sentiment: NEUTRAL
|
||||
|
||||
Feedback #15 [POSITIVE]
|
||||
Customer: Emily Williams, Online Shopper
|
||||
Department: Clothing
|
||||
Date: 2025-04-28
|
||||
Comment: Great selection of products available.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #16 [NEGATIVE]
|
||||
Customer: Sarah Miller, Occasional Shopper
|
||||
Department: Electronics
|
||||
Date: 2025-05-06
|
||||
Comment: Prices were higher than advertised online. I might reconsider shopping here in the future.
|
||||
Sentiment: NEGATIVE
|
||||
|
||||
Feedback #17 [POSITIVE]
|
||||
Customer: David Garcia, VIP Member
|
||||
Department: Returns
|
||||
Date: 2025-04-22
|
||||
Comment: Checkout process was quick and efficient. Self-checkout machines were all working properly.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #18 [POSITIVE]
|
||||
Customer: Jane Smith, First-time Shopper
|
||||
Department: Grocery
|
||||
Date: 2025-04-15
|
||||
Comment: Website navigation is intuitive and user-friendly. I appreciated the seasonal decorations.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #19 [NEGATIVE]
|
||||
Customer: Michael Johnson, Loyalty Member
|
||||
Department: Home Goods
|
||||
Date: 2025-04-13
|
||||
Comment: Website crashed during checkout. I might reconsider shopping here in the future.
|
||||
Sentiment: NEGATIVE
|
||||
|
||||
Feedback #20 [NEUTRAL]
|
||||
Customer: John Doe, Regular Customer
|
||||
Department: Store Environment
|
||||
Date: 2025-05-09
|
||||
Comment: Product selection is what I expected. Self-checkout machines were all working properly.
|
||||
Sentiment: NEUTRAL
|
||||
|
||||
Feedback #21 [POSITIVE]
|
||||
Customer: Robert Brown, Frequent Buyer
|
||||
Department: Online Shopping
|
||||
Date: 2025-04-20
|
||||
Comment: Prices were reasonable for the quality offered. The store app helped me locate products easily.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #22 [NEGATIVE]
|
||||
Customer: John Doe, Regular Customer
|
||||
Department: Store Environment
|
||||
Date: 2025-04-26
|
||||
Comment: Product I wanted was out of stock. I had to travel across the store multiple times to find everything.
|
||||
Sentiment: NEGATIVE
|
||||
|
||||
Feedback #23 [POSITIVE]
|
||||
Customer: David Garcia, VIP Member
|
||||
Department: Grocery
|
||||
Date: 2025-04-16
|
||||
Comment: I had an excellent shopping experience today. I will definitely shop here again.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #24 [NEGATIVE]
|
||||
Customer: Patricia Wilson, Weekend Shopper
|
||||
Department: Customer Service
|
||||
Date: 2025-05-08
|
||||
Comment: The store was messy and disorganized.
|
||||
Sentiment: NEGATIVE
|
||||
|
||||
Feedback #25 [NEUTRAL]
|
||||
Customer: Robert Brown, Frequent Buyer
|
||||
Department: Returns
|
||||
Date: 2025-04-18
|
||||
Comment: Online ordering works as expected. Parking was difficult to find.
|
||||
Sentiment: NEUTRAL
|
||||
|
||||
Feedback #26 [POSITIVE]
|
||||
Customer: Sarah Miller, Occasional Shopper
|
||||
Department: Checkout
|
||||
Date: 2025-04-12
|
||||
Comment: I had an excellent shopping experience today. I will definitely shop here again.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #27 [POSITIVE]
|
||||
Customer: Michael Johnson, Loyalty Member
|
||||
Department: Online Shopping
|
||||
Date: 2025-05-04
|
||||
Comment: Website navigation is intuitive and user-friendly. I appreciated the seasonal decorations.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #28 [NEUTRAL]
|
||||
Customer: John Doe, Regular Customer
|
||||
Department: Clothing
|
||||
Date: 2025-04-23
|
||||
Comment: Product quality meets basic expectations. The music playing in the store was too loud.
|
||||
Sentiment: NEUTRAL
|
||||
|
||||
Feedback #29 [NEGATIVE]
|
||||
Customer: Sarah Miller, Occasional Shopper
|
||||
Department: Returns
|
||||
Date: 2025-04-30
|
||||
Comment: Return policy is too restrictive. I might reconsider shopping here in the future.
|
||||
Sentiment: NEGATIVE
|
||||
|
||||
Feedback #30 [POSITIVE]
|
||||
Customer: Jane Smith, First-time Shopper
|
||||
Department: Checkout
|
||||
Date: 2025-05-01
|
||||
Comment: The staff was very helpful and friendly. I will definitely shop here again.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #31 [NEGATIVE]
|
||||
Customer: Patricia Wilson, Weekend Shopper
|
||||
Department: Online Shopping
|
||||
Date: 2025-04-21
|
||||
Comment: Poor quality products for the price. I might reconsider shopping here in the future.
|
||||
Sentiment: NEGATIVE
|
||||
|
||||
Feedback #32 [POSITIVE]
|
||||
Customer: Emily Williams, Online Shopper
|
||||
Department: Store Environment
|
||||
Date: 2025-05-06
|
||||
Comment: Store layout made it easy to find what I needed. The restrooms were clean and well-maintained.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #33 [NEUTRAL]
|
||||
Customer: David Garcia, VIP Member
|
||||
Department: Product Selection
|
||||
Date: 2025-04-15
|
||||
Comment: Staff was professional but not exceptional. The store was very crowded during my visit.
|
||||
Sentiment: NEUTRAL
|
||||
|
||||
Feedback #34 [POSITIVE]
|
||||
Customer: Lisa Martinez, New Customer
|
||||
Department: Electronics
|
||||
Date: 2025-04-27
|
||||
Comment: Website navigation is intuitive and user-friendly. I will definitely shop here again.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #35 [NEGATIVE]
|
||||
Customer: Emily Williams, Online Shopper
|
||||
Department: Grocery
|
||||
Date: 2025-04-12
|
||||
Comment: Staff seemed uninterested in helping me. The store was very crowded during my visit.
|
||||
Sentiment: NEGATIVE
|
||||
|
||||
Feedback #36 [POSITIVE]
|
||||
Customer: Robert Brown, Frequent Buyer
|
||||
Department: Home Goods
|
||||
Date: 2025-04-29
|
||||
Comment: I had an excellent shopping experience today. The store app helped me locate products easily.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #37 [NEUTRAL]
|
||||
Customer: Michael Johnson, Loyalty Member
|
||||
Department: Returns
|
||||
Date: 2025-05-09
|
||||
Comment: Return process was straightforward. Parking was difficult to find.
|
||||
Sentiment: NEUTRAL
|
||||
|
||||
Feedback #38 [POSITIVE]
|
||||
Customer: James Rodriguez, Returning Customer
|
||||
Department: Customer Service
|
||||
Date: 2025-04-19
|
||||
Comment: Great selection of products available. I appreciated the seasonal decorations.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #39 [NEGATIVE]
|
||||
Customer: John Doe, Regular Customer
|
||||
Department: Clothing
|
||||
Date: 2025-05-10
|
||||
Comment: I had to wait too long for assistance. I had to travel across the store multiple times to find everything.
|
||||
Sentiment: NEGATIVE
|
||||
|
||||
Feedback #40 [POSITIVE]
|
||||
Customer: Sarah Miller, Occasional Shopper
|
||||
Department: Product Selection
|
||||
Date: 2025-04-14
|
||||
Comment: I love the new store design! I will definitely shop here again.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #41 [NEUTRAL]
|
||||
Customer: Patricia Wilson, Weekend Shopper
|
||||
Department: Returns
|
||||
Date: 2025-05-03
|
||||
Comment: Checkout process took a reasonable amount of time. Self-checkout machines were all working properly.
|
||||
Sentiment: NEUTRAL
|
||||
|
||||
Feedback #42 [NEGATIVE]
|
||||
Customer: Lisa Martinez, New Customer
|
||||
Department: Checkout
|
||||
Date: 2025-04-17
|
||||
Comment: Poor quality products for the price. The music playing in the store was too loud.
|
||||
Sentiment: NEGATIVE
|
||||
|
||||
Feedback #43 [POSITIVE]
|
||||
Customer: Robert Brown, Frequent Buyer
|
||||
Department: Clothing
|
||||
Date: 2025-04-25
|
||||
Comment: Product quality exceeded my expectations. The restrooms were clean and well-maintained.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #44 [POSITIVE]
|
||||
Customer: Jane Smith, First-time Shopper
|
||||
Department: Electronics
|
||||
Date: 2025-05-02
|
||||
Comment: Great selection of products available. I appreciated the seasonal decorations.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #45 [NEGATIVE]
|
||||
Customer: David Garcia, VIP Member
|
||||
Department: Store Environment
|
||||
Date: 2025-04-23
|
||||
Comment: Product I wanted was out of stock. I might reconsider shopping here in the future.
|
||||
Sentiment: NEGATIVE
|
||||
|
||||
Feedback #46 [NEUTRAL]
|
||||
Customer: Emily Williams, Online Shopper
|
||||
Department: Checkout
|
||||
Date: 2025-04-27
|
||||
Comment: Prices are comparable to other stores. The store was very crowded during my visit.
|
||||
Sentiment: NEUTRAL
|
||||
|
||||
Feedback #47 [POSITIVE]
|
||||
Customer: James Rodriguez, Returning Customer
|
||||
Department: Grocery
|
||||
Date: 2025-05-07
|
||||
Comment: Checkout process was quick and efficient. Self-checkout machines were all working properly.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #48 [NEGATIVE]
|
||||
Customer: Sarah Miller, Occasional Shopper
|
||||
Department: Online Shopping
|
||||
Date: 2025-04-16
|
||||
Comment: Website crashed during checkout. I might reconsider shopping here in the future.
|
||||
Sentiment: NEGATIVE
|
||||
|
||||
Feedback #49 [POSITIVE]
|
||||
Customer: John Doe, Regular Customer
|
||||
Department: Product Selection
|
||||
Date: 2025-05-05
|
||||
Comment: Prices were reasonable for the quality offered. I will definitely shop here again.
|
||||
Sentiment: POSITIVE
|
||||
|
||||
Feedback #50 [NEUTRAL]
|
||||
Customer: Michael Johnson, Loyalty Member
|
||||
Department: Clothing
|
||||
Date: 2025-04-22
|
||||
Comment: Store layout is functional. Parking was difficult to find.
|
||||
Sentiment: NEUTRAL
|
||||
@@ -11,3 +11,4 @@ include 'RegressionPredictionLab'
|
||||
include 'SentimentAnalysisLab'
|
||||
include 'ImageRecognitionLab'
|
||||
include 'CustomerSupportChatBot'
|
||||
include 'FeedbackService'
|
||||
Reference in New Issue
Block a user