new flashcards
All checks were successful
Mindforge API Build and Deploy / Build Mindforge API Image (push) Successful in 4m4s
Mindforge Web Build and Deploy (internal) / Build Mindforge Web Image (push) Successful in 5m29s
Mindforge Web Build and Deploy (internal) / Deploy Mindforge Web (internal) (push) Successful in 9s
Mindforge API Build and Deploy / Deploy Mindforge API (internal) (push) Successful in 8s

This commit is contained in:
2026-05-30 11:59:19 -03:00
parent b9736293d3
commit b80d28f671
27 changed files with 1735 additions and 290 deletions

View File

@@ -1,5 +1,3 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Mindforge.API.Exceptions;
using Mindforge.API.Models.Requests;
@@ -21,23 +19,41 @@ namespace Mindforge.API.Controllers
[HttpPost("generate")]
public async Task<IActionResult> Generate([FromBody] FlashcardGenerateRequest request)
{
if (string.IsNullOrWhiteSpace(request.FileContent) || request.Amount <= 0)
request ??= new FlashcardGenerateRequest();
if (request.FilePaths is null || request.FilePaths.Count == 0)
{
throw new UserException("FileContent is required and Amount must be greater than 0.");
throw new UserException("Selecione ao menos um arquivo para gerar flashcards.");
}
try
var libraries = await _flashcardService.GenerateFlashcardsAsync(request);
return Ok(new { libraries });
}
[HttpGet("libraries")]
public async Task<IActionResult> GetLibraries()
{
var libraries = await _flashcardService.GetLibrariesAsync();
return Ok(libraries);
}
[HttpGet("libraries/{id:long}")]
public async Task<IActionResult> GetLibraryById([FromRoute] long id)
{
var library = await _flashcardService.GetLibraryByIdAsync(id);
if (library is null)
{
var base64Bytes = Convert.FromBase64String(request.FileContent);
request.FileContent = System.Text.Encoding.UTF8.GetString(base64Bytes);
}
catch (FormatException)
{
throw new UserException("FileContent must be a valid base64 string.");
return NotFound(new { error = "Biblioteca de flashcards nao encontrada." });
}
var response = await _flashcardService.GenerateFlashcardsAsync(request);
return Ok(new { result = response });
return Ok(library);
}
[HttpPost("review-session")]
public async Task<IActionResult> BuildReviewSession([FromBody] FlashcardReviewSessionRequest request)
{
request ??= new FlashcardReviewSessionRequest();
var cards = await _flashcardService.BuildReviewSessionAsync(request);
return Ok(new { cards });
}
}
}

View File

@@ -7,7 +7,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.12" />
<PackageReference Include="Npgsql" Version="9.0.4" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,9 @@
namespace Mindforge.API.Models.Flashcards
{
public class FlashcardDraftCard
{
public string Front { get; set; } = string.Empty;
public string Back { get; set; } = string.Empty;
public int Position { get; set; }
}
}

View File

@@ -0,0 +1,29 @@
namespace Mindforge.API.Models.Flashcards
{
public class FlashcardLibrarySummary
{
public long Id { get; set; }
public string FilePath { get; set; } = string.Empty;
public string FileName { get; set; } = string.Empty;
public string Subject { get; set; } = string.Empty;
public string Difficulty { get; set; } = string.Empty;
public int CardCount { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
public class FlashcardCard
{
public long Id { get; set; }
public long LibraryId { get; set; }
public string Front { get; set; } = string.Empty;
public string Back { get; set; } = string.Empty;
public int Position { get; set; }
public DateTime CreatedAt { get; set; }
}
public class FlashcardLibraryDetails : FlashcardLibrarySummary
{
public List<FlashcardCard> Cards { get; set; } = [];
}
}

View File

@@ -4,18 +4,15 @@ namespace Mindforge.API.Models.Requests
{
public class FlashcardGenerateRequest
{
public string FileContent { get; set; } = string.Empty;
public string FileName { get; set; } = string.Empty;
public List<string> FilePaths { get; set; } = [];
public int Amount { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter))]
public FlashcardMode? Mode { get; set; } = FlashcardMode.Simple;
public FlashcardDifficulty Difficulty { get; set; } = FlashcardDifficulty.Easy;
}
public enum FlashcardMode
public enum FlashcardDifficulty
{
Basic,
Simple,
Detailed,
Hyper
Easy,
Hard
}
}

