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
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:
@@ -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; } = [];
|
||||
|
||||
@@ -14,5 +14,7 @@ namespace Mindforge.API.Services.Interfaces
|
||||
Task<IReadOnlyList<FlashcardLibrarySummary>> GetLibrariesAsync();
|
||||
Task<FlashcardLibraryDetails?> GetLibraryByIdAsync(long libraryId);
|
||||
Task<IReadOnlyList<FlashcardCard>> GetCardsForLibrariesAsync(IReadOnlyList<long> libraryIds);
|
||||
Task<IReadOnlyList<FlashcardCardWithLibrary>> GetAllCardsWithLibraryAsync();
|
||||
Task RecordReviewAnswerAsync(long cardId, bool correct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,5 +9,7 @@ namespace Mindforge.API.Services.Interfaces
|
||||
Task<IReadOnlyList<FlashcardLibrarySummary>> GetLibrariesAsync();
|
||||
Task<FlashcardLibraryDetails?> GetLibraryByIdAsync(long libraryId);
|
||||
Task<IReadOnlyList<FlashcardCard>> BuildReviewSessionAsync(FlashcardReviewSessionRequest request);
|
||||
Task<FlashcardRagDashboard> GetRagStatusAsync();
|
||||
Task RecordReviewAnswerAsync(long cardId, bool correct);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user