using GermanApp.Domain.Interfaces; using Microsoft.Extensions.Logging; namespace GermanApp.Application.Services; /// /// Application service for providing feedback on user writing using Mistral AI. /// This is part of the Application layer. /// public class WritingFeedbackService { private readonly IMistralService _mistralService; private readonly ILogger _logger; /// /// Creates a new WritingFeedbackService. /// /// The Mistral text generation service /// Logger for service operations public WritingFeedbackService( IMistralService mistralService, ILogger logger) { _mistralService = mistralService; _logger = logger; } /// /// Provides feedback on user's German writing. /// /// The user's German text to evaluate /// The CEFR level of the user /// Optional custom prompt for specific feedback requests /// Cancellation token /// Feedback text in English public virtual async Task ProvideFeedbackAsync( string userText, string level, string? customPrompt = null, CancellationToken cancellationToken = default) { _logger.LogInformation( "Providing writing feedback: Level={Level}, TextLength={Length}", level, userText?.Length ?? 0); try { var feedback = await _mistralService.GenerateWritingFeedbackAsync( userText, level, customPrompt, cancellationToken); // Validate the feedback ValidateFeedback(feedback); _logger.LogInformation("Writing feedback generated successfully"); return feedback; } catch (Exception ex) { _logger.LogError(ex, "Failed to generate writing feedback"); throw new AiServiceException( "Failed to generate writing feedback: " + ex.Message, AiErrorCode.Temporary); } } /// /// Provides structured feedback with specific categories. /// /// The user's German text to evaluate /// The CEFR level of the user /// Cancellation token /// Structured feedback with grammar, suggestions, and encouragement public virtual async Task ProvideStructuredFeedbackAsync( string userText, string level, CancellationToken cancellationToken = default) { _logger.LogInformation( "Providing structured writing feedback: Level={Level}, TextLength={Length}", level, userText?.Length ?? 0); try { // Generate feedback using the service var feedbackText = await ProvideFeedbackAsync(userText, level, null, cancellationToken); // Parse the feedback into a structured format return ParseFeedback(feedbackText, userText); } catch (Exception ex) { _logger.LogError(ex, "Failed to generate structured writing feedback"); throw; } } /// /// Checks grammar in user's text. /// /// The user's German text /// The CEFR level /// Cancellation token /// List of grammar corrections public virtual async Task> CheckGrammarAsync( string userText, string level, CancellationToken cancellationToken = default) { _logger.LogInformation( "Checking grammar: Level={Level}, TextLength={Length}", level, userText?.Length ?? 0); try { var feedback = await ProvideFeedbackAsync(userText, level, null, cancellationToken); return ExtractGrammarCorrections(feedback); } catch (Exception ex) { _logger.LogError(ex, "Failed to check grammar"); throw; } } /// /// Suggests improvements for user's writing. /// /// The user's German text /// The CEFR level /// Cancellation token /// List of improvement suggestions public virtual async Task> SuggestImprovementsAsync( string userText, string level, CancellationToken cancellationToken = default) { _logger.LogInformation( "Suggesting improvements: Level={Level}, TextLength={Length}", level, userText?.Length ?? 0); try { var feedback = await ProvideFeedbackAsync(userText, level, null, cancellationToken); return ExtractImprovementSuggestions(feedback); } catch (Exception ex) { _logger.LogError(ex, "Failed to suggest improvements"); throw; } } /// /// Validates the generated feedback. /// /// The feedback text /// Thrown when feedback is invalid private void ValidateFeedback(string feedback) { if (string.IsNullOrWhiteSpace(feedback)) { _logger.LogError("Generated feedback is empty"); throw new InvalidOperationException("Generated feedback is empty"); } // Check minimum feedback length const int minFeedbackLength = 50; if (feedback.Length < minFeedbackLength) { _logger.LogWarning( "Generated feedback is too short: {Length} characters (min: {Min})", feedback.Length, minFeedbackLength); // Don't throw - just log warning } } /// /// Parses feedback text into a structured format. /// /// The raw feedback text /// The original user text /// Structured feedback object private WritingFeedback ParseFeedback(string feedbackText, string originalText) { // This is a simple parser that extracts information from the feedback // In a real implementation, you might use more sophisticated parsing // or ask the AI to return structured data (JSON) var feedback = new WritingFeedback { OriginalText = originalText, FeedbackText = feedbackText }; // Try to extract grammar corrections feedback.GrammarCorrections = ExtractGrammarCorrections(feedbackText); // Try to extract improvement suggestions feedback.ImprovementSuggestions = ExtractImprovementSuggestions(feedbackText); // Try to extract encouragement feedback.Encouragement = ExtractEncouragement(feedbackText); return feedback; } /// /// Extracts grammar corrections from feedback text. /// private IReadOnlyList ExtractGrammarCorrections(string feedbackText) { var corrections = new List(); var lines = feedbackText.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { // Look for patterns like "Incorrect: X -> Correct: Y" if (line.Contains("->") || line.Contains("→") || line.Contains("Incorrect") || line.Contains("Correction")) { // This is a simplified extraction - in production, use proper parsing corrections.Add(new GrammarCorrection { Issue = "Grammar issue found", Original = "[text]", Corrected = "[corrected]", Explanation = line }); } } return corrections; } /// /// Extracts improvement suggestions from feedback text. /// private IReadOnlyList ExtractImprovementSuggestions(string feedbackText) { var suggestions = new List(); var lines = feedbackText.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { if (line.Contains("suggest") || line.Contains("Suggestion") || line.Contains("improve") || line.Contains("better")) { suggestions.Add(line); } } return suggestions.Count > 0 ? suggestions : new List { "No specific suggestions extracted" }; } /// /// Extracts encouragement from feedback text. /// private string ExtractEncouragement(string feedbackText) { var lines = feedbackText.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { if (line.Contains("good") || line.Contains("great") || line.Contains("excellent") || line.Contains("keep") || line.Contains("Encouragement")) { return line; } } return "Keep practicing!"; } /// /// Tests the writing feedback service. /// /// Cancellation token /// True if service is working, false otherwise public virtual async Task TestServiceAsync(CancellationToken cancellationToken = default) { try { // Test with simple text var feedback = await ProvideFeedbackAsync( "Ich heisse Anna.", "A1", null, cancellationToken); return !string.IsNullOrWhiteSpace(feedback); } catch (Exception ex) { _logger.LogError(ex, "Writing feedback service test failed"); return false; } } } /// /// Structured feedback for writing. /// public class WritingFeedback { /// /// The original user text. /// public string OriginalText { get; set; } = string.Empty; /// /// The full feedback text. /// public string FeedbackText { get; set; } = string.Empty; /// /// List of grammar corrections. /// public IReadOnlyList GrammarCorrections { get; set; } = new List(); /// /// List of improvement suggestions. /// public IReadOnlyList ImprovementSuggestions { get; set; } = new List(); /// /// Encouragement message. /// public string Encouragement { get; set; } = string.Empty; } /// /// Represents a single grammar correction. /// public class GrammarCorrection { /// /// The type of grammar issue. /// public string Issue { get; set; } = string.Empty; /// /// The original text with the issue. /// public string Original { get; set; } = string.Empty; /// /// The corrected text. /// public string Corrected { get; set; } = string.Empty; /// /// Explanation of the correction. /// public string Explanation { get; set; } = string.Empty; }