From 8327d920ac854ffe82f6321bb1f7ae3221c00e84 Mon Sep 17 00:00:00 2001 From: anonymous-dyce Date: Sat, 20 Dec 2025 19:01:04 -0800 Subject: [PATCH 1/5] Created files for Gemini to handle user preferences from frontend and respond with recommendations --- .../UserPreferencesAIController.java | 181 ++++++++++++++++++ .../UserPreferencesAIRepository.java | 10 + .../UserPrefernecesAI.java | 38 ++++ 3 files changed, 229 insertions(+) create mode 100644 src/main/java/com/open/spring/mvc/geminiUserPreferences/UserPreferencesAIController.java create mode 100644 src/main/java/com/open/spring/mvc/geminiUserPreferences/UserPreferencesAIRepository.java create mode 100644 src/main/java/com/open/spring/mvc/geminiUserPreferences/UserPrefernecesAI.java diff --git a/src/main/java/com/open/spring/mvc/geminiUserPreferences/UserPreferencesAIController.java b/src/main/java/com/open/spring/mvc/geminiUserPreferences/UserPreferencesAIController.java new file mode 100644 index 00000000..3acae9e7 --- /dev/null +++ b/src/main/java/com/open/spring/mvc/geminiUserPreferences/UserPreferencesAIController.java @@ -0,0 +1,181 @@ +package com.open.spring.mvc.geminiUserPreferences; + +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; +import io.github.cdimascio.dotenv.Dotenv; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@RestController +@RequestMapping("/api/upai") +public class UserPreferencesAIController { + + @Autowired + private UserPreferencesAIRepository geminiRepository; + + private final Dotenv dotenv = Dotenv.load(); + private final String geminiApiKey = dotenv.get("GEMINI_API_KEY"); + private final String geminiApiUrl = dotenv.get("GEMINI_API_URL"); + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ThemeRequest { + // The free-form description of the user's theme preferences + private String prompt; + } + + // POST - Generate a site theme recommendation from a user's preferences + // Endpoint: POST /api/upai (frontend posts to this root path) + @PostMapping("") + public ResponseEntity gradeTheme(@RequestBody ThemeRequest request) { + try { + String prompt = request.getPrompt(); + + if (prompt == null || prompt.trim().isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("error", "Missing prompt")); + } + + // Build a clear instruction for Gemini to return only a compact JSON object + String fullPrompt = String.format(""" + You are a design assistant that recommends a website theme based on a user's free-form preferences. + The user may provide adjectives like \"modern\", \"playful\", \"professional\", preferred colors, shades, tones, tints, hues, and any other hints. + Produce a single JSON object — and ONLY the JSON object — with the following keys: + - backgroundColor: a hex color like \"#RRGGBB\" for the page background. + - buttonColor: a hex color for primary buttons. + - selectionColor: a hex color for selection/highlight states. + - textColor: a hex color for primary text. + - fontFamily: one of [\"inter\", \"open sans\", \"roboto\", \"lato\", \"montserrat\", \"georgia serif\", \"source code pro\"] + - suggestions: a very short string (1-2 sentences) describing why these choices fit the user's request. + + Use HEX color codes. If a color family is requested (e.g., \"pastel blues\"), pick a representative HEX. + Keep the JSON compact (no extra explanation). If you cannot decide between two fonts, pick the most suitable one from the allowed list. + + User preferences: %s + """, prompt.replace("\"", "\\\"").replace("\n", " ")); + + String jsonPayload = String.format(""" + { + "contents": [{ + "parts": [{ + "text": "%s" + }] + }] + } + """, fullPrompt.replace("\"", "\\\"").replace("\n", "\\n")); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity httpRequest = new HttpEntity<>(jsonPayload, headers); + String fullUrl = geminiApiUrl + "?key=" + geminiApiKey; + + RestTemplate restTemplate = new RestTemplate(); + String geminiBody = null; + String extractedText = null; + try { + ResponseEntity response = restTemplate.exchange(fullUrl, HttpMethod.POST, httpRequest, String.class); + geminiBody = response.getBody(); + extractedText = extractGradingText(geminiBody); + } catch (org.springframework.web.client.HttpClientErrorException.TooManyRequests e) { + // Provider quota exhausted. Log and provide a deterministic fallback so frontend can continue testing. + e.printStackTrace(); + String fallback = "{\"backgroundColor\":\"#FFFFFF\",\"buttonColor\":\"#1F8EF1\",\"selectionColor\":\"#DDEEFF\",\"textColor\":\"#111827\",\"fontFamily\":\"inter\",\"suggestions\":\"Failed to generate theme due to quota limit. When you click on 'Apply to Form', a fallback theme will be implemented.\"}"; + extractedText = fallback; + geminiBody = ""; + } + + // Try to parse extractedText as JSON; if it's wrapped in text, try to find the JSON substring + ObjectMapper mapper = new ObjectMapper(); + Object responseObject = null; + try { + responseObject = mapper.readValue(extractedText, Map.class); + } catch (Exception e) { + // Attempt to locate first '{' and last '}' and parse substring + int first = extractedText.indexOf('{'); + int last = extractedText.lastIndexOf('}'); + if (first >= 0 && last > first) { + String maybeJson = extractedText.substring(first, last + 1); + try { + responseObject = mapper.readValue(maybeJson, Map.class); + // Use the cleaned JSON text as extractedText + extractedText = maybeJson; + } catch (Exception ex) { + responseObject = extractedText; + } + } else { + responseObject = extractedText; + } + } + + // Persist the user's prompt and the raw recommendation text + UserPrefernecesAI record = new UserPrefernecesAI(prompt, ""); + record.setGradingResult(extractedText); + UserPrefernecesAI saved = geminiRepository.save(record); + + return ResponseEntity.ok(Map.of( + "status", "success", + "id", saved.getId(), + "prompt", saved.getQuestion(), + "response", responseObject + )); + + } catch (HttpClientErrorException.TooManyRequests e) { + return ResponseEntity.status(429).body(Map.of("error", "Gemini quota exceeded. Please try again later.")); + } catch (Exception e) { + e.printStackTrace(); + return ResponseEntity.status(500).body(Map.of("error", "Internal server error: " + e.getMessage())); + } + } + + // GET - Fetch all grading results (no user filtering) + @GetMapping("/grades") + public ResponseEntity getGrades() { + List results = geminiRepository.findAll(); + return ResponseEntity.ok(Map.of( + "count", results.size(), + "results", results + )); + } + + // Helper method to extract grading text from Gemini API response + private String extractGradingText(String jsonResponse) { + try { + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(jsonResponse); + + // Navigate: candidates[0].content.parts[0].text + JsonNode textNode = root.path("candidates") + .get(0) + .path("content") + .path("parts") + .get(0) + .path("text"); + + if (textNode.isTextual()) { + return textNode.asText(); + } + } catch (Exception e) { + e.printStackTrace(); + } + + return "Error parsing grading result"; + } +} diff --git a/src/main/java/com/open/spring/mvc/geminiUserPreferences/UserPreferencesAIRepository.java b/src/main/java/com/open/spring/mvc/geminiUserPreferences/UserPreferencesAIRepository.java new file mode 100644 index 00000000..0412f6a2 --- /dev/null +++ b/src/main/java/com/open/spring/mvc/geminiUserPreferences/UserPreferencesAIRepository.java @@ -0,0 +1,10 @@ +package com.open.spring.mvc.geminiUserPreferences; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserPreferencesAIRepository extends JpaRepository { + // This interface is intentionally left blank. + // Default JPA methods are used for database operations. +} diff --git a/src/main/java/com/open/spring/mvc/geminiUserPreferences/UserPrefernecesAI.java b/src/main/java/com/open/spring/mvc/geminiUserPreferences/UserPrefernecesAI.java new file mode 100644 index 00000000..dd404e5a --- /dev/null +++ b/src/main/java/com/open/spring/mvc/geminiUserPreferences/UserPrefernecesAI.java @@ -0,0 +1,38 @@ +package com.open.spring.mvc.geminiUserPreferences; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class UserPrefernecesAI { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + @Column(nullable = false, columnDefinition = "TEXT") + private String question; + + @Column(nullable = false, columnDefinition = "TEXT") + private String answer; + + @Column(columnDefinition = "TEXT") + private String gradingResult; + + @Column(nullable = false, updatable = false) + private Long createdAt; + + public UserPrefernecesAI(String question, String answer) { + this.question = question; + this.answer = answer; + this.createdAt = System.currentTimeMillis(); + } +} From 1dd5803ea18af6b0732bd2569ad0a5d73e115bc4 Mon Sep 17 00:00:00 2001 From: anonymous-dyce Date: Sat, 20 Dec 2025 19:02:11 -0800 Subject: [PATCH 2/5] bypassed authentication for user preferences api endpoint --- src/main/java/com/open/spring/security/SecurityConfig.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/com/open/spring/security/SecurityConfig.java b/src/main/java/com/open/spring/security/SecurityConfig.java index bae7d849..3cbdfb6f 100644 --- a/src/main/java/com/open/spring/security/SecurityConfig.java +++ b/src/main/java/com/open/spring/security/SecurityConfig.java @@ -92,6 +92,8 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce .requestMatchers(HttpMethod.DELETE, "/api/synergy/saigai/").hasAnyAuthority("ROLE_STUDENT", "ROLE_TEACHER", "ROLE_ADMIN") // Teacher and admin access for other POST operations .requestMatchers(HttpMethod.POST, "/api/synergy/**").hasAnyAuthority("ROLE_TEACHER", "ROLE_ADMIN") + // Allow unauthenticated frontend/client requests to the AI preferences endpoint + .requestMatchers(HttpMethod.POST, "/api/upai").permitAll() // Admin access for certificates + quests .requestMatchers(HttpMethod.POST, "/api/quests/**").hasAnyAuthority("ROLE_TEACHER", "ROLE_ADMIN") .requestMatchers(HttpMethod.PUT, "/api/quests/**").hasAnyAuthority("ROLE_TEACHER", "ROLE_ADMIN") @@ -109,6 +111,11 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce // These endpoints are currently wide open - consider if they should require authentication .requestMatchers("/api/analytics/**").permitAll() .requestMatchers("/api/plant/**").permitAll() + + .requestMatchers(HttpMethod.GET, "/api/styles/**").permitAll() + .requestMatchers(HttpMethod.POST, "/api/styles/**").permitAll() + .requestMatchers(HttpMethod.PUT, "/api/styles/**").authenticated() + .requestMatchers(HttpMethod.DELETE, "/api/styles/**").authenticated() .requestMatchers("/api/groups/**").permitAll() .requestMatchers("/api/grade-prediction/**").permitAll() .requestMatchers("/api/admin-evaluation/**").permitAll() From cc0df8a6a727398700bac72c313d59b6e3af9b2d Mon Sep 17 00:00:00 2001 From: anonymous-dyce Date: Sat, 20 Dec 2025 20:56:25 -0800 Subject: [PATCH 3/5] reverted changes to /api/styles in SecurityConfig --- src/main/java/com/open/spring/security/SecurityConfig.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/java/com/open/spring/security/SecurityConfig.java b/src/main/java/com/open/spring/security/SecurityConfig.java index 3cbdfb6f..70e2a9e6 100644 --- a/src/main/java/com/open/spring/security/SecurityConfig.java +++ b/src/main/java/com/open/spring/security/SecurityConfig.java @@ -111,11 +111,6 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce // These endpoints are currently wide open - consider if they should require authentication .requestMatchers("/api/analytics/**").permitAll() .requestMatchers("/api/plant/**").permitAll() - - .requestMatchers(HttpMethod.GET, "/api/styles/**").permitAll() - .requestMatchers(HttpMethod.POST, "/api/styles/**").permitAll() - .requestMatchers(HttpMethod.PUT, "/api/styles/**").authenticated() - .requestMatchers(HttpMethod.DELETE, "/api/styles/**").authenticated() .requestMatchers("/api/groups/**").permitAll() .requestMatchers("/api/grade-prediction/**").permitAll() .requestMatchers("/api/admin-evaluation/**").permitAll() From 678ad69ad115576a3db348973f2d16c0fd3fbeac Mon Sep 17 00:00:00 2001 From: anonymous-dyce Date: Mon, 12 Jan 2026 23:12:39 -0800 Subject: [PATCH 4/5] allow all get requests to /api/upai to be accessed --- src/main/java/com/open/spring/security/SecurityConfig.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/open/spring/security/SecurityConfig.java b/src/main/java/com/open/spring/security/SecurityConfig.java index 1158c4da..a6c565b2 100644 --- a/src/main/java/com/open/spring/security/SecurityConfig.java +++ b/src/main/java/com/open/spring/security/SecurityConfig.java @@ -94,6 +94,7 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce .requestMatchers(HttpMethod.POST, "/api/synergy/**").hasAnyAuthority("ROLE_TEACHER", "ROLE_ADMIN") // Allow unauthenticated frontend/client requests to the AI preferences endpoint .requestMatchers(HttpMethod.POST, "/api/upai").permitAll() + .requestMatchers(HttpMethod.GET, "/api/upai/**").permitAll() // Admin access for certificates + quests .requestMatchers(HttpMethod.POST, "/api/quests/**").hasAnyAuthority("ROLE_TEACHER", "ROLE_ADMIN") .requestMatchers(HttpMethod.PUT, "/api/quests/**").hasAnyAuthority("ROLE_TEACHER", "ROLE_ADMIN") From d502348e276f6188494a5f37f9678e62093af924 Mon Sep 17 00:00:00 2001 From: anonymous-dyce Date: Tue, 13 Jan 2026 08:28:16 -0800 Subject: [PATCH 5/5] allow all get requests to /api/gemini-frq/grade (the gemini api endpoint for FRQ grading) to be accessed --- src/main/java/com/open/spring/security/SecurityConfig.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/open/spring/security/SecurityConfig.java b/src/main/java/com/open/spring/security/SecurityConfig.java index a6c565b2..2b1fe4ee 100644 --- a/src/main/java/com/open/spring/security/SecurityConfig.java +++ b/src/main/java/com/open/spring/security/SecurityConfig.java @@ -95,6 +95,8 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce // Allow unauthenticated frontend/client requests to the AI preferences endpoint .requestMatchers(HttpMethod.POST, "/api/upai").permitAll() .requestMatchers(HttpMethod.GET, "/api/upai/**").permitAll() + .requestMatchers(HttpMethod.POST, "/api/gemini-frq/grade").permitAll() + .requestMatchers(HttpMethod.GET, "/api/gemini-frq/grade/**").permitAll() // Admin access for certificates + quests .requestMatchers(HttpMethod.POST, "/api/quests/**").hasAnyAuthority("ROLE_TEACHER", "ROLE_ADMIN") .requestMatchers(HttpMethod.PUT, "/api/quests/**").hasAnyAuthority("ROLE_TEACHER", "ROLE_ADMIN")