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 'SentimentAnalysisLab'
|
||||||
include 'ImageRecognitionLab'
|
include 'ImageRecognitionLab'
|
||||||
include 'CustomerSupportChatBot'
|
include 'CustomerSupportChatBot'
|
||||||
|
include 'FeedbackService'
|
||||||
Reference in New Issue
Block a user