View File

@@ -0,0 +1,7 @@
namespace Mindforge.API.Models.Requests
{
public class FlashcardReviewSessionRequest
{
public List<long> LibraryIds { get; set; } = [];
}
}

View File

@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Mindforge.API.Providers;
using Mindforge.API.Repositories;
using Mindforge.API.Services;
using Mindforge.API.Services.Interfaces;
@@ -39,10 +40,25 @@ builder.Services.AddScoped<ILlmApiProvider, OpenAIApiProvider>();
builder.Services.AddScoped<IAgentService, AgentService>();
builder.Services.AddScoped<IFileService, FileService>();
builder.Services.AddScoped<IFlashcardService, FlashcardService>();
builder.Services.AddScoped<IFlashcardRepository, FlashcardRepository>();
builder.Services.AddScoped<IGiteaService, GiteaService>();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
try
{
var flashcardRepository = scope.ServiceProvider.GetRequiredService<IFlashcardRepository>();
await flashcardRepository.EnsureSchemaAsync();
app.Logger.LogInformation("Flashcard schema checked successfully.");
}
catch (Exception ex)
{
app.Logger.LogError(ex, "Could not initialize flashcard schema on startup.");
}
}
app.UseCors("AllowAll");
app.UseMiddleware<Mindforge.API.Middlewares.ExceptionHandlingMiddleware>();
@@ -80,4 +96,7 @@ if (string.IsNullOrEmpty(giteaRepoUrl))
if (string.IsNullOrEmpty(giteaAccessToken))
app.Logger.LogWarning("GITEA_ACCESS_TOKEN not found in configuration. Repository features will not work.");
if (string.IsNullOrEmpty(builder.Configuration.GetConnectionString("MindforgeDb")))
app.Logger.LogWarning("MindforgeDb connection string not found. Falling back to default PostgreSQL connection.");
app.Run();

View File

