From b43ec0856b652bd226528ee002147c05a907c0f1 Mon Sep 17 00:00:00 2001 From: John Ahlroos Date: Wed, 12 Nov 2025 14:17:41 +0100 Subject: [PATCH] Add Store Feedback service --- FeedbackService/build.gradle | 40 ++ .../feedback/FeedbackServiceApplication.java | 16 + .../feedback/SentimentAnalyzer.java | 343 +++++++++++++++++ .../controller/FeedbackController.java | 59 +++ .../feedback/model/EnhancedFeedback.java | 29 ++ .../feedback/model/FeedbackEntry.java | 46 +++ .../feedback/model/FeedbackSummary.java | 44 +++ .../feedback/service/FeedbackService.java | 278 ++++++++++++++ .../feedback/service/GeminiService.java | 108 ++++++ .../src/main/resources/application.properties | 13 + .../src/main/resources/logback.xml | 14 + .../main/resources/templates/dashboard.html | 327 ++++++++++++++++ .../src/main/resources/templates/error.html | 27 ++ .../FeedbackServiceApplicationTests.java | 13 + FeedbackService/store_feedback.txt | 349 ++++++++++++++++++ settings.gradle | 3 +- 16 files changed, 1708 insertions(+), 1 deletion(-) create mode 100644 FeedbackService/build.gradle create mode 100644 FeedbackService/src/main/java/com/retailstore/feedback/FeedbackServiceApplication.java create mode 100644 FeedbackService/src/main/java/com/retailstore/feedback/SentimentAnalyzer.java create mode 100644 FeedbackService/src/main/java/com/retailstore/feedback/controller/FeedbackController.java create mode 100644 FeedbackService/src/main/java/com/retailstore/feedback/model/EnhancedFeedback.java create mode 100644 FeedbackService/src/main/java/com/retailstore/feedback/model/FeedbackEntry.java create mode 100644 FeedbackService/src/main/java/com/retailstore/feedback/model/FeedbackSummary.java create mode 100644 FeedbackService/src/main/java/com/retailstore/feedback/service/FeedbackService.java create mode 100644 FeedbackService/src/main/java/com/retailstore/feedback/service/GeminiService.java create mode 100644 FeedbackService/src/main/resources/application.properties create mode 100644 FeedbackService/src/main/resources/logback.xml create mode 100644 FeedbackService/src/main/resources/templates/dashboard.html create mode 100644 FeedbackService/src/main/resources/templates/error.html create mode 100644 FeedbackService/src/test/java/com/retailstore/feedback/FeedbackServiceApplicationTests.java create mode 100644 FeedbackService/store_feedback.txt diff --git a/FeedbackService/build.gradle b/FeedbackService/build.gradle new file mode 100644 index 0000000..ecbd0c5 --- /dev/null +++ b/FeedbackService/build.gradle @@ -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' +} \ No newline at end of file diff --git a/FeedbackService/src/main/java/com/retailstore/feedback/FeedbackServiceApplication.java b/FeedbackService/src/main/java/com/retailstore/feedback/FeedbackServiceApplication.java new file mode 100644 index 0000000..5aea4d8 --- /dev/null +++ b/FeedbackService/src/main/java/com/retailstore/feedback/FeedbackServiceApplication.java @@ -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); + } + +} \ No newline at end of file diff --git a/FeedbackService/src/main/java/com/retailstore/feedback/SentimentAnalyzer.java b/FeedbackService/src/main/java/com/retailstore/feedback/SentimentAnalyzer.java new file mode 100644 index 0000000..7d57ecb --- /dev/null +++ b/FeedbackService/src/main/java/com/retailstore/feedback/SentimentAnalyzer.java @@ -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 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 processFeedbackFile(String filePath) throws IOException { + String content = new String(Files.readAllBytes(Paths.get(filePath))); + + List 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 sentences = doc.sentences(); + + if (sentences.isEmpty()) { + return "NEUTRAL"; + } + + // Calculate the average sentiment + Map 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 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 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 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> byDepartment = feedbackEntries.stream() + .collect(Collectors.groupingBy(FeedbackEntry::getDepartment)); + + for (Map.Entry> entry : byDepartment.entrySet()) { + writer.write("### " + entry.getKey() + "\n\n"); + + // Count sentiments per department + Map deptSentimentCounts = entry.getValue().stream() + .collect(Collectors.groupingBy(FeedbackEntry::getSentiment, Collectors.counting())); + + for (Map.Entry 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; } + } +} \ No newline at end of file diff --git a/FeedbackService/src/main/java/com/retailstore/feedback/controller/FeedbackController.java b/FeedbackService/src/main/java/com/retailstore/feedback/controller/FeedbackController.java new file mode 100644 index 0000000..1ca643d --- /dev/null +++ b/FeedbackService/src/main/java/com/retailstore/feedback/controller/FeedbackController.java @@ -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> getFeedback() { + try { + List feedback = feedbackService.getEnhancedFeedback(); + return ResponseEntity.ok(feedback); + } catch (IOException e) { + return ResponseEntity.internalServerError().build(); + } + } +} \ No newline at end of file diff --git a/FeedbackService/src/main/java/com/retailstore/feedback/model/EnhancedFeedback.java b/FeedbackService/src/main/java/com/retailstore/feedback/model/EnhancedFeedback.java new file mode 100644 index 0000000..b94e671 --- /dev/null +++ b/FeedbackService/src/main/java/com/retailstore/feedback/model/EnhancedFeedback.java @@ -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; + } +} \ No newline at end of file diff --git a/FeedbackService/src/main/java/com/retailstore/feedback/model/FeedbackEntry.java b/FeedbackService/src/main/java/com/retailstore/feedback/model/FeedbackEntry.java new file mode 100644 index 0000000..1a9d5a4 --- /dev/null +++ b/FeedbackService/src/main/java/com/retailstore/feedback/model/FeedbackEntry.java @@ -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; } +} \ No newline at end of file diff --git a/FeedbackService/src/main/java/com/retailstore/feedback/model/FeedbackSummary.java b/FeedbackService/src/main/java/com/retailstore/feedback/model/FeedbackSummary.java new file mode 100644 index 0000000..8a445bd --- /dev/null +++ b/FeedbackService/src/main/java/com/retailstore/feedback/model/FeedbackSummary.java @@ -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 sentimentCounts; + private Map categoryCounts; + private Map departmentCounts; + private List recentFeedback; + + // Default constructor + public FeedbackSummary() {} + + // Getters and setters + public int getTotalFeedback() { return totalFeedback; } + public void setTotalFeedback(int totalFeedback) { + this.totalFeedback = totalFeedback; + } + + public Map getSentimentCounts() { return sentimentCounts; } + public void setSentimentCounts(Map sentimentCounts) { + this.sentimentCounts = sentimentCounts; + } + + public Map getCategoryCounts() { return categoryCounts; } + public void setCategoryCounts(Map categoryCounts) { + this.categoryCounts = categoryCounts; + } + + public Map getDepartmentCounts() { return departmentCounts; } + public void setDepartmentCounts(Map departmentCounts) { + this.departmentCounts = departmentCounts; + } + + public List getRecentFeedback() { return recentFeedback; } + public void setRecentFeedback(List recentFeedback) { + this.recentFeedback = recentFeedback; + } +} \ No newline at end of file diff --git a/FeedbackService/src/main/java/com/retailstore/feedback/service/FeedbackService.java b/FeedbackService/src/main/java/com/retailstore/feedback/service/FeedbackService.java new file mode 100644 index 0000000..480f0c6 --- /dev/null +++ b/FeedbackService/src/main/java/com/retailstore/feedback/service/FeedbackService.java @@ -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 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 readFeedbackData() throws IOException { + List 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 getEnhancedFeedback() throws IOException { + // Return cached data if available + if (enhancedFeedbackCache != null) { + return enhancedFeedbackCache; + } + + List entries = readFeedbackData(); + List 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 allFeedback = getEnhancedFeedback(); + FeedbackSummary summary = new FeedbackSummary(); + + // Set total feedback count + summary.setTotalFeedback(allFeedback.size()); + + // Count sentiments + Map 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 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 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 recentFeedback = allFeedback.stream() + .sorted(Comparator.comparing(EnhancedFeedback::getId).reversed()) + .limit(5) + .collect(Collectors.toList()); + summary.setRecentFeedback(recentFeedback); + + return summary; + } +} \ No newline at end of file diff --git a/FeedbackService/src/main/java/com/retailstore/feedback/service/GeminiService.java b/FeedbackService/src/main/java/com/retailstore/feedback/service/GeminiService.java new file mode 100644 index 0000000..f550ebe --- /dev/null +++ b/FeedbackService/src/main/java/com/retailstore/feedback/service/GeminiService.java @@ -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 request = RequestEntity.post(serviceUri).body(requestBody); + + // Send the request + ResponseEntity 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); + } +} \ No newline at end of file diff --git a/FeedbackService/src/main/resources/application.properties b/FeedbackService/src/main/resources/application.properties new file mode 100644 index 0000000..c63b54c --- /dev/null +++ b/FeedbackService/src/main/resources/application.properties @@ -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 \ No newline at end of file diff --git a/FeedbackService/src/main/resources/logback.xml b/FeedbackService/src/main/resources/logback.xml new file mode 100644 index 0000000..9d7fec0 --- /dev/null +++ b/FeedbackService/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + \ No newline at end of file diff --git a/FeedbackService/src/main/resources/templates/dashboard.html b/FeedbackService/src/main/resources/templates/dashboard.html new file mode 100644 index 0000000..995162c --- /dev/null +++ b/FeedbackService/src/main/resources/templates/dashboard.html @@ -0,0 +1,327 @@ + + + + + + Customer Feedback Dashboard + + + + + + + +
+ +
+
+
+
+
Feedback Summary
+

