timed revision
All checks were successful
Mindforge API Build and Deploy / Build Mindforge API Image (push) Successful in 3m58s
Mindforge API Build and Deploy / Deploy Mindforge API (internal) (push) Successful in 37s
Mindforge Web Build and Deploy (internal) / Build Mindforge Web Image (push) Successful in 5m19s
Mindforge Web Build and Deploy (internal) / Deploy Mindforge Web (internal) (push) Successful in 11s

This commit is contained in:
2026-06-01 19:08:48 -03:00
parent b80d28f671
commit f03bcc40e3
14 changed files with 1138 additions and 14 deletions

View File

@@ -98,6 +98,61 @@ namespace Mindforge.API.Services
return await _flashcardRepository.GetCardsForLibrariesAsync(libraryIds);
}
public async Task<FlashcardRagDashboard> GetRagStatusAsync()
{
var now = DateTime.UtcNow;
var cards = await _flashcardRepository.GetAllCardsWithLibraryAsync();
var ragCards = cards
.Select(card => BuildRagCard(card, now))
.ToList();
var subjectGroups = ragCards
.GroupBy(card => card.Subject, StringComparer.OrdinalIgnoreCase)
.OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase)
.Select(subjectGroup =>
{
var subSubjectGroups = subjectGroup
.GroupBy(card => card.SubSubject, StringComparer.OrdinalIgnoreCase)
.OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase)
.Select(subSubjectGroup =>
{
var subSubjectCards = subSubjectGroup.ToList();
return new FlashcardRagSubSubjectGroup
{
SubSubject = subSubjectGroup.Key,
Summary = BuildSummary(subSubjectCards),
Cards = subSubjectCards
};
})
.ToList();
return new FlashcardRagSubjectGroup
{
Subject = subjectGroup.Key,
Summary = BuildSummary(subjectGroup),
SubSubjects = subSubjectGroups
};
})
.ToList();
return new FlashcardRagDashboard
{
GeneratedAt = now,
Subjects = subjectGroups
};
}
public async Task RecordReviewAnswerAsync(long cardId, bool correct)
{
if (cardId <= 0)
{
throw new UserException("Card de revisao invalido.");
}
await _flashcardRepository.RecordReviewAnswerAsync(cardId, correct);
}
private async Task<IReadOnlyList<FlashcardDraftCard>> GenerateCardsForFileAsync(
string filePath,
string fileContent,
@@ -228,6 +283,120 @@ namespace Mindforge.API.Services
return segments[0];
}
private static string ExtractSubSubject(string filePath)
{
var normalized = filePath.Replace('\\', '/');
var segments = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (segments.Length <= 2)
{
return "Geral";
}
var concursosIndex = Array.FindIndex(
segments,
segment => segment.Equals("concursos", StringComparison.OrdinalIgnoreCase)
|| segment.Equals("concurso", StringComparison.OrdinalIgnoreCase));
var subjectIndex = concursosIndex >= 0 && concursosIndex + 1 < segments.Length
? concursosIndex + 1
: 0;
var subSubjectStart = subjectIndex + 1;
var subSubjectEnd = segments.Length - 1;
if (subSubjectStart >= subSubjectEnd)
{
return "Geral";
}
return string.Join(" / ", segments[subSubjectStart..subSubjectEnd]);
}
private static FlashcardRagCard BuildRagCard(FlashcardCardWithLibrary card, DateTime referenceTime)
{
var subject = string.IsNullOrWhiteSpace(card.Subject)
? ExtractSubject(card.FilePath)
: card.Subject;
var subSubject = ExtractSubSubject(card.FilePath);
var totalAnswers = card.CorrectCount + card.IncorrectCount;
var performanceRate = totalAnswers == 0
? 0
: (double)card.CorrectCount / totalAnswers;
return new FlashcardRagCard
{
CardId = card.Id,
LibraryId = card.LibraryId,
FileName = card.FileName,
Subject = subject,
SubSubject = subSubject,
Front = card.Front,
Back = card.Back,
CorrectCount = card.CorrectCount,
IncorrectCount = card.IncorrectCount,
TotalAnswers = totalAnswers,
PerformanceRate = performanceRate,
LastReviewedAt = card.LastReviewedAt,
RagStatus = DetermineRagStatus(card.LastReviewedAt, performanceRate, referenceTime)
};
}
private static string DetermineRagStatus(DateTime? lastReviewedAt, double performanceRate, DateTime referenceTime)
{
if (!lastReviewedAt.HasValue)
{
return "Grey";
}
var lastReviewUtc = lastReviewedAt.Value.ToUniversalTime();
var elapsedDays = (referenceTime.Date - lastReviewUtc.Date).TotalDays;
if (elapsedDays >= 40 || performanceRate < 0.4)
{
return "Red";
}
if (elapsedDays >= 30 || performanceRate <= 0.6)
{
return "Amber";
}
if (elapsedDays < 30 && performanceRate > 0.6)
{
return "Green";
}
return "Amber";
}
private static FlashcardRagSummary BuildSummary(IEnumerable<FlashcardRagCard> cards)
{
var cardList = cards.ToList();
var greenCount = cardList.Count(card => card.RagStatus == "Green");
var amberCount = cardList.Count(card => card.RagStatus == "Amber");
var redCount = cardList.Count(card => card.RagStatus == "Red");
var greyCount = cardList.Count(card => card.RagStatus == "Grey");
var activeCount = greenCount + amberCount + redCount;
var greenPercentage = activeCount == 0
? 0
: Math.Round((double)greenCount / activeCount * 100, 2);
var attentionPercentage = activeCount == 0
? 0
: Math.Round((double)(amberCount + redCount) / activeCount * 100, 2);
return new FlashcardRagSummary
{
GreenCount = greenCount,
AmberCount = amberCount,
RedCount = redCount,
GreyCount = greyCount,
ActiveCount = activeCount,
GreenPercentage = greenPercentage,
AttentionPercentage = attentionPercentage
};
}
private class FlashcardJsonPayload
{
public List<FlashcardJsonCard> Flashcards { get; set; } = [];