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