Total Feedback: 0

+
+
Sentiment Distribution
+
+ +
+
+
+
+
+
+
+
+
Category Distribution
+
+ +
+
+
+
+
+
+
+
Department Distribution
+
+ +
+
+
+
+
+ + +
+
+
+
+
Recent Feedback
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
IDCustomerDepartmentDateCommentSentimentCategoryInsight
1John DoeElectronics2023-02-15Great service! + + POSITIVE + + Customer Service +
+ Recognize staff for excellent service. +
+
+
+
+
+
+
+ + +
+
+
+
+
All Feedback
+ +
+
+
+

Loading all feedback...

+
+
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/FeedbackService/src/main/resources/templates/error.html b/FeedbackService/src/main/resources/templates/error.html new file mode 100644 index 0000000..2b50aa4 --- /dev/null +++ b/FeedbackService/src/main/resources/templates/error.html @@ -0,0 +1,27 @@ + + + + + + Error - Customer Feedback Dashboard + + + +
+
+
+
+
+

Error

+
+
+

An error occurred.

+

Please ensure that the sentiment analysis has been run and the output file exists.

+ Try Again +
+
+
+
+
+ + \ No newline at end of file diff --git a/FeedbackService/src/test/java/com/retailstore/feedback/FeedbackServiceApplicationTests.java b/FeedbackService/src/test/java/com/retailstore/feedback/FeedbackServiceApplicationTests.java new file mode 100644 index 0000000..7e23fff --- /dev/null +++ b/FeedbackService/src/test/java/com/retailstore/feedback/FeedbackServiceApplicationTests.java @@ -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() { + } + +} diff --git a/FeedbackService/store_feedback.txt b/FeedbackService/store_feedback.txt new file mode 100644 index 0000000..303d632 --- /dev/null +++ b/FeedbackService/store_feedback.txt @@ -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 \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 489d279..3dc33b2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,4 +10,5 @@ include 'SoftwareDevChatbot' include 'RegressionPredictionLab' include 'SentimentAnalysisLab' include 'ImageRecognitionLab' -include 'CustomerSupportChatBot' \ No newline at end of file +include 'CustomerSupportChatBot' +include 'FeedbackService' \ No newline at end of file