feat: implement WASM-compatible service proxies and update repository logic for ingestion state and database compatibility
This commit is contained in:
@@ -21,4 +21,18 @@ public interface ISyncBroadcaster
|
|||||||
DateTime timestamp,
|
DateTime timestamp,
|
||||||
string? excludedConnectionId,
|
string? excludedConnectionId,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Broadcasts real-time ingestion status updates to a specific user.
|
||||||
|
/// This is used by background workers to provide feedback during AI-intensive processing.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The ID of the user who owns the ingestion request.</param>
|
||||||
|
/// <param name="message">A human-readable status message (e.g., "Parsing chapters...").</param>
|
||||||
|
/// <param name="progress">Progress percentage (0.0 to 1.0).</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
Task BroadcastIngestionProgressAsync(
|
||||||
|
string userId,
|
||||||
|
string message,
|
||||||
|
double progress,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ public class Ebook
|
|||||||
public string? LastChapter { get; set; }
|
public string? LastChapter { get; set; }
|
||||||
|
|
||||||
public int LastChapterIndex { get; set; } = 0;
|
public int LastChapterIndex { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the ebook has been processed by the AI ingestion engine
|
||||||
|
/// and is ready for reading (Knowledge Units generated).
|
||||||
|
/// </summary>
|
||||||
|
public bool IsReadyForReading { get; set; } = false;
|
||||||
|
|
||||||
// Relationship to NexusUser
|
// Relationship to NexusUser
|
||||||
[Required]
|
[Required]
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ public static class DependencyInjection
|
|||||||
if (!string.IsNullOrEmpty(pgConnectionString))
|
if (!string.IsNullOrEmpty(pgConnectionString))
|
||||||
{
|
{
|
||||||
services.AddDbContextFactory<AppDbContext>(options =>
|
services.AddDbContextFactory<AppDbContext>(options =>
|
||||||
options.UseNpgsql(pgConnectionString, x => x.UseVector()));
|
options.UseNpgsql(pgConnectionString, x => x.UseVector()),
|
||||||
|
ServiceLifetime.Scoped);
|
||||||
|
|
||||||
// Also register a scoped DbContext for repositories that need it
|
// Also register a scoped DbContext for repositories that need it
|
||||||
services.AddDbContext<AppDbContext>(options =>
|
services.AddDbContext<AppDbContext>(options =>
|
||||||
@@ -41,7 +42,8 @@ public static class DependencyInjection
|
|||||||
{
|
{
|
||||||
var sqliteConnectionString = configuration.GetConnectionString("SqliteConnection") ?? "Data Source=nexus.db";
|
var sqliteConnectionString = configuration.GetConnectionString("SqliteConnection") ?? "Data Source=nexus.db";
|
||||||
services.AddDbContextFactory<AppDbContext>(options =>
|
services.AddDbContextFactory<AppDbContext>(options =>
|
||||||
options.UseSqlite(sqliteConnectionString));
|
options.UseSqlite(sqliteConnectionString),
|
||||||
|
ServiceLifetime.Scoped);
|
||||||
|
|
||||||
services.AddDbContext<AppDbContext>(options =>
|
services.AddDbContext<AppDbContext>(options =>
|
||||||
options.UseSqlite(sqliteConnectionString));
|
options.UseSqlite(sqliteConnectionString));
|
||||||
|
|||||||
@@ -21,9 +21,14 @@ internal sealed class EbookRepository : IEbookRepository
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<Author?> FindAuthorByNameAsync(string name, CancellationToken cancellationToken = default)
|
public async Task<Author?> FindAuthorByNameAsync(string name, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
// EF.Functions.ILike is PostgreSQL-specific; fall back to ToLower for SQLite.
|
// Use PostgreSQL ILike for case-insensitive searching if on Npgsql provider,
|
||||||
// The Author table is expected to be small, so the full scan is acceptable.
|
// otherwise fallback to string comparison.
|
||||||
// TODO: Add a case-insensitive collation to the Author.Name column in a future migration.
|
if (_context.Database.IsNpgsql())
|
||||||
|
{
|
||||||
|
return await _context.Authors
|
||||||
|
.FirstOrDefaultAsync(a => EF.Functions.ILike(a.Name, name), cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
return await _context.Authors
|
return await _context.Authors
|
||||||
.FirstOrDefaultAsync(
|
.FirstOrDefaultAsync(
|
||||||
a => a.Name.ToLower() == name.ToLower(),
|
a => a.Name.ToLower() == name.ToLower(),
|
||||||
@@ -34,7 +39,12 @@ internal sealed class EbookRepository : IEbookRepository
|
|||||||
public void AddAuthor(Author author) => _context.Authors.Add(author);
|
public void AddAuthor(Author author) => _context.Authors.Add(author);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void AddEbook(Ebook ebook) => _context.Ebooks.Add(ebook);
|
public void AddEbook(Ebook ebook)
|
||||||
|
{
|
||||||
|
// Explicitly set the readiness flag to false upon addition
|
||||||
|
ebook.IsReadyForReading = false;
|
||||||
|
_context.Ebooks.Add(ebook);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
public Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
|
|||||||
@@ -25,19 +25,37 @@ internal sealed class SignalRSyncBroadcaster : ISyncBroadcaster
|
|||||||
string? excludedConnectionId,
|
string? excludedConnectionId,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var groupName = $"User_{userId}";
|
// Using Clients.User(userId) targeted broadcasting.
|
||||||
|
// This pushes to all of a user's connected devices across all sessions.
|
||||||
if (!string.IsNullOrEmpty(excludedConnectionId))
|
if (!string.IsNullOrEmpty(excludedConnectionId))
|
||||||
{
|
{
|
||||||
await _hubContext.Clients
|
await _hubContext.Clients
|
||||||
.GroupExcept(groupName, excludedConnectionId)
|
.User(userId)
|
||||||
.SendAsync("ProgressUpdated", pageId, timestamp, cancellationToken: cancellationToken);
|
.SendAsync("ProgressUpdated", pageId, timestamp, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
// Note: SignalR HubContext doesn't easily support 'Except' when using .User(id)
|
||||||
|
// from outside the Hub itself without custom IUserIdProvider.
|
||||||
|
// If strict exclusion is needed, we'd use groups, but requirements mandate .User(userId).
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await _hubContext.Clients
|
await _hubContext.Clients
|
||||||
.Group(groupName)
|
.User(userId)
|
||||||
.SendAsync("ProgressUpdated", pageId, timestamp, cancellationToken: cancellationToken);
|
.SendAsync("ProgressUpdated", pageId, timestamp, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task BroadcastIngestionProgressAsync(
|
||||||
|
string userId,
|
||||||
|
string message,
|
||||||
|
double progress,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// Pushes ingestion status (e.g., "Parsing chapters...") and progress (0.0-1.0)
|
||||||
|
// directly to the user's active session components (like BookIngestionModal).
|
||||||
|
await _hubContext.Clients
|
||||||
|
.User(userId)
|
||||||
|
.SendAsync("IngestionProgress", message, progress, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ using NexusReader.Application;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
using NexusReader.Data.Persistence;
|
using NexusReader.Data.Persistence;
|
||||||
|
using NexusReader.Application.Abstractions.Persistence;
|
||||||
|
using NexusReader.Application.Abstractions.Messaging;
|
||||||
|
using NexusReader.Domain.Entities;
|
||||||
|
|
||||||
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
@@ -42,10 +45,12 @@ builder.Services.AddHttpClient("NexusAPI", client =>
|
|||||||
|
|
||||||
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("NexusAPI"));
|
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("NexusAPI"));
|
||||||
|
|
||||||
// Dummy registrations for server-only handlers to satisfy DI validation
|
// Real WASM implementations for application abstractions
|
||||||
builder.Services.AddSingleton<IDbContextFactory<AppDbContext>>(new ThrowingDbContextFactory());
|
builder.Services.AddSingleton<IDbContextFactory<AppDbContext>>(new ThrowingDbContextFactory());
|
||||||
builder.Services.AddSingleton<IEmbeddingGenerator<string, Embedding<float>>>(new ThrowingEmbeddingGenerator());
|
builder.Services.AddScoped<IEmbeddingGenerator<string, Embedding<float>>, WasmEmbeddingGenerator>();
|
||||||
builder.Services.AddSingleton<IBookStorageService>(new ThrowingBookStorageService());
|
builder.Services.AddScoped<IBookStorageService, WasmBookStorageService>();
|
||||||
|
builder.Services.AddScoped<IEbookRepository, WasmEbookRepository>();
|
||||||
|
builder.Services.AddScoped<ISyncBroadcaster, WasmSyncBroadcaster>();
|
||||||
|
|
||||||
builder.Services.AddApplication();
|
builder.Services.AddApplication();
|
||||||
builder.Services.AddScoped<IEpubReader, WasmEpubReader>();
|
builder.Services.AddScoped<IEpubReader, WasmEpubReader>();
|
||||||
@@ -55,23 +60,5 @@ await builder.Build().RunAsync();
|
|||||||
|
|
||||||
public class ThrowingDbContextFactory : IDbContextFactory<AppDbContext>
|
public class ThrowingDbContextFactory : IDbContextFactory<AppDbContext>
|
||||||
{
|
{
|
||||||
public AppDbContext CreateDbContext() => throw new NotSupportedException("DbContext cannot be used in WASM client.");
|
public AppDbContext CreateDbContext() => throw new NotSupportedException("DbContext cannot be used in WASM client. Use API proxies for data access.");
|
||||||
}
|
|
||||||
|
|
||||||
public class ThrowingEmbeddingGenerator : IEmbeddingGenerator<string, Embedding<float>>
|
|
||||||
{
|
|
||||||
public void Dispose() { }
|
|
||||||
public Task<GeneratedEmbeddings<Embedding<float>>> GenerateAsync(IEnumerable<string> values, EmbeddingGenerationOptions? options = null, CancellationToken cancellationToken = default)
|
|
||||||
=> throw new NotSupportedException("Embedding generation cannot be used in WASM client.");
|
|
||||||
public object? GetService(Type serviceType, object? serviceKey = null) => null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ThrowingBookStorageService : IBookStorageService
|
|
||||||
{
|
|
||||||
private const string ErrorMessage = "File storage operations are not supported in the WASM client. Use the API endpoint for ingestion.";
|
|
||||||
|
|
||||||
public Task<string> SaveEbookAsync(byte[] data, string fileName) => throw new NotSupportedException(ErrorMessage);
|
|
||||||
public Task<string> SaveEbookAsync(Stream data, string fileName) => throw new NotSupportedException(ErrorMessage);
|
|
||||||
public Task<string?> SaveCoverAsync(byte[] data, string fileName) => throw new NotSupportedException(ErrorMessage);
|
|
||||||
public Task<string?> SaveCoverAsync(Stream data, string fileName) => throw new NotSupportedException(ErrorMessage);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using NexusReader.Application.Abstractions.Services;
|
||||||
|
|
||||||
|
namespace NexusReader.Web.Client.Services;
|
||||||
|
|
||||||
|
public class WasmBookStorageService : IBookStorageService
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
public WasmBookStorageService(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> SaveEbookAsync(byte[] data, string fileName)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.PostAsJsonAsync("/api/storage/save/ebook", new { data, fileName });
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<StorageResponse>();
|
||||||
|
return result?.Path ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> SaveEbookAsync(Stream data, string fileName)
|
||||||
|
{
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
await data.CopyToAsync(ms);
|
||||||
|
return await SaveEbookAsync(ms.ToArray(), fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> SaveCoverAsync(byte[] data, string fileName)
|
||||||
|
{
|
||||||
|
if (data == null || data.Length == 0) return null;
|
||||||
|
var response = await _httpClient.PostAsJsonAsync("/api/storage/save/cover", new { data, fileName });
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<StorageResponse>();
|
||||||
|
return result?.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> SaveCoverAsync(Stream data, string fileName)
|
||||||
|
{
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
await data.CopyToAsync(ms);
|
||||||
|
return await SaveCoverAsync(ms.ToArray(), fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record StorageResponse(string Path);
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using NexusReader.Application.Abstractions.Persistence;
|
||||||
|
using NexusReader.Domain.Entities;
|
||||||
|
|
||||||
|
namespace NexusReader.Web.Client.Services;
|
||||||
|
|
||||||
|
public class WasmEbookRepository : IEbookRepository
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
public WasmEbookRepository(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Author?> FindAuthorByNameAsync(string name, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.PostAsJsonAsync("/api/repository/author/find", new { name }, cancellationToken);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return await response.Content.ReadFromJsonAsync<Author>(cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddAuthor(Author author)
|
||||||
|
{
|
||||||
|
// For a repository in WASM, we can't easily do 'void' fire-and-forget Add without a local state.
|
||||||
|
// However, we can either queue it or just do nothing if the caller expects SaveChangesAsync to handle it.
|
||||||
|
// But the common pattern for this app seems to be calling the API.
|
||||||
|
// For now, we'll assume the entity will be sent during SaveChanges or a separate command.
|
||||||
|
// Given the constraints, we'll mark it for later serialization or just throw if not supported.
|
||||||
|
// Better yet: we'll implement a 'Real' enough version that tracks changes locally.
|
||||||
|
_stagedAuthors.Add(author);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddEbook(Ebook ebook)
|
||||||
|
{
|
||||||
|
_stagedEbooks.Add(ebook);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly List<Author> _stagedAuthors = new();
|
||||||
|
private readonly List<Ebook> _stagedEbooks = new();
|
||||||
|
|
||||||
|
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
foreach (var author in _stagedAuthors)
|
||||||
|
{
|
||||||
|
await _httpClient.PostAsJsonAsync("/api/repository/author/add", author, cancellationToken);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
foreach (var ebook in _stagedEbooks)
|
||||||
|
{
|
||||||
|
await _httpClient.PostAsJsonAsync("/api/repository/ebook/add", ebook, cancellationToken);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
_stagedAuthors.Clear();
|
||||||
|
_stagedEbooks.Clear();
|
||||||
|
|
||||||
|
await _httpClient.PostAsync("/api/repository/save", null, cancellationToken);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
|
||||||
|
namespace NexusReader.Web.Client.Services;
|
||||||
|
|
||||||
|
public class WasmEmbeddingGenerator : IEmbeddingGenerator<string, Embedding<float>>
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
public WasmEmbeddingGenerator(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() { }
|
||||||
|
|
||||||
|
public async Task<GeneratedEmbeddings<Embedding<float>>> GenerateAsync(
|
||||||
|
IEnumerable<string> values,
|
||||||
|
EmbeddingGenerationOptions? options = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.PostAsJsonAsync("/api/ai/embeddings", new { values, options }, cancellationToken);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<GeneratedEmbeddings<Embedding<float>>>(cancellationToken: cancellationToken);
|
||||||
|
return result ?? new GeneratedEmbeddings<Embedding<float>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? GetService(Type serviceType, object? serviceKey = null)
|
||||||
|
{
|
||||||
|
if (serviceType == typeof(IEmbeddingGenerator<string, Embedding<float>>)) return this;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using NexusReader.Application.Abstractions.Messaging;
|
||||||
|
|
||||||
|
namespace NexusReader.Web.Client.Services;
|
||||||
|
|
||||||
|
public class WasmSyncBroadcaster : ISyncBroadcaster
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
public WasmSyncBroadcaster(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task BroadcastProgressAsync(
|
||||||
|
string userId,
|
||||||
|
string pageId,
|
||||||
|
DateTime timestamp,
|
||||||
|
string? excludedConnectionId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _httpClient.PostAsJsonAsync("/api/broadcaster/progress", new
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
pageId,
|
||||||
|
timestamp,
|
||||||
|
excludedConnectionId
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task BroadcastIngestionProgressAsync(
|
||||||
|
string userId,
|
||||||
|
string message,
|
||||||
|
double progress,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _httpClient.PostAsJsonAsync("/api/broadcaster/ingestion-progress", new
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
message,
|
||||||
|
progress
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,9 @@ using Microsoft.AspNetCore.Authentication;
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using NexusReader.Infrastructure.Services;
|
using NexusReader.Infrastructure.Services;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
using NexusReader.Application.Abstractions.Persistence;
|
||||||
|
using NexusReader.Application.Abstractions.Messaging;
|
||||||
|
|
||||||
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||||
|
|
||||||
@@ -250,6 +253,13 @@ app.MapGet("/api/epub/{ebookId:guid}/{index:int}", async (Guid ebookId, int inde
|
|||||||
return Results.BadRequest(errorMsg);
|
return Results.BadRequest(errorMsg);
|
||||||
}).RequireAuthorization();
|
}).RequireAuthorization();
|
||||||
|
|
||||||
|
// Proxy API for AI services (Embeddings)
|
||||||
|
app.MapPost("/api/ai/embeddings", async (EmbeddingsRequest request, IEmbeddingGenerator<string, Embedding<float>> generator) =>
|
||||||
|
{
|
||||||
|
var result = await generator.GenerateAsync(request.Values, request.Options);
|
||||||
|
return Results.Ok(result);
|
||||||
|
}).RequireAuthorization();
|
||||||
|
|
||||||
var knowledgeApi = app.MapGroup("/api/knowledge").RequireAuthorization("HasAvailableTokens");
|
var knowledgeApi = app.MapGroup("/api/knowledge").RequireAuthorization("HasAvailableTokens");
|
||||||
|
|
||||||
knowledgeApi.MapPost("/", async (KnowledgeRequest request, ClaimsPrincipal user, IKnowledgeService knowledgeService) =>
|
knowledgeApi.MapPost("/", async (KnowledgeRequest request, ClaimsPrincipal user, IKnowledgeService knowledgeService) =>
|
||||||
@@ -293,6 +303,63 @@ knowledgeApi.MapDelete("/", async (IKnowledgeService knowledgeService) =>
|
|||||||
return Results.BadRequest(errorMsg);
|
return Results.BadRequest(errorMsg);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Proxy API for WASM Repository calls
|
||||||
|
var repoApi = app.MapGroup("/api/repository").RequireAuthorization();
|
||||||
|
|
||||||
|
repoApi.MapPost("/author/find", async (AuthorFindRequest request, IEbookRepository repo) =>
|
||||||
|
{
|
||||||
|
var author = await repo.FindAuthorByNameAsync(request.Name);
|
||||||
|
return author != null ? Results.Ok(author) : Results.NotFound();
|
||||||
|
});
|
||||||
|
|
||||||
|
repoApi.MapPost("/author/add", (Author author, IEbookRepository repo) =>
|
||||||
|
{
|
||||||
|
repo.AddAuthor(author);
|
||||||
|
return Results.Ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
repoApi.MapPost("/ebook/add", (Ebook ebook, IEbookRepository repo) =>
|
||||||
|
{
|
||||||
|
repo.AddEbook(ebook);
|
||||||
|
return Results.Ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
repoApi.MapPost("/save", async (IEbookRepository repo) =>
|
||||||
|
{
|
||||||
|
await repo.SaveChangesAsync();
|
||||||
|
return Results.Ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Proxy API for WASM Broadcaster calls
|
||||||
|
var broadcasterApi = app.MapGroup("/api/broadcaster").RequireAuthorization();
|
||||||
|
|
||||||
|
broadcasterApi.MapPost("/progress", async (BroadcastProgressRequest request, ISyncBroadcaster broadcaster) =>
|
||||||
|
{
|
||||||
|
await broadcaster.BroadcastProgressAsync(request.UserId, request.PageId, request.Timestamp, request.ExcludedConnectionId);
|
||||||
|
return Results.Ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
broadcasterApi.MapPost("/ingestion-progress", async (BroadcastIngestionProgressRequest request, ISyncBroadcaster broadcaster) =>
|
||||||
|
{
|
||||||
|
await broadcaster.BroadcastIngestionProgressAsync(request.UserId, request.Message, request.Progress);
|
||||||
|
return Results.Ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Proxy API for WASM Storage calls
|
||||||
|
var storageApi = app.MapGroup("/api/storage").RequireAuthorization();
|
||||||
|
|
||||||
|
storageApi.MapPost("/save/ebook", async (StorageRequest request, IBookStorageService storage) =>
|
||||||
|
{
|
||||||
|
var path = await storage.SaveEbookAsync(request.Data, request.FileName);
|
||||||
|
return Results.Ok(new { Path = path });
|
||||||
|
});
|
||||||
|
|
||||||
|
storageApi.MapPost("/save/cover", async (StorageRequest request, IBookStorageService storage) =>
|
||||||
|
{
|
||||||
|
var path = await storage.SaveCoverAsync(request.Data, request.FileName);
|
||||||
|
return Results.Ok(new { Path = path });
|
||||||
|
});
|
||||||
|
|
||||||
app.MapPost("/api/library/ingest", async ([FromBody] IngestEbookRequest request, ClaimsPrincipal user, IMediator mediator) =>
|
app.MapPost("/api/library/ingest", async ([FromBody] IngestEbookRequest request, ClaimsPrincipal user, IMediator mediator) =>
|
||||||
{
|
{
|
||||||
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
@@ -518,3 +585,8 @@ app.Run();
|
|||||||
|
|
||||||
public record KnowledgeRequest(string Text);
|
public record KnowledgeRequest(string Text);
|
||||||
public record GroundednessRequest(string Answer, string Context);
|
public record GroundednessRequest(string Answer, string Context);
|
||||||
|
public record AuthorFindRequest(string Name);
|
||||||
|
public record BroadcastProgressRequest(string UserId, string PageId, DateTime Timestamp, string? ExcludedConnectionId);
|
||||||
|
public record BroadcastIngestionProgressRequest(string UserId, string Message, double Progress);
|
||||||
|
public record StorageRequest(byte[] Data, string FileName);
|
||||||
|
public record EmbeddingsRequest(IEnumerable<string> Values, EmbeddingGenerationOptions? Options);
|
||||||
|
|||||||
Reference in New Issue
Block a user