@@ -0,0 +1,260 @@
using Dapper;
using Mindforge.API.Models.Flashcards;
using Mindforge.API.Services.Interfaces;
using Npgsql;
namespace Mindforge.API.Repositories
{
public class FlashcardRepository : IFlashcardRepository
{
private const string DefaultConnectionString = "Host=iris.haven;Database=mindforge;Username=root;Password=root";
private readonly string _connectionString;
public FlashcardRepository(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("MindforgeDb")
?? configuration["MINDFORGE_DB_CONNECTION"]
?? DefaultConnectionString;
}
public async Task EnsureSchemaAsync()
{
const string sql = """
CREATE TABLE IF NOT EXISTS flashcard_libraries (
id BIGSERIAL PRIMARY KEY,
file_path TEXT NOT NULL UNIQUE,
file_name TEXT NOT NULL,
subject TEXT NOT NULL,
difficulty TEXT NOT NULL,
card_count INTEGER NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS flashcards (
id BIGSERIAL PRIMARY KEY,
library_id BIGINT NOT NULL REFERENCES flashcard_libraries(id) ON DELETE CASCADE,
front TEXT NOT NULL,
back TEXT NOT NULL,
position INTEGER NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS ix_flashcard_libraries_subject ON flashcard_libraries(subject);
CREATE INDEX IF NOT EXISTS ix_flashcards_library_id_position ON flashcards(library_id, position);
""";
await using var connection = CreateConnection();
await connection.OpenAsync();
await connection.ExecuteAsync(sql);
}
public async Task<FlashcardLibraryDetails> UpsertLibraryAsync(
string filePath,
string fileName,
string subject,
string difficulty,
IReadOnlyList<FlashcardDraftCard> cards)
{
await using var connection = CreateConnection();
await connection.OpenAsync();
await using var transaction = await connection.BeginTransactionAsync();
try
{
var existingId = await connection.ExecuteScalarAsync<long?>(
"SELECT id FROM flashcard_libraries WHERE file_path = @FilePath FOR UPDATE;",
new { FilePath = filePath },
transaction);
long libraryId;
if (existingId.HasValue)
{
libraryId = existingId.Value;
await connection.ExecuteAsync(
"""
UPDATE flashcard_libraries
SET file_name = @FileName,
subject = @Subject,
difficulty = @Difficulty,
card_count = @CardCount,
updated_at = NOW()
WHERE id = @LibraryId;
""",
new
{
FileName = fileName,
Subject = subject,
Difficulty = difficulty,
CardCount = cards.Count,
LibraryId = libraryId
},
transaction);
await connection.ExecuteAsync(
"DELETE FROM flashcards WHERE library_id = @LibraryId;",
new { LibraryId = libraryId },
transaction);
}
else
{
libraryId = await connection.ExecuteScalarAsync<long>(
"""
INSERT INTO flashcard_libraries (file_path, file_name, subject, difficulty, card_count)
VALUES (@FilePath, @FileName, @Subject, @Difficulty, @CardCount)
RETURNING id;
""",
new
{
FilePath = filePath,
FileName = fileName,
Subject = subject,
Difficulty = difficulty,
CardCount = cards.Count
},
transaction);
}
if (cards.Count > 0)
{
await connection.ExecuteAsync(
"""
INSERT INTO flashcards (library_id, front, back, position)
VALUES (@LibraryId, @Front, @Back, @Position);
""",
cards.Select(card => new
{
LibraryId = libraryId,
card.Front,
card.Back,
card.Position
}),
transaction);
}
await transaction.CommitAsync();
var details = await GetLibraryByIdAsync(libraryId);
if (details is null)
{
throw new InvalidOperationException("Biblioteca gerada nao encontrada apos persistencia.");
}
return details;
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
public async Task<IReadOnlyList<FlashcardLibrarySummary>> GetLibrariesAsync()
{
const string sql = """
SELECT
id AS Id,
file_path AS FilePath,
file_name AS FileName,
subject AS Subject,
difficulty AS Difficulty,
card_count AS CardCount,
created_at AS CreatedAt,
updated_at AS UpdatedAt
FROM flashcard_libraries
ORDER BY subject, file_name;
""";
await using var connection = CreateConnection();
await connection.OpenAsync();
var rows = await connection.QueryAsync<FlashcardLibrarySummary>(sql);
return rows.ToList();
}
public async Task<FlashcardLibraryDetails?> GetLibraryByIdAsync(long libraryId)
{
const string librarySql = """
SELECT
id AS Id,
file_path AS FilePath,
file_name AS FileName,
subject AS Subject,
difficulty AS Difficulty,
card_count AS CardCount,
created_at AS CreatedAt,
updated_at AS UpdatedAt
FROM flashcard_libraries
WHERE id = @LibraryId;
""";
const string cardsSql = """
SELECT
id AS Id,
library_id AS LibraryId,
front AS Front,
back AS Back,
position AS Position,
created_at AS CreatedAt
FROM flashcards
WHERE library_id = @LibraryId
ORDER BY position;
""";
await using var connection = CreateConnection();
await connection.OpenAsync();
var library = await connection.QuerySingleOrDefaultAsync<FlashcardLibrarySummary>(
librarySql,
new { LibraryId = libraryId });
if (library is null)
{
return null;
}
var cards = await connection.QueryAsync<FlashcardCard>(cardsSql, new { LibraryId = libraryId });
return new FlashcardLibraryDetails
{
Id = library.Id,
FilePath = library.FilePath,
FileName = library.FileName,
Subject = library.Subject,
Difficulty = library.Difficulty,
CardCount = library.CardCount,
CreatedAt = library.CreatedAt,
UpdatedAt = library.UpdatedAt,
Cards = cards.ToList()
};
}
public async Task<IReadOnlyList<FlashcardCard>> GetCardsForLibrariesAsync(IReadOnlyList<long> libraryIds)
{
if (libraryIds.Count == 0)
{
return [];
}
const string sql = """
SELECT
id AS Id,
library_id AS LibraryId,
front AS Front,
back AS Back,
position AS Position,
created_at AS CreatedAt
FROM flashcards
WHERE library_id = ANY(@LibraryIds)
ORDER BY library_id, position;
""";
await using var connection = CreateConnection();
await connection.OpenAsync();
var cards = await connection.QueryAsync<FlashcardCard>(sql, new { LibraryIds = libraryIds.ToArray() });
return cards.ToList();
}
private NpgsqlConnection CreateConnection()
{
return new NpgsqlConnection(_connectionString);
}
}
}

View File

@@ -1,4 +1,6 @@
using System.Threading.Tasks;
using System.Text.Json;
using Mindforge.API.Exceptions;
using Mindforge.API.Models.Flashcards;
using Mindforge.API.Models.Requests;
using Mindforge.API.Services.Interfaces;
@@ -7,59 +9,234 @@ namespace Mindforge.API.Services
public class FlashcardService : IFlashcardService
{
private readonly IAgentService _agentService;
private readonly IGiteaService _giteaService;
private readonly IFlashcardRepository _flashcardRepository;
private readonly ILogger<FlashcardService> _logger;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
public FlashcardService(IAgentService agentService, ILogger<FlashcardService> logger)
public FlashcardService(
IAgentService agentService,
IGiteaService giteaService,
IFlashcardRepository flashcardRepository,
ILogger<FlashcardService> logger)
{
_agentService = agentService;
_giteaService = giteaService;
_flashcardRepository = flashcardRepository;
_logger = logger;
}
public async Task<string> GenerateFlashcardsAsync(FlashcardGenerateRequest request)
public async Task<IReadOnlyList<FlashcardLibraryDetails>> GenerateFlashcardsAsync(FlashcardGenerateRequest request)
{
var extraPrompt = request.Mode switch
if (request.FilePaths.Count == 0)
{
FlashcardMode.Detailed => "Crie flashcards mais detalhados.",
FlashcardMode.Hyper => "Adicione também pequenas questões para fixação, para que o usuário possa testar seus conhecimentos. As questões devem ser curtas e objetivas, como se fosse cobradas em prova mesmo.",
_ => ""
throw new UserException("Selecione ao menos um arquivo para gerar flashcards.");
}
if (request.Amount is < 10 or > 30)
{
throw new UserException("A quantidade de flashcards deve estar entre 10 e 30 por arquivo.");
}
var uniqueFilePaths = request.FilePaths
.Where(path => !string.IsNullOrWhiteSpace(path))
.Select(path => path.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (uniqueFilePaths.Count == 0)
{
throw new UserException("Nenhum caminho de arquivo valido foi enviado.");
}
var generatedLibraries = new List<FlashcardLibraryDetails>();
foreach (var filePath in uniqueFilePaths)
{
var fileContent = await _giteaService.GetFileContentAsync(filePath);
var fileName = Path.GetFileName(filePath);
var subject = ExtractSubject(filePath);
var cards = await GenerateCardsForFileAsync(filePath, fileContent, request.Amount, request.Difficulty);
var library = await _flashcardRepository.UpsertLibraryAsync(
filePath,
fileName,
subject,
request.Difficulty.ToString(),
cards);
generatedLibraries.Add(library);
}
return generatedLibraries;
}
public Task<IReadOnlyList<FlashcardLibrarySummary>> GetLibrariesAsync()
{
return _flashcardRepository.GetLibrariesAsync();
}
public Task<FlashcardLibraryDetails?> GetLibraryByIdAsync(long libraryId)
{
return _flashcardRepository.GetLibraryByIdAsync(libraryId);
}
public async Task<IReadOnlyList<FlashcardCard>> BuildReviewSessionAsync(FlashcardReviewSessionRequest request)
{
var libraryIds = (request.LibraryIds ?? [])
.Where(id => id > 0)
.Distinct()
.ToList();
if (libraryIds.Count == 0)
{
throw new UserException("Selecione ao menos uma biblioteca para iniciar a revisao.");
}
return await _flashcardRepository.GetCardsForLibrariesAsync(libraryIds);
}
private async Task<IReadOnlyList<FlashcardDraftCard>> GenerateCardsForFileAsync(
string filePath,
string fileContent,
int amount,
FlashcardDifficulty difficulty)
{
var difficultyInstruction = difficulty switch
{
FlashcardDifficulty.Hard => "Crie perguntas mais desafiadoras, focando diferencas finas e cobrancas de prova.",
_ => "Crie perguntas diretas e objetivas para facilitar memorizacao inicial."
};
string systemPrompt = $@"Você é um assistente educacional especializado em criar flashcards para o Anki.
Baseado no texto fornecido, crie exatamente {request.Amount} flashcards que focam nos conceitos mais importantes e difíceis.
A resposta FINAL deve ser APENAS no formato CSV, pronto para importação no Anki, sem nenhum texto adicional antes ou depois.
O formato CSV deve ter duas colunas: a frente da carta (pergunta/conceito) e o verso (resposta/explicação). Use ponto e vírgula (;) como separador. Não adicione o cabeçalho.
As perguntas e respostas devem estar estritamente em Português do Brasil.
var systemPrompt =
"Voce e um assistente educacional especializado em flashcards.\n"
+ $"Gere exatamente {amount} flashcards em Portugues do Brasil.\n"
+ "Cada item precisa conter:\n"
+ "- front: pergunta ou gatilho curto\n"
+ "- back: resposta clara, correta e objetiva\n"
+ $"{difficultyInstruction}\n"
+ "Responda apenas com JSON valido, sem markdown, no formato:\n"
+ "{\n"
+ " \"flashcards\": [\n"
+ " { \"front\": \"texto\", \"back\": \"texto\" }\n"
+ " ]\n"
+ "}";
Exemplo de saída:
""Qual é a capital do Brasil?"";""Brasília""
""Qual é a maior cidade do Brasil?"";""São Paulo""
var userPrompt = $"""
Arquivo: {filePath}
Conteudo:
{fileContent}
""";
Com base no arquivo fornecido, crie exatamente {request.Amount} flashcards que focam nos conceitos mais importantes e difíceis.
{extraPrompt}
";
var rawResult = await _agentService.ProcessRequestAsync(systemPrompt, userPrompt);
var cards = ParseFlashcardsFromJson(rawResult);
string userPrompt = $"Arquivo: {request.FileName}\nConteúdo:\n{request.FileContent}";
var result = await _agentService.ProcessRequestAsync(systemPrompt, userPrompt);
var lines = result.Split('\n');
if (lines.Length == 0)
if (cards.Count < amount)
{
throw new Exception("Nenhum flashcard gerado.");
throw new Exception($"Quantidade insuficiente de flashcards gerados para {filePath}. Esperado: {amount}. Recebido: {cards.Count}.");
}
if (lines.Length > request.Amount)
if (cards.Count > amount)
{
_logger.LogWarning("Quantidade de flashcards excede o limite.");
_logger.LogWarning(
"Quantidade de flashcards acima do solicitado para {FilePath}. Solicitado: {Amount}. Recebido: {Generated}.",
filePath,
amount,
cards.Count);
cards = cards.Take(amount).ToList();
}
if (lines.Length < request.Amount)
for (var i = 0; i < cards.Count; i++)
{
_logger.LogWarning("Quantidade de flashcards abaixo do limite.");
cards[i].Position = i + 1;
}
return result;
return cards;
}
private static List<FlashcardDraftCard> ParseFlashcardsFromJson(string rawResult)
{
var cleanJson = StripJsonCodeBlock(rawResult);
var payload = JsonSerializer.Deserialize<FlashcardJsonPayload>(cleanJson, JsonOptions);
if (payload?.Flashcards is { Count: > 0 })
{
return payload.Flashcards
.Where(card => !string.IsNullOrWhiteSpace(card.Front) && !string.IsNullOrWhiteSpace(card.Back))
.Select(card => new FlashcardDraftCard
{
Front = card.Front.Trim(),
Back = card.Back.Trim()
})
.ToList();
}
var directList = JsonSerializer.Deserialize<List<FlashcardJsonCard>>(cleanJson, JsonOptions);
if (directList is { Count: > 0 })
{
return directList
.Where(card => !string.IsNullOrWhiteSpace(card.Front) && !string.IsNullOrWhiteSpace(card.Back))
.Select(card => new FlashcardDraftCard
{
Front = card.Front.Trim(),
Back = card.Back.Trim()
})
.ToList();
}
throw new Exception("Resposta de flashcards invalida. O modelo nao retornou JSON estruturado.");
}
private static string StripJsonCodeBlock(string value)
{
var trimmed = value.Trim();
if (!trimmed.StartsWith("```", StringComparison.Ordinal))
{
return trimmed;
}
var lines = trimmed.Split('\n');
if (lines.Length <= 2)
{
return trimmed.Trim('`', '\r', '\n');
}
return string.Join('\n', lines.Skip(1).Take(lines.Length - 2)).Trim();
}
private static string ExtractSubject(string filePath)
{
var normalized = filePath.Replace('\\', '/');
var segments = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (segments.Length == 0)
{
return "Geral";
}
var concursosIndex = Array.FindIndex(
segments,
segment => segment.Equals("concursos", StringComparison.OrdinalIgnoreCase)
|| segment.Equals("concurso", StringComparison.OrdinalIgnoreCase));
if (concursosIndex >= 0 && concursosIndex + 1 < segments.Length)
{
return segments[concursosIndex + 1];
}
return segments[0];
}
private class FlashcardJsonPayload
{
public List<FlashcardJsonCard> Flashcards { get; set; } = [];
}
private class FlashcardJsonCard
{
public string Front { get; set; } = string.Empty;
public string Back { get; set; } = string.Empty;
}
}
}

View File

@@ -10,10 +10,11 @@ namespace Mindforge.API.Services
public class GiteaService : IGiteaService
{
private readonly HttpClient _httpClient;
private readonly string _baseUrl;
private readonly string _owner;
private readonly string _repo;
private readonly string _token;
private readonly string _baseUrl = string.Empty;
private readonly string _owner = string.Empty;
private readonly string _repo = string.Empty;
private readonly string _token = string.Empty;
private readonly bool _isConfigured;
private static readonly JsonSerializerOptions JsonOptions = new()
{
@@ -25,25 +26,35 @@ namespace Mindforge.API.Services
_httpClient = httpClient;
var repoUrl = configuration["GITEA_REPO_URL"];
if (string.IsNullOrEmpty(repoUrl))
throw new InvalidOperationException("GITEA_REPO_URL is not set in configuration.");
var token = configuration["GITEA_ACCESS_TOKEN"];
if (string.IsNullOrWhiteSpace(repoUrl) || string.IsNullOrWhiteSpace(token))
{
_isConfigured = false;
return;
}
_token = configuration["GITEA_ACCESS_TOKEN"]
?? throw new InvalidOperationException("GITEA_ACCESS_TOKEN is not set in configuration.");
// Parse: https://host/owner/repo or https://host/owner/repo.git
var normalizedUrl = repoUrl.TrimEnd('/').TrimEnd(".git".ToCharArray());
var uri = new Uri(normalizedUrl);
_baseUrl = $"{uri.Scheme}://{uri.Host}{(uri.IsDefaultPort ? "" : $":{uri.Port}")}";
var parts = uri.AbsolutePath.Trim('/').Split('/');
if (parts.Length < 2)
{
_isConfigured = false;
return;
}
_baseUrl = $"{uri.Scheme}://{uri.Host}{(uri.IsDefaultPort ? "" : $":{uri.Port}")}";
_owner = parts[0];
_repo = parts[1];
_token = token;
_isConfigured = true;
}
public string GetRepositoryName() => _repo;
public string GetRepositoryName() => _isConfigured ? _repo : "repositorio";
public async Task<List<FileTreeNode>> GetFileTreeAsync()
{
EnsureConfigured();
// Get the master branch to obtain the latest commit SHA
var branchJson = await GetApiAsync($"/api/v1/repos/{_owner}/{_repo}/branches/master");
var branch = JsonSerializer.Deserialize<GiteaBranch>(branchJson, JsonOptions)
@@ -61,6 +72,8 @@ namespace Mindforge.API.Services
public async Task<string> GetFileContentAsync(string path)
{
EnsureConfigured();
var request = new HttpRequestMessage(HttpMethod.Get,
$"{_baseUrl}/api/v1/repos/{_owner}/{_repo}/raw/{path}?ref=master");
request.Headers.Add("Authorization", $"token {_token}");
@@ -75,6 +88,8 @@ namespace Mindforge.API.Services
private async Task<string> GetApiAsync(string endpoint)
{
EnsureConfigured();
var request = new HttpRequestMessage(HttpMethod.Get, $"{_baseUrl}{endpoint}");
request.Headers.Add("Authorization", $"token {_token}");
var response = await _httpClient.SendAsync(request);
@@ -82,6 +97,14 @@ namespace Mindforge.API.Services
return await response.Content.ReadAsStringAsync();
}
private void EnsureConfigured()
{
if (!_isConfigured)
{
throw new InvalidOperationException("Gitea nao configurado. Defina GITEA_REPO_URL e GITEA_ACCESS_TOKEN.");
}
}
private static List<FileTreeNode> BuildTree(List<GiteaTreeItem> items)
{
var root = new List<FileTreeNode>();

View File

@@ -0,0 +1,18 @@
using Mindforge.API.Models.Flashcards;
namespace Mindforge.API.Services.Interfaces
{
public interface IFlashcardRepository
{
Task EnsureSchemaAsync();
Task<FlashcardLibraryDetails> UpsertLibraryAsync(
string filePath,
string fileName,
string subject,
string difficulty,
IReadOnlyList<FlashcardDraftCard> cards);
Task<IReadOnlyList<FlashcardLibrarySummary>> GetLibrariesAsync();
Task<FlashcardLibraryDetails?> GetLibraryByIdAsync(long libraryId);
Task<IReadOnlyList<FlashcardCard>> GetCardsForLibrariesAsync(IReadOnlyList<long> libraryIds);
}
}

View File

@@ -1,10 +1,13 @@
using System.Threading.Tasks;
using Mindforge.API.Models.Flashcards;
using Mindforge.API.Models.Requests;
namespace Mindforge.API.Services.Interfaces
{
public interface IFlashcardService
{
Task<string> GenerateFlashcardsAsync(FlashcardGenerateRequest request);
Task<IReadOnlyList<FlashcardLibraryDetails>> GenerateFlashcardsAsync(FlashcardGenerateRequest request);
Task<IReadOnlyList<FlashcardLibrarySummary>> GetLibrariesAsync();
Task<FlashcardLibraryDetails?> GetLibraryByIdAsync(long libraryId);
Task<IReadOnlyList<FlashcardCard>> BuildReviewSessionAsync(FlashcardReviewSessionRequest request);
}
}

View File

@@ -6,6 +6,9 @@
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"MindforgeDb": "Host=localhost;Port=3307;Database=mindforge;Username=root;Password=root"
},
"OPENAI_API_URL": "https://openrouter.ai/api/v1",
"OPENAI_TOKEN": "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",
"OPENAI_MODEL": "openai/gpt-5.4-mini",

View File

@@ -39,6 +39,11 @@ spec:
secretKeyRef:
name: mindforge-secrets
key: GITEA_ACCESS_TOKEN
- name: ConnectionStrings__MindforgeDb
valueFrom:
secretKeyRef:
name: mindforge-secrets
key: ConnectionStrings__MindforgeDb
resources:
requests:
memory: "128Mi"