From 48b015e0954ff4728cbb00d4383c25f2ca9e6c74 Mon Sep 17 00:00:00 2001 From: chloe Date: Thu, 12 Mar 2026 20:17:25 +0000 Subject: [PATCH] Stronger prompting, UI update --- .../Controllers/RecipeController.cs | 10 ++ Seasoned.Backend/DTOs/ChatRequestDto.cs | 6 ++ .../DTOs/ChefConsultResponseDto.cs | 7 ++ Seasoned.Backend/DTOs/RecipeResponseDto.cs | 1 + Seasoned.Backend/Services/IRecipeService.cs | 1 + Seasoned.Backend/Services/RecipeService.cs | 55 +++++++++++ .../app/assets/css/app-theme.css | 60 ++++++++++++ Seasoned.Frontend/app/pages/gallery.vue | 1 + Seasoned.Frontend/app/pages/index.vue | 96 ++++++++++++++++++- Seasoned.Frontend/app/pages/login.vue | 1 + 10 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 Seasoned.Backend/DTOs/ChatRequestDto.cs create mode 100644 Seasoned.Backend/DTOs/ChefConsultResponseDto.cs diff --git a/Seasoned.Backend/Controllers/RecipeController.cs b/Seasoned.Backend/Controllers/RecipeController.cs index b2dc399..fb3108d 100644 --- a/Seasoned.Backend/Controllers/RecipeController.cs +++ b/Seasoned.Backend/Controllers/RecipeController.cs @@ -97,4 +97,14 @@ public class RecipeController : ControllerBase return Ok(myRecipes); } + + [HttpPost("consult")] + public async Task Consult([FromBody] ChatRequestDto request) + { + if (string.IsNullOrWhiteSpace(request.Prompt)) + return BadRequest("The Chef needs a prompt."); + + var result = await _recipeService.ConsultChefAsync(request.Prompt); + return Ok(result); + } } \ No newline at end of file diff --git a/Seasoned.Backend/DTOs/ChatRequestDto.cs b/Seasoned.Backend/DTOs/ChatRequestDto.cs new file mode 100644 index 0000000..a5fafcf --- /dev/null +++ b/Seasoned.Backend/DTOs/ChatRequestDto.cs @@ -0,0 +1,6 @@ +namespace Seasoned.Backend.DTOs; + +public class ChatRequestDto +{ + public string Prompt { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Seasoned.Backend/DTOs/ChefConsultResponseDto.cs b/Seasoned.Backend/DTOs/ChefConsultResponseDto.cs new file mode 100644 index 0000000..fa9da30 --- /dev/null +++ b/Seasoned.Backend/DTOs/ChefConsultResponseDto.cs @@ -0,0 +1,7 @@ +namespace Seasoned.Backend.DTOs; + +public class ChefConsultResponseDto +{ + public string Reply { get; set; } = string.Empty; + public RecipeResponseDto? Recipe { get; set; } +} \ No newline at end of file diff --git a/Seasoned.Backend/DTOs/RecipeResponseDto.cs b/Seasoned.Backend/DTOs/RecipeResponseDto.cs index dd81672..aa10a42 100644 --- a/Seasoned.Backend/DTOs/RecipeResponseDto.cs +++ b/Seasoned.Backend/DTOs/RecipeResponseDto.cs @@ -6,4 +6,5 @@ public class RecipeResponseDto public string Icon { get; set; } = "mdi-silverware-fork-knife"; public List Ingredients { get; set; } = new(); public List Instructions { get; set; } = new(); + } \ No newline at end of file diff --git a/Seasoned.Backend/Services/IRecipeService.cs b/Seasoned.Backend/Services/IRecipeService.cs index 1095b7c..bc90db6 100644 --- a/Seasoned.Backend/Services/IRecipeService.cs +++ b/Seasoned.Backend/Services/IRecipeService.cs @@ -5,4 +5,5 @@ namespace Seasoned.Backend.Services; public interface IRecipeService { Task ParseRecipeImageAsync(IFormFile image); + Task ConsultChefAsync(string userPrompt); } \ No newline at end of file diff --git a/Seasoned.Backend/Services/RecipeService.cs b/Seasoned.Backend/Services/RecipeService.cs index 64eecd5..75f648b 100644 --- a/Seasoned.Backend/Services/RecipeService.cs +++ b/Seasoned.Backend/Services/RecipeService.cs @@ -84,4 +84,59 @@ public class RecipeService : IRecipeService return new RecipeResponseDto { Title = "Parsing Error" }; } } + + public async Task ConsultChefAsync(string userPrompt) + { + var googleAI = new GoogleAI(_apiKey); + var model = googleAI.GenerativeModel("gemini-3.1-flash-lite-preview"); + + var systemPrompt = @"You are the 'Seasoned' Head Chef, a master of real-world culinary arts. + You operate a professional kitchen and only provide advice that can be used in a real kitchen. + + STRICT CONTENT RULES: + 1. REAL FOOD ONLY: You specialize in real-world ingredients and techniques. + 2. CELEBRITY CHEFS: You can provide recipes from real chefs like Gordon Ramsay or Julia Child. + 3. FICTIONAL FOOD TRANSLATION: If a user asks for food from a game (like a Skyrim Sweetroll) or movie, + do NOT give game mechanics. Instead, provide a REAL-WORLD recipe that recreates that item. + In your 'reply', treat it like a fun culinary challenge. + 4. REFUSAL POLICY: If the user asks about non-food topics (video game strategies, tech support, politics, or 'Minecraft crafting grids'), + politely refuse. Stay in character: 'The Chef's Grimoire is for spices, not spells' or 'I deal in pans, not pixels.' + + RESPONSE FORMAT: + You MUST return ONLY a raw JSON object with these keys: + { + ""reply"": ""A friendly, thematic response from the Chef."", + ""recipe"": { + ""title"": ""string"", + ""icon"": ""string (must be a valid mdi- icon name)"", + ""ingredients"": [""string"", ""string""], + ""instructions"": [""string"", ""string""] + } + } + Note: Set the 'recipe' object to null if you are only chatting or refusing a non-food prompt."; + + var fullPrompt = $"{systemPrompt}\n\nUser Question: {userPrompt}"; + + var generationConfig = new GenerationConfig { + ResponseMimeType = "application/json", + Temperature = 0.7f + }; + + var request = new GenerateContentRequest(fullPrompt, generationConfig); + var response = await model.GenerateContent(request); + + try + { + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + string jsonToParse = response.Text ?? "{ \"reply\": \"The chef is speechless. Try again?\" }"; + + var result = JsonSerializer.Deserialize(jsonToParse, options); + + return result ?? new ChefConsultResponseDto { Reply = "Chef is a bit confused!" }; + } + catch (JsonException) + { + return new ChefConsultResponseDto { Reply = "The kitchen is a mess right now. Try again?" }; + } + } } \ No newline at end of file diff --git a/Seasoned.Frontend/app/assets/css/app-theme.css b/Seasoned.Frontend/app/assets/css/app-theme.css index 4073c7d..56961ff 100644 --- a/Seasoned.Frontend/app/assets/css/app-theme.css +++ b/Seasoned.Frontend/app/assets/css/app-theme.css @@ -264,4 +264,64 @@ font-family: 'Crimson Text', serif; font-size: 1.05rem; letter-spacing: 0.02em; +} + +.chat-container { + background: rgba(244, 237, 225, 0.6); + border: 1px dashed #d1c7b7; + border-radius: 8px; +} + +.chat-container { + width: 100%; + background-color: rgba(62, 42, 20, 0.03) !important; + border: 2px dashed #8c857b; + border-radius: 12px; + padding: 20px; + transition: all 0.3s ease; + display: flex; + flex-direction: column; +} + +.chat-container:focus-within { + background-color: rgba(85, 107, 47, 0.05) !important; + border-color: #556b2f; +} + +.chat-input .v-field__input { + color: #5d4037 !important; + font-family: 'Crimson Text', serif; + font-size: 1.1rem; +} + +.chat-input .v-field__input::placeholder { + color: #8c7e6a !important; + opacity: 1; +} + +.chat-placeholder { + font-style: italic; + color: #8c7e6a; + text-align: center; + padding: 20px; +} + +.message { + margin-bottom: 10px; + padding: 8px 12px; + border-radius: 4px; +} + +.message.user { + background: rgba(93, 64, 55, 0.1); + text-align: right; + color: #5d4037; + font-weight: bold; +} + +.message.assistant { + background: transparent; + text-align: left; + color: #2c3e50; + border-left: 3px solid #556b2f; } \ No newline at end of file diff --git a/Seasoned.Frontend/app/pages/gallery.vue b/Seasoned.Frontend/app/pages/gallery.vue index 91b3fe4..d47e159 100644 --- a/Seasoned.Frontend/app/pages/gallery.vue +++ b/Seasoned.Frontend/app/pages/gallery.vue @@ -191,6 +191,7 @@ \ No newline at end of file diff --git a/Seasoned.Frontend/app/pages/login.vue b/Seasoned.Frontend/app/pages/login.vue index d14a010..44cf736 100644 --- a/Seasoned.Frontend/app/pages/login.vue +++ b/Seasoned.Frontend/app/pages/login.vue @@ -81,6 +81,7 @@