Add Store Feedback service

This commit is contained in:
2025-11-12 14:17:41 +01:00
parent 9cce120e60
commit b43ec0856b
16 changed files with 1708 additions and 1 deletions

View 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'
}

View File

@@ -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);
}
}

View File

@@ -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; }
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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; }
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View 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

View 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>

View 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>

View 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>

View File

@@ -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() {
}
}

View 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

View File

@@ -10,4 +10,5 @@ include 'SoftwareDevChatbot'
include 'RegressionPredictionLab'
include 'SentimentAnalysisLab'
include 'ImageRecognitionLab'
include 'CustomerSupportChatBot'
include 'CustomerSupportChatBot'
include 'FeedbackService'