feat(backend): add knowledge base, innovation library and trends modules
Add knowledge item and innovation CRUD with search/filtering, published-only visibility for non-staff, admin publish/update, challenge dictionary, problem aggregation with trends (admin only) and canonical embedding text preparation for matchmaking.
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IChallengeService
|
||||
{
|
||||
Task<IReadOnlyList<SocialChallengeResponse>> ListAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IInnovationService
|
||||
{
|
||||
Task<IReadOnlyList<InnovationResponse>> ListAsync(
|
||||
string? challengeId,
|
||||
string? stage,
|
||||
string? query,
|
||||
bool onlyPublished,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<InnovationResponse?> GetAsync(string innovationId, CancellationToken cancellationToken);
|
||||
|
||||
Task<InnovationResponse> CreateAsync(InnovationRequest request, CancellationToken cancellationToken);
|
||||
|
||||
Task<InnovationResponse?> UpdateAsync(
|
||||
string innovationId,
|
||||
InnovationRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IKnowledgeItemService
|
||||
{
|
||||
Task<IReadOnlyList<KnowledgeItemResponse>> ListAsync(
|
||||
string? kind,
|
||||
string? challengeId,
|
||||
string? query,
|
||||
bool onlyPublished,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<KnowledgeItemResponse?> GetAsync(string knowledgeItemId, CancellationToken cancellationToken);
|
||||
|
||||
Task<KnowledgeItemResponse> CreateAsync(KnowledgeItemRequest request, CancellationToken cancellationToken);
|
||||
|
||||
Task<KnowledgeItemResponse?> UpdateAsync(
|
||||
string knowledgeItemId,
|
||||
KnowledgeItemRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<KnowledgeItemResponse?> PublishAsync(string knowledgeItemId, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface ITrendService
|
||||
{
|
||||
Task<TrendsResponse> GetAsync(
|
||||
string? challengeId,
|
||||
DateTimeOffset from,
|
||||
DateTimeOffset to,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
public sealed record InnovationRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Title,
|
||||
[StringLength(2000)] string? Summary,
|
||||
[StringLength(8000)] string? Description,
|
||||
[Required] string ChallengeId,
|
||||
[Required] string Stage,
|
||||
string? Cost,
|
||||
IReadOnlyDictionary<string, object?>? Contact,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, object?>>? Media,
|
||||
bool Published);
|
||||
|
||||
public sealed record InnovationResponse(
|
||||
string Id,
|
||||
string Title,
|
||||
string Summary,
|
||||
string Description,
|
||||
string ChallengeId,
|
||||
string Stage,
|
||||
string? Cost,
|
||||
IReadOnlyDictionary<string, object?>? Contact,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, object?>> Media,
|
||||
bool Published,
|
||||
DateTimeOffset? UpdatedAt);
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
public sealed record KnowledgeItemRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Title,
|
||||
[Required] string Kind,
|
||||
string? ChallengeId,
|
||||
string? SourceUrl,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, object?>>? Attachments,
|
||||
bool Published);
|
||||
|
||||
public sealed record KnowledgeItemResponse(
|
||||
string Id,
|
||||
string Title,
|
||||
string Kind,
|
||||
string? ChallengeId,
|
||||
string? SourceUrl,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, object?>> Attachments,
|
||||
bool Published,
|
||||
DateTimeOffset? UpdatedAt);
|
||||
|
||||
public sealed record SocialChallengeResponse(
|
||||
string Id,
|
||||
string Name,
|
||||
string Description,
|
||||
int Order);
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
public sealed record TrendPoint(
|
||||
string ChallengeId,
|
||||
string ChallengeName,
|
||||
int SubmissionCount);
|
||||
|
||||
public sealed record TrendsResponse(
|
||||
DateTimeOffset From,
|
||||
DateTimeOffset To,
|
||||
IReadOnlyList<TrendPoint> Points);
|
||||
@@ -0,0 +1,29 @@
|
||||
using TarninaHub.Domain.Enums;
|
||||
|
||||
namespace TarninaHub.Application.Mappings;
|
||||
|
||||
public static class InnovationStageWire
|
||||
{
|
||||
private static readonly Dictionary<string, InnovationStage> ByWire = new(StringComparer.Ordinal)
|
||||
{
|
||||
["idea"] = InnovationStage.Idea,
|
||||
["pilot"] = InnovationStage.Pilot,
|
||||
["scaling"] = InnovationStage.Scaling,
|
||||
["service"] = InnovationStage.Service,
|
||||
};
|
||||
|
||||
public static bool IsValid(string? value) =>
|
||||
!string.IsNullOrWhiteSpace(value) && ByWire.ContainsKey(value);
|
||||
|
||||
public static string ToWire(this InnovationStage stage) => stage switch
|
||||
{
|
||||
InnovationStage.Idea => "idea",
|
||||
InnovationStage.Pilot => "pilot",
|
||||
InnovationStage.Scaling => "scaling",
|
||||
InnovationStage.Service => "service",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(stage), stage, null),
|
||||
};
|
||||
|
||||
public static InnovationStage? Parse(string? value) =>
|
||||
value is not null && ByWire.TryGetValue(value, out var stage) ? stage : null;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace TarninaHub.Application.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the canonical text used to compute an innovation embedding
|
||||
/// (matchmaking, steps 14-15).
|
||||
/// </summary>
|
||||
public static class EmbeddingTextBuilder
|
||||
{
|
||||
public static string Build(string title, string? summary, string? description, string? challengeName = null)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
parts.Add(title.Trim());
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(summary))
|
||||
{
|
||||
parts.Add(summary.Trim());
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
parts.Add(description.Trim());
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(challengeName))
|
||||
{
|
||||
parts.Add(challengeName.Trim());
|
||||
}
|
||||
|
||||
return string.Join("\n", parts);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user