diff --git a/.aspire/settings.json b/.aspire/settings.json new file mode 100644 index 0000000..2ece20b --- /dev/null +++ b/.aspire/settings.json @@ -0,0 +1,3 @@ +{ + "appHostPath": "../src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj" +} \ No newline at end of file diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 2dfb32f..8587f12 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -57,7 +57,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - file: ./NoteBookmark.Api/Dockerfile + file: ./src/NoteBookmark.Api/Dockerfile push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta-api.outputs.tags }} labels: ${{ steps.meta-api.outputs.labels }} @@ -66,7 +66,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - file: ./NoteBookmark.BlazorApp/Dockerfile + file: ./src/NoteBookmark.BlazorApp/Dockerfile push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta-blazor.outputs.tags }} labels: ${{ steps.meta-blazor.outputs.labels }} diff --git a/.gitignore b/.gitignore index 906e9dd..7684431 100644 --- a/.gitignore +++ b/.gitignore @@ -493,3 +493,7 @@ NoteBookmark.BlazorApp/appsettings.Development.json .azure NoteBookmark.AppHost/appsettings.Development.json + +src/NoteBookmark.AppHost/appsettings.[Dd]evelopment.json + +src/NoteBookmark.AppHost/appsettings.json diff --git a/NoteBookmark.AIServices/Choice.cs b/NoteBookmark.AIServices/Choice.cs deleted file mode 100644 index a6e4451..0000000 --- a/NoteBookmark.AIServices/Choice.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class Choice -{ - [JsonPropertyName("finish_reason")] - public string? FinishReason { get; set; } - - [JsonPropertyName("index")] - public int Index { get; set; } - - [JsonPropertyName("logprobs")] - public object? Logprobs { get; set; } - - [JsonPropertyName("message")] - public Message? Message { get; set; } -} \ No newline at end of file diff --git a/NoteBookmark.AIServices/ContentItem.cs b/NoteBookmark.AIServices/ContentItem.cs deleted file mode 100644 index 613dad9..0000000 --- a/NoteBookmark.AIServices/ContentItem.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class ContentItem -{ - [JsonPropertyName("type")] - public string? Type { get; set; } - - [JsonPropertyName("text")] - public string? Text { get; set; } -} \ No newline at end of file diff --git a/NoteBookmark.AIServices/Message.cs b/NoteBookmark.AIServices/Message.cs deleted file mode 100644 index 23f6616..0000000 --- a/NoteBookmark.AIServices/Message.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class Message -{ - [JsonPropertyName("content")] - public string? Content { get; set; } - - [JsonPropertyName("refusal")] - public object? Refusal { get; set; } - - [JsonPropertyName("role")] - public string? Role { get; set; } - - [JsonPropertyName("annotations")] - public object? Annotations { get; set; } - - [JsonPropertyName("audio")] - public object? Audio { get; set; } - - [JsonPropertyName("function_call")] - public object? FunctionCall { get; set; } - - [JsonPropertyName("tool_calls")] - public object? ToolCalls { get; set; } - - [JsonPropertyName("reasoning_content")] - public string? ReasoningContent { get; set; } - - [JsonPropertyName("reasoning_steps")] - public List? ReasoningSteps { get; set; } -} \ No newline at end of file diff --git a/NoteBookmark.AIServices/ReasoningStep.cs b/NoteBookmark.AIServices/ReasoningStep.cs deleted file mode 100644 index 80cdf76..0000000 --- a/NoteBookmark.AIServices/ReasoningStep.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class ReasoningStep -{ - [JsonPropertyName("role")] - public string? Role { get; set; } - - [JsonPropertyName("content")] - public object? Content { get; set; } - - [JsonPropertyName("reasoning_content")] - public string? ReasoningContent { get; set; } - - [JsonPropertyName("tool_calls")] - public List? ToolCalls { get; set; } - - [JsonPropertyName("tool_call_id")] - public string? ToolCallId { get; set; } -} \ No newline at end of file diff --git a/NoteBookmark.AIServices/RekaChatResponse.cs b/NoteBookmark.AIServices/RekaChatResponse.cs deleted file mode 100644 index 9e2f020..0000000 --- a/NoteBookmark.AIServices/RekaChatResponse.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class RekaChatResponse -{ - [JsonPropertyName("id")] - public string? Id { get; set; } - - [JsonPropertyName("model")] - public string? Model { get; set; } - - [JsonPropertyName("usage")] - public RekaUsage? Usage { get; set; } - - [JsonPropertyName("responses")] - public List? Responses { get; set; } -} \ No newline at end of file diff --git a/NoteBookmark.AIServices/RekaMessage.cs b/NoteBookmark.AIServices/RekaMessage.cs deleted file mode 100644 index 596537c..0000000 --- a/NoteBookmark.AIServices/RekaMessage.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class RekaMessage -{ - [JsonPropertyName("role")] - public string? Role { get; set; } - - [JsonPropertyName("content")] - public List? Content { get; set; } - - [JsonPropertyName("in_reasoning")] - public bool InReasoning { get; set; } -} \ No newline at end of file diff --git a/NoteBookmark.AIServices/RekaResponse.cs b/NoteBookmark.AIServices/RekaResponse.cs deleted file mode 100644 index ffcbecf..0000000 --- a/NoteBookmark.AIServices/RekaResponse.cs +++ /dev/null @@ -1,31 +0,0 @@ - -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class RekaResponse -{ - [JsonPropertyName("id")] - public string? Id { get; set; } - - [JsonPropertyName("choices")] - public List? Choices { get; set; } - - [JsonPropertyName("created")] - public long Created { get; set; } - - [JsonPropertyName("model")] - public string? Model { get; set; } - - [JsonPropertyName("object")] - public string? Object { get; set; } - - [JsonPropertyName("service_tier")] - public string? ServiceTier { get; set; } - - [JsonPropertyName("system_fingerprint")] - public string? SystemFingerprint { get; set; } - - [JsonPropertyName("usage")] - public Usage? Usage { get; set; } -} \ No newline at end of file diff --git a/NoteBookmark.AIServices/RekaUsage.cs b/NoteBookmark.AIServices/RekaUsage.cs deleted file mode 100644 index 558311d..0000000 --- a/NoteBookmark.AIServices/RekaUsage.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class RekaUsage -{ - [JsonPropertyName("input_tokens")] - public int InputTokens { get; set; } - - [JsonPropertyName("output_tokens")] - public int OutputTokens { get; set; } - - [JsonPropertyName("reasoning_tokens")] - public int ReasoningTokens { get; set; } -} \ No newline at end of file diff --git a/NoteBookmark.AIServices/ResponseItem.cs b/NoteBookmark.AIServices/ResponseItem.cs deleted file mode 100644 index 5780d97..0000000 --- a/NoteBookmark.AIServices/ResponseItem.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class ResponseItem -{ - [JsonPropertyName("finish_reason")] - public string? FinishReason { get; set; } - - [JsonPropertyName("message")] - public RekaMessage? Message { get; set; } -} \ No newline at end of file diff --git a/NoteBookmark.AIServices/ToolCall.cs b/NoteBookmark.AIServices/ToolCall.cs deleted file mode 100644 index 38f0d12..0000000 --- a/NoteBookmark.AIServices/ToolCall.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class ToolCall -{ - [JsonPropertyName("id")] - public string? Id { get; set; } - - [JsonPropertyName("name")] - public string? Name { get; set; } - - [JsonPropertyName("args")] - public object? Args { get; set; } -} \ No newline at end of file diff --git a/NoteBookmark.AIServices/Usage.cs b/NoteBookmark.AIServices/Usage.cs deleted file mode 100644 index 1f49ca4..0000000 --- a/NoteBookmark.AIServices/Usage.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class Usage -{ - [JsonPropertyName("completion_tokens")] - public int CompletionTokens { get; set; } - - [JsonPropertyName("prompt_tokens")] - public int PromptTokens { get; set; } - - [JsonPropertyName("total_tokens")] - public int TotalTokens { get; set; } - - [JsonPropertyName("completion_tokens_details")] - public object? CompletionTokensDetails { get; set; } - - [JsonPropertyName("prompt_tokens_details")] - public object? PromptTokensDetails { get; set; } -} \ No newline at end of file diff --git a/NoteBookmark.Api.Tests/Endpoints/SummaryEndpointsTests.cs b/NoteBookmark.Api.Tests/Endpoints/SummaryEndpointsTests.cs deleted file mode 100644 index 1f4a4db..0000000 --- a/NoteBookmark.Api.Tests/Endpoints/SummaryEndpointsTests.cs +++ /dev/null @@ -1,226 +0,0 @@ -using FluentAssertions; -using NoteBookmark.Api.Tests.Fixtures; -using NoteBookmark.Domain; -using System.Net; -using System.Net.Http.Json; -using System.Text; -using Xunit; - -namespace NoteBookmark.Api.Tests.Endpoints; - -public class SummaryEndpointsTests : IClassFixture -{ - private readonly NoteBookmarkApiTestFactory _factory; - private readonly HttpClient _client; - - public SummaryEndpointsTests(NoteBookmarkApiTestFactory factory) - { - _factory = factory; - _client = _factory.CreateClient(); - } - - [Fact] - public async Task GetSummaries_ReturnsAllSummaries() - { - // Arrange - await SeedTestSummaries(); - - // Act - var response = await _client.GetAsync("/api/summary/"); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.OK); - - var summaries = await response.Content.ReadFromJsonAsync>(); - summaries.Should().NotBeNull(); - summaries.Should().NotBeEmpty(); - } - - [Fact] - public async Task SaveSummary_WithValidSummary_ReturnsCreated() - { - // Arrange - var testSummary = CreateTestSummary(); - - // Act - var response = await _client.PostAsJsonAsync("/api/summary/summary", testSummary); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.Created); - var createdSummary = await response.Content.ReadFromJsonAsync(); - createdSummary.Should().NotBeNull(); - createdSummary!.RowKey.Should().Be(testSummary.RowKey); - createdSummary.Title.Should().Be(testSummary.Title); - } - - [Fact] - public async Task SaveSummary_WithInvalidSummary_ReturnsBadRequest() - { - // Arrange - var invalidSummary = new Summary - { - PartitionKey = "summaries", - RowKey = "invalid-summary" - }; // Missing required fields - - // Act - var response = await _client.PostAsJsonAsync("/api/summary/summary", invalidSummary); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - } - - [Fact] - public async Task GetReadingNotes_WhenExists_ReturnsOkWithReadingNotes() - { - // Arrange - var readingNotes = CreateTestReadingNotes(); - await SeedReadingNotes(readingNotes); - - // Act - var response = await _client.GetAsync($"/api/summary/{readingNotes.Number}"); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.OK); - - var retrievedReadingNotes = await response.Content.ReadFromJsonAsync(); - retrievedReadingNotes.Should().NotBeNull(); - retrievedReadingNotes!.Number.Should().Be(readingNotes.Number); - retrievedReadingNotes.Title.Should().Be(readingNotes.Title); - } - - [Fact] - public async Task GetReadingNotes_WhenDoesNotExist_ReturnsNotFound() - { - // Arrange - var nonExistentNumber = "999"; - - // Act - var response = await _client.GetAsync($"/api/summary/{nonExistentNumber}"); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.NotFound); - } - - [Fact] - public async Task SaveReadingNotesMarkdown_WithValidMarkdown_ReturnsOkWithUrl() - { - // Arrange - var number = "456"; - var markdown = "# Test Reading Notes\n\nThis is test markdown content.\n\n## Summary\n\nTest summary here."; - var content = new StringContent(markdown, Encoding.UTF8, "text/plain"); - - // Act - var response = await _client.PostAsync($"/api/summary/{number}/markdown", content); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.OK); - - var resultUrl = await response.Content.ReadAsStringAsync(); - resultUrl.Should().NotBeNullOrEmpty(); - resultUrl.Should().Contain($"readingnotes-{number}.md"); - } - - [Fact] - public async Task SaveReadingNotesMarkdown_WithEmptyMarkdown_ReturnsBadRequest() - { - // Arrange - var number = "789"; - var content = new StringContent("", Encoding.UTF8, "text/plain"); - - // Act - var response = await _client.PostAsync($"/api/summary/{number}/markdown", content); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - } - - [Fact] - public async Task SaveSummary_ThenGetSummaries_IncludesNewSummary() - { - // Arrange - var testSummary = CreateTestSummary(); - - // Act - Save summary - var saveResponse = await _client.PostAsJsonAsync("/api/summary/summary", testSummary); - saveResponse.EnsureSuccessStatusCode(); - - // Act - Get all summaries - var getResponse = await _client.GetAsync("/api/summary/"); - - // Assert - getResponse.StatusCode.Should().Be(HttpStatusCode.OK); - - var summaries = await getResponse.Content.ReadFromJsonAsync>(); - summaries.Should().NotBeNull(); - summaries.Should().Contain(s => s.RowKey == testSummary.RowKey); - } - - [Fact] - public async Task SaveSummary_ExistingSummary_UpdatesSuccessfully() - { - // Arrange - var testSummary = CreateTestSummary(); - await _client.PostAsJsonAsync("/api/summary/summary", testSummary); - // Modify the summary - testSummary.Title = "Updated summary content"; - - // Act - var response = await _client.PostAsJsonAsync("/api/summary/summary", testSummary); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.Created); - - // Verify the summary was updated - var summaries = await GetAllSummaries(); - var updatedSummary = summaries.FirstOrDefault(s => s.RowKey == testSummary.RowKey); - updatedSummary.Should().NotBeNull(); - updatedSummary!.Title.Should().Be("Updated summary content"); - } - - // Helper methods - private async Task SeedTestSummaries() - { var summary1 = CreateTestSummary(); - summary1.RowKey = "summary-1"; - summary1.Id = "100"; - - var summary2 = CreateTestSummary(); - summary2.RowKey = "summary-2"; - summary2.Id = "101"; - - await _client.PostAsJsonAsync("/api/summary/summary", summary1); - await _client.PostAsJsonAsync("/api/summary/summary", summary2); - } - - private async Task SeedReadingNotes(ReadingNotes readingNotes) - { - // Save reading notes via the notes endpoint - await _client.PostAsJsonAsync("/api/notes/SaveReadingNotes", readingNotes); - } - - private async Task> GetAllSummaries() - { - var response = await _client.GetAsync("/api/summary/"); - response.EnsureSuccessStatusCode(); - var summaries = await response.Content.ReadFromJsonAsync>(); - return summaries ?? new List(); - } private static Summary CreateTestSummary() - { - return new Summary - { - PartitionKey = "summaries", - RowKey = Guid.NewGuid().ToString(), - Id = "123", - Title = "Test summary content for reading notes 123.", - FileName = "readingnotes-123.json" - }; } - - private static ReadingNotes CreateTestReadingNotes() - { - return new ReadingNotes("456") - { - Title = "Test Reading Notes for Summary", - Intro = "Test description for summary endpoint testing" - }; - } -} diff --git a/NoteBookmark.Api/Dockerfile b/NoteBookmark.Api/Dockerfile deleted file mode 100644 index aca8267..0000000 --- a/NoteBookmark.Api/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base -WORKDIR /app -EXPOSE 8000 -EXPOSE 8002 - -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build -WORKDIR /src -COPY ["NoteBookmark.Api/NoteBookmark.Api.csproj", "NoteBookmark.Api/"] -COPY ["NoteBookmark.Domain/NoteBookmark.Domain.csproj", "NoteBookmark.Domain/"] -COPY ["NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj", "NoteBookmark.ServiceDefaults/"] -RUN dotnet restore "NoteBookmark.Api/NoteBookmark.Api.csproj" -COPY . . -WORKDIR "/src/NoteBookmark.Api" -RUN dotnet build "NoteBookmark.Api.csproj" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "NoteBookmark.Api.csproj" -c Release -o /app/publish - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "NoteBookmark.Api.dll"] diff --git a/NoteBookmark.BlazorApp/Components/Pages/Home.razor b/NoteBookmark.BlazorApp/Components/Pages/Home.razor deleted file mode 100644 index 858da0c..0000000 --- a/NoteBookmark.BlazorApp/Components/Pages/Home.razor +++ /dev/null @@ -1,7 +0,0 @@ -@page "/" - -Home - -

Hello, world!

- -Welcome to your new Fluent Blazor app. \ No newline at end of file diff --git a/NoteBookmark.BlazorApp/Components/Pages/Settings.razor b/NoteBookmark.BlazorApp/Components/Pages/Settings.razor deleted file mode 100644 index d96427d..0000000 --- a/NoteBookmark.BlazorApp/Components/Pages/Settings.razor +++ /dev/null @@ -1,94 +0,0 @@ -@page "/settings" - -@using Microsoft.FluentUI.AspNetCore.Components.Extensions -@using NoteBookmark.Domain -@inject ILogger Logger -@inject PostNoteClient client -@inject NavigationManager Navigation -@using NoteBookmark.BlazorApp - -@rendermode InteractiveServer - - - -

Settings

- -
- - - - - - - @context - - - - -
- -@if( settings != null) -{ -
- - - - - - - - - - - - Save - - - -
-} - - -@code { - public DesignThemeModes Mode { get; set; } - public OfficeColor? OfficeColor { get; set; } - - private Domain.Settings? settings; - - protected override async Task OnInitializedAsync() - { - settings = await client.GetSettings(); - } - - private async Task SaveSettings() - { - if (settings != null) - { - await client.SaveSettings(settings); - Navigation.NavigateTo("/"); - } - } - - void OnLoaded(LoadedEventArgs e) - { - Logger.LogInformation($"Loaded: {(e.Mode == DesignThemeModes.System ? "System" : "")} {(e.IsDark ? "Dark" : "Light")}"); - } - - void OnLuminanceChanged(LuminanceChangedEventArgs e) - { - Logger.LogInformation($"Changed: {(e.Mode == DesignThemeModes.System ? "System" : "")} {(e.IsDark ? "Dark" : "Light")}"); - } - - private void IncrementCounter() - { - var cnt = Convert.ToInt32(settings!.ReadingNotesCounter)+1; - settings.ReadingNotesCounter = (cnt).ToString(); - } -} diff --git a/NoteBookmark.sln b/NoteBookmark.sln index 389aa08..c39b4a6 100644 --- a/NoteBookmark.sln +++ b/NoteBookmark.sln @@ -3,19 +3,19 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.2.0 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NoteBookmark.Api", "NoteBookmark.Api\NoteBookmark.Api.csproj", "{482A0C21-91F3-4B77-B8CD-E1241D33B40D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NoteBookmark.Api", "src\NoteBookmark.Api\NoteBookmark.Api.csproj", "{482A0C21-91F3-4B77-B8CD-E1241D33B40D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.ServiceDefaults", "NoteBookmark.ServiceDefaults\NoteBookmark.ServiceDefaults.csproj", "{B3C00A0B-AFF2-4CF2-8A24-98430BC0206B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.ServiceDefaults", "src\NoteBookmark.ServiceDefaults\NoteBookmark.ServiceDefaults.csproj", "{B3C00A0B-AFF2-4CF2-8A24-98430BC0206B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.AppHost", "NoteBookmark.AppHost\NoteBookmark.AppHost.csproj", "{49440529-42B9-497D-8F2F-D0A2DE20253E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.AppHost", "src\NoteBookmark.AppHost\NoteBookmark.AppHost.csproj", "{49440529-42B9-497D-8F2F-D0A2DE20253E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.Domain", "NoteBookmark.Domain\NoteBookmark.Domain.csproj", "{204E2DC3-7131-4D7C-BA02-68AD272F9C58}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.Domain", "src\NoteBookmark.Domain\NoteBookmark.Domain.csproj", "{204E2DC3-7131-4D7C-BA02-68AD272F9C58}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.BlazorApp", "NoteBookmark.BlazorApp\NoteBookmark.BlazorApp.csproj", "{91470C33-E926-409D-8C0B-3647D7D5848B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.BlazorApp", "src\NoteBookmark.BlazorApp\NoteBookmark.BlazorApp.csproj", "{91470C33-E926-409D-8C0B-3647D7D5848B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.Api.Tests", "NoteBookmark.Api.Tests\NoteBookmark.Api.Tests.csproj", "{3DBF1970-A8DA-44E2-8AE8-0858F0269382}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.Api.Tests", "src\NoteBookmark.Api.Tests\NoteBookmark.Api.Tests.csproj", "{3DBF1970-A8DA-44E2-8AE8-0858F0269382}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.AIServices", "NoteBookmark.AIServices\NoteBookmark.AIServices.csproj", "{D29D80A5-82EC-4350-B738-96BAF88EB9DD}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.AIServices", "src\NoteBookmark.AIServices\NoteBookmark.AIServices.csproj", "{D29D80A5-82EC-4350-B738-96BAF88EB9DD}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/azure.yaml b/azure.yaml index 47f65b3..b98d8e0 100644 --- a/azure.yaml +++ b/azure.yaml @@ -4,5 +4,5 @@ name: NoteBookmark services: app: language: dotnet - project: ./NoteBookmark.AppHost/NoteBookmark.AppHost.csproj + project: ./src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj host: containerapp diff --git a/docker-compose/build-and-push.ps1 b/docker-compose/build-and-push.ps1 index ab3d207..f0cdc83 100644 --- a/docker-compose/build-and-push.ps1 +++ b/docker-compose/build-and-push.ps1 @@ -13,7 +13,7 @@ Write-Host "Building and pushing Docker images for NoteBookmark..." -ForegroundC # Build API image Write-Host "Building API image..." -ForegroundColor Yellow -docker build -f ../NoteBookmark.Api/Dockerfile -t "$DockerHubUsername/notebookmark-api:$ApiTag" .. +docker build -f ../src/NoteBookmark.Api/Dockerfile -t "$DockerHubUsername/notebookmark-api:$ApiTag" .. if ($LASTEXITCODE -ne 0) { Write-Error "Failed to build API image" @@ -22,7 +22,7 @@ if ($LASTEXITCODE -ne 0) { # Build Blazor App image Write-Host "Building Blazor App image..." -ForegroundColor Yellow -docker build -f ../NoteBookmark.BlazorApp/Dockerfile -t "$DockerHubUsername/notebookmark-blazor:$BlazorTag" .. +docker build -f ../src/NoteBookmark.BlazorApp/Dockerfile -t "$DockerHubUsername/notebookmark-blazor:$BlazorTag" .. if ($LASTEXITCODE -ne 0) { Write-Error "Failed to build Blazor App image" diff --git a/NoteBookmark.AIServices/NoteBookmark.AIServices.csproj b/src/NoteBookmark.AIServices/NoteBookmark.AIServices.csproj similarity index 71% rename from NoteBookmark.AIServices/NoteBookmark.AIServices.csproj rename to src/NoteBookmark.AIServices/NoteBookmark.AIServices.csproj index fd72d67..a9bdbe7 100644 --- a/NoteBookmark.AIServices/NoteBookmark.AIServices.csproj +++ b/src/NoteBookmark.AIServices/NoteBookmark.AIServices.csproj @@ -9,6 +9,12 @@ + + + + + + diff --git a/src/NoteBookmark.AIServices/ResearchService.cs b/src/NoteBookmark.AIServices/ResearchService.cs new file mode 100644 index 0000000..bedd895 --- /dev/null +++ b/src/NoteBookmark.AIServices/ResearchService.cs @@ -0,0 +1,147 @@ +using System.Text; +using System.Text.Json; +using System.IO; +using System.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Reka.SDK; +using NoteBookmark.Domain; + +namespace NoteBookmark.AIServices; + +public class ResearchService(HttpClient client, ILogger logger, IConfiguration config) +{ + private readonly HttpClient _client = client; + private readonly ILogger _logger = logger; + private const string BASE_URL = "https://api.reka.ai/v1/chat/completions"; + private const string MODEL_NAME = "reka-flash-research"; + private readonly string _apiKey = config["AppSettings:REKA_API_KEY"] ?? Environment.GetEnvironmentVariable("REKA_API_KEY") ?? throw new InvalidOperationException("REKA_API_KEY environment variable is not set."); + + public async Task SearchSuggestionsAsync(SearchCriterias searchCriterias) + { + PostSuggestions suggestions = new PostSuggestions(); + + var webSearch = new Dictionary + { + ["max_uses"] = 3 + }; + + var allowedDomains = searchCriterias.GetSplittedAllowedDomains(); + var blockedDomains = searchCriterias.GetSplittedBlockedDomains(); + + if (allowedDomains != null && allowedDomains.Length > 0) + { + webSearch["allowed_domains"] = allowedDomains; + } + else if (blockedDomains != null && blockedDomains.Length > 0) + { + webSearch["blocked_domains"] = blockedDomains; + } + + var requestPayload = new + { + model = MODEL_NAME, + + messages = new[] + { + new + { + role = "user", + content = searchCriterias.GetSearchPrompt() + } + }, + response_format = GetResponseFormat(), + research = new + { + web_search = webSearch + }, + }; + + var jsonPayload = JsonSerializer.Serialize(requestPayload, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + // await SaveToFile("research_request", jsonPayload); + + HttpResponseMessage? response = null; + + try + { + using var request = new HttpRequestMessage(HttpMethod.Post, BASE_URL); + request.Headers.Add("Authorization", $"Bearer {_apiKey}"); + request.Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); + + response = await _client.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + + await SaveToFile("research_response", responseContent); + + var rekaResponse = JsonSerializer.Deserialize(responseContent); + + if (response.IsSuccessStatusCode) + { + suggestions = JsonSerializer.Deserialize(rekaResponse!.Choices![0].Message!.Content!)!; + } + else + { + throw new Exception($"Request failed with status code: {response.StatusCode}. Response: {responseContent}"); + } + } + catch (Exception ex) + { + _logger.LogError($"An error occurred while fetching research suggestions: {ex.Message}"); + } + + return suggestions; + } + + + private object GetResponseFormat() + { + return new + { + type = "json_schema", + json_schema = new + { + name = "post_suggestions", + schema = new + { + type = "object", + properties = new + { + suggestions = new + { + type = "array", + items = new + { + type = "object", + properties = new + { + title = new { type = "string" }, + author = new { type = "string" }, + summary = new { type = "string", maxLength = 100 }, + publication_date = new { type = "string", format = "date" }, + url = new { type = "string" } + }, + required = new[] { "title", "summary", "url" } + } + } + }, + required = new[] { "post_suggestions" } + } + } + }; + } + + private async Task SaveToFile(string prefix, string responseContent) + { + string datetime = DateTime.Now.ToString("yyyy-MM-dd_HH-mm"); + string fileName = $"{prefix}_{datetime}.json"; + string folderPath = "Data"; + Directory.CreateDirectory(folderPath); + string filePath = Path.Combine(folderPath, fileName); + await File.WriteAllTextAsync(filePath, responseContent); + } + +} \ No newline at end of file diff --git a/NoteBookmark.AIServices/SummaryService.cs b/src/NoteBookmark.AIServices/SummaryService.cs similarity index 90% rename from NoteBookmark.AIServices/SummaryService.cs rename to src/NoteBookmark.AIServices/SummaryService.cs index e04b95c..9257aa3 100644 --- a/NoteBookmark.AIServices/SummaryService.cs +++ b/src/NoteBookmark.AIServices/SummaryService.cs @@ -4,6 +4,7 @@ using System.Linq; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Reka.SDK; namespace NoteBookmark.AIServices; @@ -15,10 +16,9 @@ public class SummaryService(HttpClient client, ILogger logger, I private const string MODEL_NAME = "reka-flash-3.1"; private readonly string _apiKey = config["AppSettings:REKA_API_KEY"] ?? Environment.GetEnvironmentVariable("REKA_API_KEY") ?? throw new InvalidOperationException("REKA_API_KEY environment variable is not set."); - public async Task GenerateSummaryAsync(string summaryText) + public async Task GenerateSummaryAsync(string prompt) { string introParagraph; - string query = $"write a short introduction paragraph, without using '—', for the blog post: {summaryText}"; _client.Timeout = TimeSpan.FromSeconds(300); @@ -31,7 +31,7 @@ public async Task GenerateSummaryAsync(string summaryText) new { role = "user", - content = query + content = prompt } } }; diff --git a/NoteBookmark.Api.Tests/Domain/NoteTests.cs b/src/NoteBookmark.Api.Tests/Domain/NoteTests.cs similarity index 100% rename from NoteBookmark.Api.Tests/Domain/NoteTests.cs rename to src/NoteBookmark.Api.Tests/Domain/NoteTests.cs diff --git a/NoteBookmark.Api.Tests/Domain/PostLTests.cs b/src/NoteBookmark.Api.Tests/Domain/PostLTests.cs similarity index 100% rename from NoteBookmark.Api.Tests/Domain/PostLTests.cs rename to src/NoteBookmark.Api.Tests/Domain/PostLTests.cs diff --git a/NoteBookmark.Api.Tests/Domain/PostTests.cs b/src/NoteBookmark.Api.Tests/Domain/PostTests.cs similarity index 100% rename from NoteBookmark.Api.Tests/Domain/PostTests.cs rename to src/NoteBookmark.Api.Tests/Domain/PostTests.cs diff --git a/NoteBookmark.Api.Tests/Domain/ReadingNoteTests.cs b/src/NoteBookmark.Api.Tests/Domain/ReadingNoteTests.cs similarity index 100% rename from NoteBookmark.Api.Tests/Domain/ReadingNoteTests.cs rename to src/NoteBookmark.Api.Tests/Domain/ReadingNoteTests.cs diff --git a/NoteBookmark.Api.Tests/Domain/ReadingNotesTests.cs b/src/NoteBookmark.Api.Tests/Domain/ReadingNotesTests.cs similarity index 100% rename from NoteBookmark.Api.Tests/Domain/ReadingNotesTests.cs rename to src/NoteBookmark.Api.Tests/Domain/ReadingNotesTests.cs diff --git a/NoteBookmark.Api.Tests/Domain/SettingsTests.cs b/src/NoteBookmark.Api.Tests/Domain/SettingsTests.cs similarity index 100% rename from NoteBookmark.Api.Tests/Domain/SettingsTests.cs rename to src/NoteBookmark.Api.Tests/Domain/SettingsTests.cs diff --git a/NoteBookmark.Api.Tests/Domain/SummaryTests.cs b/src/NoteBookmark.Api.Tests/Domain/SummaryTests.cs similarity index 100% rename from NoteBookmark.Api.Tests/Domain/SummaryTests.cs rename to src/NoteBookmark.Api.Tests/Domain/SummaryTests.cs diff --git a/NoteBookmark.Api.Tests/Endpoints/NoteEndpointsTests.cs b/src/NoteBookmark.Api.Tests/Endpoints/NoteEndpointsTests.cs similarity index 100% rename from NoteBookmark.Api.Tests/Endpoints/NoteEndpointsTests.cs rename to src/NoteBookmark.Api.Tests/Endpoints/NoteEndpointsTests.cs diff --git a/NoteBookmark.Api.Tests/Endpoints/PostEndpointsTests.cs b/src/NoteBookmark.Api.Tests/Endpoints/PostEndpointsTests.cs similarity index 100% rename from NoteBookmark.Api.Tests/Endpoints/PostEndpointsTests.cs rename to src/NoteBookmark.Api.Tests/Endpoints/PostEndpointsTests.cs diff --git a/NoteBookmark.Api.Tests/Endpoints/SettingEndpointsTests.cs b/src/NoteBookmark.Api.Tests/Endpoints/SettingEndpointsTests.cs similarity index 100% rename from NoteBookmark.Api.Tests/Endpoints/SettingEndpointsTests.cs rename to src/NoteBookmark.Api.Tests/Endpoints/SettingEndpointsTests.cs diff --git a/src/NoteBookmark.Api.Tests/Endpoints/SummaryEndpointsTests.cs b/src/NoteBookmark.Api.Tests/Endpoints/SummaryEndpointsTests.cs new file mode 100644 index 0000000..e69de29 diff --git a/NoteBookmark.Api.Tests/Fixtures/AzureStorageTestFixture.cs b/src/NoteBookmark.Api.Tests/Fixtures/AzureStorageTestFixture.cs similarity index 92% rename from NoteBookmark.Api.Tests/Fixtures/AzureStorageTestFixture.cs rename to src/NoteBookmark.Api.Tests/Fixtures/AzureStorageTestFixture.cs index 59ec531..54325a2 100644 --- a/NoteBookmark.Api.Tests/Fixtures/AzureStorageTestFixture.cs +++ b/src/NoteBookmark.Api.Tests/Fixtures/AzureStorageTestFixture.cs @@ -20,8 +20,10 @@ public class AzureStorageTestFixture : IAsyncLifetime public async Task InitializeAsync() { // Start Azurite container for integration testing + // Use --skipApiVersionCheck to allow newer Azure SDK versions _azuriteContainer = new AzuriteBuilder() .WithImage("mcr.microsoft.com/azure-storage/azurite:latest") + .WithCommand("--skipApiVersionCheck") .Build(); await _azuriteContainer.StartAsync(); diff --git a/NoteBookmark.Api.Tests/Fixtures/NoteBookmarkApiTestFactory.cs b/src/NoteBookmark.Api.Tests/Fixtures/NoteBookmarkApiTestFactory.cs similarity index 94% rename from NoteBookmark.Api.Tests/Fixtures/NoteBookmarkApiTestFactory.cs rename to src/NoteBookmark.Api.Tests/Fixtures/NoteBookmarkApiTestFactory.cs index 1f023cb..da53256 100644 --- a/NoteBookmark.Api.Tests/Fixtures/NoteBookmarkApiTestFactory.cs +++ b/src/NoteBookmark.Api.Tests/Fixtures/NoteBookmarkApiTestFactory.cs @@ -39,8 +39,10 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) public async Task InitializeAsync() { + // Use --skipApiVersionCheck to allow newer Azure SDK versions _azuriteContainer = new AzuriteBuilder() .WithImage("mcr.microsoft.com/azure-storage/azurite:latest") + .WithCommand("--skipApiVersionCheck") .Build(); await _azuriteContainer.StartAsync(); diff --git a/NoteBookmark.Api.Tests/Helpers/TestDataBuilder.cs b/src/NoteBookmark.Api.Tests/Helpers/TestDataBuilder.cs similarity index 100% rename from NoteBookmark.Api.Tests/Helpers/TestDataBuilder.cs rename to src/NoteBookmark.Api.Tests/Helpers/TestDataBuilder.cs diff --git a/NoteBookmark.Api.Tests/Integration/NoteBookmarkWorkflowTests.cs b/src/NoteBookmark.Api.Tests/Integration/NoteBookmarkWorkflowTests.cs similarity index 100% rename from NoteBookmark.Api.Tests/Integration/NoteBookmarkWorkflowTests.cs rename to src/NoteBookmark.Api.Tests/Integration/NoteBookmarkWorkflowTests.cs diff --git a/NoteBookmark.Api.Tests/NoteBookmark.Api.Tests.csproj b/src/NoteBookmark.Api.Tests/NoteBookmark.Api.Tests.csproj similarity index 95% rename from NoteBookmark.Api.Tests/NoteBookmark.Api.Tests.csproj rename to src/NoteBookmark.Api.Tests/NoteBookmark.Api.Tests.csproj index 9b25b0a..a69d6ea 100644 --- a/NoteBookmark.Api.Tests/NoteBookmark.Api.Tests.csproj +++ b/src/NoteBookmark.Api.Tests/NoteBookmark.Api.Tests.csproj @@ -13,7 +13,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -25,7 +25,7 @@ - + diff --git a/NoteBookmark.Api.Tests/Services/DataStorageServiceTests.cs b/src/NoteBookmark.Api.Tests/Services/DataStorageServiceTests.cs similarity index 100% rename from NoteBookmark.Api.Tests/Services/DataStorageServiceTests.cs rename to src/NoteBookmark.Api.Tests/Services/DataStorageServiceTests.cs diff --git a/NoteBookmark.Api/DataStorageService.cs b/src/NoteBookmark.Api/DataStorageService.cs similarity index 100% rename from NoteBookmark.Api/DataStorageService.cs rename to src/NoteBookmark.Api/DataStorageService.cs diff --git a/src/NoteBookmark.Api/Dockerfile b/src/NoteBookmark.Api/Dockerfile new file mode 100644 index 0000000..fc0996e --- /dev/null +++ b/src/NoteBookmark.Api/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +WORKDIR /app +EXPOSE 8000 +EXPOSE 8002 + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src +COPY ["src/NoteBookmark.Api/NoteBookmark.Api.csproj", "src/NoteBookmark.Api/"] +COPY ["src/NoteBookmark.Domain/NoteBookmark.Domain.csproj", "src/NoteBookmark.Domain/"] +COPY ["src/NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj", "src/NoteBookmark.ServiceDefaults/"] +COPY ["src/NoteBookmark.AIServices/NoteBookmark.AIServices.csproj", "src/NoteBookmark.AIServices/"] +RUN dotnet restore "src/NoteBookmark.Api/NoteBookmark.Api.csproj" +COPY . . +WORKDIR "/src/src/NoteBookmark.Api" +RUN dotnet build "NoteBookmark.Api.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "NoteBookmark.Api.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "NoteBookmark.Api.dll"] diff --git a/NoteBookmark.Api/IDataStorageService.cs b/src/NoteBookmark.Api/IDataStorageService.cs similarity index 100% rename from NoteBookmark.Api/IDataStorageService.cs rename to src/NoteBookmark.Api/IDataStorageService.cs diff --git a/NoteBookmark.Api/NoteBookmark.Api.csproj b/src/NoteBookmark.Api/NoteBookmark.Api.csproj similarity index 97% rename from NoteBookmark.Api/NoteBookmark.Api.csproj rename to src/NoteBookmark.Api/NoteBookmark.Api.csproj index af0ee3f..be07b08 100644 --- a/NoteBookmark.Api/NoteBookmark.Api.csproj +++ b/src/NoteBookmark.Api/NoteBookmark.Api.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/NoteBookmark.Api/NoteBookmark.Api.http b/src/NoteBookmark.Api/NoteBookmark.Api.http similarity index 100% rename from NoteBookmark.Api/NoteBookmark.Api.http rename to src/NoteBookmark.Api/NoteBookmark.Api.http diff --git a/NoteBookmark.Api/NoteEnpoints.cs b/src/NoteBookmark.Api/NoteEnpoints.cs similarity index 96% rename from NoteBookmark.Api/NoteEnpoints.cs rename to src/NoteBookmark.Api/NoteEnpoints.cs index ce4316a..77d36f3 100644 --- a/NoteBookmark.Api/NoteEnpoints.cs +++ b/src/NoteBookmark.Api/NoteEnpoints.cs @@ -1,118 +1,118 @@ -using System; -using Azure.Data.Tables; -using Azure.Storage.Blobs; -using Microsoft.AspNetCore.Http.HttpResults; -using NoteBookmark.Domain; - -namespace NoteBookmark.Api; - -public static class NoteEnpoints -{ - public static void MapNoteEndpoints(this IEndpointRouteBuilder app) - { - var endpoints = app.MapGroup("api/notes") - .WithOpenApi(); - - endpoints.MapPost("/note", CreateNote) - .WithDescription("Create a new note"); - - endpoints.MapGet("/", GetNotes) - .WithDescription("Get all unused reading notes with with the info about the related post."); - - endpoints.MapGet("/GetNotesForSummary/{ReadingNotesId}", GetNotesForSummary) - .WithDescription("Get all notes with the info about the related post for a specific reading notes summary."); - - endpoints.MapPost("/SaveReadingNotes", SaveReadingNotes) - .WithDescription("Create a new note"); - - endpoints.MapGet("/UpdatePostReadStatus", UpdatePostReadStatus) - .WithDescription("Update the read status of all posts to true if they have a note referencing them."); - } - - static Results, BadRequest> CreateNote(Note note, - TableServiceClient tblClient, - BlobServiceClient blobClient) - { - try - { - if (!note.Validate()) - { - return TypedResults.BadRequest(); - } - - var dataStorageService = new DataStorageService(tblClient, blobClient); - dataStorageService.CreateNote(note); - var post = dataStorageService.GetPost(note.PostId!); - if(post is not null) - { - post.is_read = true; - dataStorageService.SavePost(post); - } - return TypedResults.Created($"/api/notes/{note.RowKey}", note); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred while creating a note: {ex.Message}"); - return TypedResults.BadRequest(); - } - } - - static Results>, NotFound> GetNotes(TableServiceClient tblClient, - BlobServiceClient blobClient) - { - var dataStorageService = new DataStorageService(tblClient, blobClient); - var notes = dataStorageService.GetNotes(); - return notes == null ? TypedResults.NotFound() : TypedResults.Ok(notes); - } - - static Results>, NotFound> GetNotesForSummary(string ReadingNotesId, - TableServiceClient tblClient, - BlobServiceClient blobClient) - { - - var dataStorageService = new DataStorageService(tblClient, blobClient); - var notes = dataStorageService.GetNotesForSummary(ReadingNotesId); - return notes == null ? TypedResults.NotFound() : TypedResults.Ok(notes); - } - - private static async Task, BadRequest>> SaveReadingNotes(ReadingNotes readingNotes, - TableServiceClient tblClient, - BlobServiceClient blobClient) - { - try - { - if (readingNotes == null || string.IsNullOrWhiteSpace(readingNotes.Number) || string.IsNullOrWhiteSpace(readingNotes.Title)) - { - return TypedResults.BadRequest(); - } - var dataStorageService = new DataStorageService(tblClient, blobClient); - var url = await dataStorageService.SaveReadingNotes(readingNotes); - if (string.IsNullOrEmpty(url)) - { - return TypedResults.BadRequest(); - } - return TypedResults.Ok(url); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred while creating a note: {ex.Message}"); - return TypedResults.BadRequest(); - } - } - - private static async Task> UpdatePostReadStatus(TableServiceClient tblClient, - BlobServiceClient blobClient) - { - try - { - var dataStorageService = new DataStorageService(tblClient, blobClient); - await dataStorageService.UpdatePostReadStatus(); - return TypedResults.Ok(); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred while updating post read status: {ex.Message}"); - return TypedResults.BadRequest(); - } - } -} +using System; +using Azure.Data.Tables; +using Azure.Storage.Blobs; +using Microsoft.AspNetCore.Http.HttpResults; +using NoteBookmark.Domain; + +namespace NoteBookmark.Api; + +public static class NoteEnpoints +{ + public static void MapNoteEndpoints(this IEndpointRouteBuilder app) + { + var endpoints = app.MapGroup("api/notes") + .WithOpenApi(); + + endpoints.MapPost("/note", CreateNote) + .WithDescription("Create a new note"); + + endpoints.MapGet("/", GetNotes) + .WithDescription("Get all unused reading notes with with the info about the related post."); + + endpoints.MapGet("/GetNotesForSummary/{ReadingNotesId}", GetNotesForSummary) + .WithDescription("Get all notes with the info about the related post for a specific reading notes summary."); + + endpoints.MapPost("/SaveReadingNotes", SaveReadingNotes) + .WithDescription("Create a new note"); + + endpoints.MapGet("/UpdatePostReadStatus", UpdatePostReadStatus) + .WithDescription("Update the read status of all posts to true if they have a note referencing them."); + } + + static Results, BadRequest> CreateNote(Note note, + TableServiceClient tblClient, + BlobServiceClient blobClient) + { + try + { + if (!note.Validate()) + { + return TypedResults.BadRequest(); + } + + var dataStorageService = new DataStorageService(tblClient, blobClient); + dataStorageService.CreateNote(note); + var post = dataStorageService.GetPost(note.PostId!); + if(post is not null) + { + post.is_read = true; + dataStorageService.SavePost(post); + } + return TypedResults.Created($"/api/notes/{note.RowKey}", note); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred while creating a note: {ex.Message}"); + return TypedResults.BadRequest(); + } + } + + static Results>, NotFound> GetNotes(TableServiceClient tblClient, + BlobServiceClient blobClient) + { + var dataStorageService = new DataStorageService(tblClient, blobClient); + var notes = dataStorageService.GetNotes(); + return notes == null ? TypedResults.NotFound() : TypedResults.Ok(notes); + } + + static Results>, NotFound> GetNotesForSummary(string ReadingNotesId, + TableServiceClient tblClient, + BlobServiceClient blobClient) + { + + var dataStorageService = new DataStorageService(tblClient, blobClient); + var notes = dataStorageService.GetNotesForSummary(ReadingNotesId); + return notes == null ? TypedResults.NotFound() : TypedResults.Ok(notes); + } + + private static async Task, BadRequest>> SaveReadingNotes(ReadingNotes readingNotes, + TableServiceClient tblClient, + BlobServiceClient blobClient) + { + try + { + if (readingNotes == null || string.IsNullOrWhiteSpace(readingNotes.Number) || string.IsNullOrWhiteSpace(readingNotes.Title)) + { + return TypedResults.BadRequest(); + } + var dataStorageService = new DataStorageService(tblClient, blobClient); + var url = await dataStorageService.SaveReadingNotes(readingNotes); + if (string.IsNullOrEmpty(url)) + { + return TypedResults.BadRequest(); + } + return TypedResults.Ok(url); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred while creating a note: {ex.Message}"); + return TypedResults.BadRequest(); + } + } + + private static async Task> UpdatePostReadStatus(TableServiceClient tblClient, + BlobServiceClient blobClient) + { + try + { + var dataStorageService = new DataStorageService(tblClient, blobClient); + await dataStorageService.UpdatePostReadStatus(); + return TypedResults.Ok(); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred while updating post read status: {ex.Message}"); + return TypedResults.BadRequest(); + } + } +} diff --git a/NoteBookmark.Api/PostEndpoints.cs b/src/NoteBookmark.Api/PostEndpoints.cs similarity index 100% rename from NoteBookmark.Api/PostEndpoints.cs rename to src/NoteBookmark.Api/PostEndpoints.cs diff --git a/NoteBookmark.Api/Program.cs b/src/NoteBookmark.Api/Program.cs similarity index 100% rename from NoteBookmark.Api/Program.cs rename to src/NoteBookmark.Api/Program.cs diff --git a/NoteBookmark.Api/Properties/launchSettings.json b/src/NoteBookmark.Api/Properties/launchSettings.json similarity index 100% rename from NoteBookmark.Api/Properties/launchSettings.json rename to src/NoteBookmark.Api/Properties/launchSettings.json diff --git a/NoteBookmark.Api/SettingEndpoints.cs b/src/NoteBookmark.Api/SettingEndpoints.cs similarity index 85% rename from NoteBookmark.Api/SettingEndpoints.cs rename to src/NoteBookmark.Api/SettingEndpoints.cs index c64d3ac..6301b92 100644 --- a/NoteBookmark.Api/SettingEndpoints.cs +++ b/src/NoteBookmark.Api/SettingEndpoints.cs @@ -60,6 +60,17 @@ static async Task, BadRequest>> GetSettings(TableServiceCli { var dataStorageService = new DataStorageService(tblClient, blobClient); var settings = await dataStorageService.GetSettings(); + + if(settings!.SearchPrompt == null) + { + settings.SearchPrompt = "Provide interesting a list of 3 blog posts, published recently, that talks about the topic: {topic}."; + } + + if(settings.SummaryPrompt == null) + { + settings.SummaryPrompt = "write a short introduction paragraph, without using '—', for the blog post: {content}"; + } + return settings != null ? TypedResults.Ok(settings) : TypedResults.BadRequest(); } } diff --git a/NoteBookmark.Api/SummaryEndpoints.cs b/src/NoteBookmark.Api/SummaryEndpoints.cs similarity index 100% rename from NoteBookmark.Api/SummaryEndpoints.cs rename to src/NoteBookmark.Api/SummaryEndpoints.cs diff --git a/NoteBookmark.Api/appsettings.json b/src/NoteBookmark.Api/appsettings.json similarity index 100% rename from NoteBookmark.Api/appsettings.json rename to src/NoteBookmark.Api/appsettings.json diff --git a/NoteBookmark.AppHost/AppHost.cs b/src/NoteBookmark.AppHost/AppHost.cs similarity index 100% rename from NoteBookmark.AppHost/AppHost.cs rename to src/NoteBookmark.AppHost/AppHost.cs diff --git a/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj b/src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj similarity index 80% rename from NoteBookmark.AppHost/NoteBookmark.AppHost.csproj rename to src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj index 4412ddf..1f81dbe 100644 --- a/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj +++ b/src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj @@ -1,5 +1,5 @@ - + Exe net9.0 @@ -9,9 +9,9 @@ 0784f0a9-b1e6-4e65-8d31-00f1369f6d75 - - - + + + diff --git a/NoteBookmark.AppHost/Properties/launchSettings.json b/src/NoteBookmark.AppHost/Properties/launchSettings.json similarity index 100% rename from NoteBookmark.AppHost/Properties/launchSettings.json rename to src/NoteBookmark.AppHost/Properties/launchSettings.json diff --git a/NoteBookmark.AppHost/appsettings.json b/src/NoteBookmark.AppHost/appsettings.json similarity index 100% rename from NoteBookmark.AppHost/appsettings.json rename to src/NoteBookmark.AppHost/appsettings.json diff --git a/NoteBookmark.AppHost/infra/api.tmpl.yaml b/src/NoteBookmark.AppHost/infra/api.tmpl.yaml similarity index 100% rename from NoteBookmark.AppHost/infra/api.tmpl.yaml rename to src/NoteBookmark.AppHost/infra/api.tmpl.yaml diff --git a/NoteBookmark.AppHost/infra/blazor-app.tmpl.yaml b/src/NoteBookmark.AppHost/infra/blazor-app.tmpl.yaml similarity index 100% rename from NoteBookmark.AppHost/infra/blazor-app.tmpl.yaml rename to src/NoteBookmark.AppHost/infra/blazor-app.tmpl.yaml diff --git a/NoteBookmark.BlazorApp/Components/App.razor b/src/NoteBookmark.BlazorApp/Components/App.razor similarity index 96% rename from NoteBookmark.BlazorApp/Components/App.razor rename to src/NoteBookmark.BlazorApp/Components/App.razor index 78d17e3..f917029 100644 --- a/NoteBookmark.BlazorApp/Components/App.razor +++ b/src/NoteBookmark.BlazorApp/Components/App.razor @@ -1,22 +1,22 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/NoteBookmark.BlazorApp/Components/Layout/MainLayout.razor b/src/NoteBookmark.BlazorApp/Components/Layout/MainLayout.razor similarity index 96% rename from NoteBookmark.BlazorApp/Components/Layout/MainLayout.razor rename to src/NoteBookmark.BlazorApp/Components/Layout/MainLayout.razor index b008fbe..340eae8 100644 --- a/NoteBookmark.BlazorApp/Components/Layout/MainLayout.razor +++ b/src/NoteBookmark.BlazorApp/Components/Layout/MainLayout.razor @@ -1,45 +1,45 @@ -@inherits LayoutComponentBase -@using Microsoft.FluentUI.AspNetCore.Components -@using Microsoft.FluentUI.AspNetCore.Components.Extensions -@inject NavigationManager NavigationManager - - - - Note Bookmark - - - - - - - - -
- @Body -
-
-
- - Documentation and demos - - About Blazor - -
- - - - - -
- An unhandled error has occurred. - Reload - 🗙 -
- - - -@code { - - public DesignThemeModes Mode { get; set; } - public OfficeColor? OfficeColor { get; set; } -} +@inherits LayoutComponentBase +@using Microsoft.FluentUI.AspNetCore.Components +@using Microsoft.FluentUI.AspNetCore.Components.Extensions +@inject NavigationManager NavigationManager + + + + Note Bookmark + + + + + + + + +
+ @Body +
+
+
+ + Documentation and demos + + About Blazor + +
+ + + + + +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + +@code { + + public DesignThemeModes Mode { get; set; } + public OfficeColor? OfficeColor { get; set; } +} diff --git a/NoteBookmark.BlazorApp/Components/Layout/MinimalLayout.razor b/src/NoteBookmark.BlazorApp/Components/Layout/MinimalLayout.razor similarity index 100% rename from NoteBookmark.BlazorApp/Components/Layout/MinimalLayout.razor rename to src/NoteBookmark.BlazorApp/Components/Layout/MinimalLayout.razor diff --git a/NoteBookmark.BlazorApp/Components/Layout/NavMenu.razor b/src/NoteBookmark.BlazorApp/Components/Layout/NavMenu.razor similarity index 89% rename from NoteBookmark.BlazorApp/Components/Layout/NavMenu.razor rename to src/NoteBookmark.BlazorApp/Components/Layout/NavMenu.razor index 3fa220f..fa00510 100644 --- a/NoteBookmark.BlazorApp/Components/Layout/NavMenu.razor +++ b/src/NoteBookmark.BlazorApp/Components/Layout/NavMenu.razor @@ -9,6 +9,7 @@ Generate Summary Summaries Posts + Search diff --git a/NoteBookmark.BlazorApp/Components/Pages/Error.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Error.razor similarity index 97% rename from NoteBookmark.BlazorApp/Components/Pages/Error.razor rename to src/NoteBookmark.BlazorApp/Components/Pages/Error.razor index 7a84043..576cc2d 100644 --- a/NoteBookmark.BlazorApp/Components/Pages/Error.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Error.razor @@ -1,36 +1,36 @@ -@page "/Error" -@using System.Diagnostics - -Error - -

Error.

-

An error occurred while processing your request.

- -@if (ShowRequestId) -{ -

- Request ID: @RequestId -

-} - -

Development Mode

-

- Swapping to Development environment will display more detailed information about the error that occurred. -

-

- The Development environment shouldn't be enabled for deployed applications. - It can result in displaying sensitive information from exceptions to end users. - For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development - and restarting the app. -

- -@code{ - [CascadingParameter] - private HttpContext? HttpContext { get; set; } - - private string? RequestId { get; set; } - private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - - protected override void OnInitialized() => - RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; -} +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/Home.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Home.razor new file mode 100644 index 0000000..6f7e2e6 --- /dev/null +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Home.razor @@ -0,0 +1,66 @@ +@page "/" +@using Microsoft.FluentUI.AspNetCore.Components +@inject NavigationManager Navigation + +Home - NoteBookmark + + +

📚 NoteBookmark

+ + +

Your personal reading companion for capturing thoughts and insights from articles and blog posts. + Transform your reading notes into polished summaries, perfect for sharing your weekly discoveries.

+
+ + + + +
📝
+

Manage Posts

+

Collect articles to read and add your notes as you go through them.

+
+
+ + + +
🔍
+

AI-Powered Search

+

Discover relevant content with intelligent suggestions tailored to your interests.

+
+
+ + + +
+

Generate Summaries

+

Create beautiful summaries of your reading notes with AI assistance.

+
+
+
+ + + + +

Built with Modern Tech

+ + + .NET 9 + + + Blazor + + + Fluent UI Blazor + + + Aspire + + + Azure Table Storage + + + Reka AI + + +
+
\ No newline at end of file diff --git a/NoteBookmark.BlazorApp/Components/Pages/PostEditor.razor b/src/NoteBookmark.BlazorApp/Components/Pages/PostEditor.razor similarity index 100% rename from NoteBookmark.BlazorApp/Components/Pages/PostEditor.razor rename to src/NoteBookmark.BlazorApp/Components/Pages/PostEditor.razor diff --git a/NoteBookmark.BlazorApp/Components/Pages/PostEditorLight.razor b/src/NoteBookmark.BlazorApp/Components/Pages/PostEditorLight.razor similarity index 100% rename from NoteBookmark.BlazorApp/Components/Pages/PostEditorLight.razor rename to src/NoteBookmark.BlazorApp/Components/Pages/PostEditorLight.razor diff --git a/NoteBookmark.BlazorApp/Components/Pages/Posts.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Posts.razor similarity index 98% rename from NoteBookmark.BlazorApp/Components/Pages/Posts.razor rename to src/NoteBookmark.BlazorApp/Components/Pages/Posts.razor index 78500d9..e43e68f 100644 --- a/NoteBookmark.BlazorApp/Components/Pages/Posts.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Posts.razor @@ -24,7 +24,7 @@ - + @@ -50,7 +50,7 @@ Sortable="true" SortBy="@defSort" IsDefaultSortColumn="true" Width="125px"/> - + diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor new file mode 100644 index 0000000..9c4b02d --- /dev/null +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor @@ -0,0 +1,117 @@ +@page "/search" +@using NoteBookmark.AIServices +@using NoteBookmark.BlazorApp.Components.Shared +@using NoteBookmark.Domain +@using Microsoft.FluentUI.AspNetCore.Components +@inject PostNoteClient client +@* @inject IJSRuntime jsRuntime *@ +@inject IToastService toastService +@* @inject IDialogService DialogService *@ +@inject ResearchService aiService +@inject NavigationManager Navigation +@rendermode InteractiveServer + +Search + +

Search

+ + + + + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + @(isSearching ? "Searching..." : "Search") + +
+ +
+
+ +
+
+ @* + Read Only + UnRead Only + *@ + +
+ + + + +@code { + private List? suggestions; + private GridSort defSort = GridSort.ByDescending(c => c.PublicationDate); + private string newPostUrl = string.Empty; + private bool showRead = false; + private bool isSearching = false; + + private SearchCriterias _criterias = new SearchCriterias(string.Empty); + + + protected override async Task OnInitializedAsync() + { + Domain.Settings? settings = await client.GetSettings(); + if (settings != null) + { + _criterias = new SearchCriterias(settings.SearchPrompt); + _criterias.AllowedDomains = settings.FavoriteDomains; + _criterias.BlockedDomains = settings.BlockedDomains; + } + @* await LoadPosts(); *@ + } + + private async Task FetchSuggestions() + { + isSearching = true; + if (string.IsNullOrWhiteSpace(_criterias.SearchTopic)) + { + toastService.ShowError("Please enter a search prompt."); + isSearching = false; + return; + } + + try{ + + PostSuggestions result = await aiService.SearchSuggestionsAsync(_criterias); + suggestions = result.Suggestions ?? []; + StateHasChanged(); + } + catch(Exception ex) + { + toastService.ShowError($"Oops! Error: {ex.Message}"); + } + finally + { + isSearching = false; + } + } + + @* private async Task OpenUrlInNewWindow(string? url) + { + await jsRuntime.InvokeVoidAsync("open", url, "_blank"); + } *@ + + + + +} diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor new file mode 100644 index 0000000..4c57b86 --- /dev/null +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor @@ -0,0 +1,122 @@ +@page "/settings" + +@using Microsoft.FluentUI.AspNetCore.Components.Extensions +@using NoteBookmark.Domain +@inject ILogger Logger +@inject PostNoteClient client +@inject NavigationManager Navigation +@using NoteBookmark.BlazorApp + +@rendermode InteractiveServer + + + +

Settings

+ +
+ + + + + + + + + + + @context + + + + + +
+ +
+ +@if( settings != null) +{ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Save + + + +
+} + + +@code { + public DesignThemeModes Mode { get; set; } + public OfficeColor? OfficeColor { get; set; } + + private Domain.Settings? settings; + + protected override async Task OnInitializedAsync() + { + settings = await client.GetSettings(); + } + + private async Task SaveSettings() + { + if (settings != null) + { + await client.SaveSettings(settings); + Navigation.NavigateTo("/"); + } + } + + void OnLoaded(LoadedEventArgs e) + { + Logger.LogInformation($"Loaded: {(e.Mode == DesignThemeModes.System ? "System" : "")} {(e.IsDark ? "Dark" : "Light")}"); + } + + void OnLuminanceChanged(LuminanceChangedEventArgs e) + { + Logger.LogInformation($"Changed: {(e.Mode == DesignThemeModes.System ? "System" : "")} {(e.IsDark ? "Dark" : "Light")}"); + } + + private void IncrementCounter() + { + var cnt = Convert.ToInt32(settings!.ReadingNotesCounter)+1; + settings.ReadingNotesCounter = (cnt).ToString(); + } +} diff --git a/NoteBookmark.BlazorApp/Components/Pages/Summaries.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Summaries.razor similarity index 100% rename from NoteBookmark.BlazorApp/Components/Pages/Summaries.razor rename to src/NoteBookmark.BlazorApp/Components/Pages/Summaries.razor diff --git a/NoteBookmark.BlazorApp/Components/Pages/SummaryEditor.razor b/src/NoteBookmark.BlazorApp/Components/Pages/SummaryEditor.razor similarity index 97% rename from NoteBookmark.BlazorApp/Components/Pages/SummaryEditor.razor rename to src/NoteBookmark.BlazorApp/Components/Pages/SummaryEditor.razor index 881ef0c..c9bfd49 100644 --- a/NoteBookmark.BlazorApp/Components/Pages/SummaryEditor.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/SummaryEditor.razor @@ -1,271 +1,279 @@ -@page "/summaryeditor/{number?}" - -@using Markdig -@using NoteBookmark.Domain -@using NoteBookmark.AIServices - -@inject IDialogService DialogService -@inject PostNoteClient client -@inject IJSRuntime jsRuntime -@inject IToastService toastService -@inject SummaryService aiService - -@rendermode InteractiveServer - -

SummaryEditor

- -@if (readingNotes == null) -{ -

Loading...

-} -else{ - - - - - - - Save as Draft - - - -
- -
-
- -
-
- -
-
- -
- Generate - -
- @foreach (var note in readingNotes!.Notes) - { - string category = note.Key; - List rnList = note.Value; -
- -

@category

- -
- - @foreach (ReadingNote rn in rnList) - { -
- - - - - - - - - - - - - -
- - } - Add Note -
- } - Add Category -
- -
-
- -
- Publish | - Save as Draft -
- -
- - - - - - - - - -
- -} - - - -@code { - - [Parameter] - public string? number { get; set; } - private ReadingNotes? readingNotes; - private string? activeid = "tabEdit"; - private FluentTab? changedto; - - private string? readingNotesMD = string.Empty; - private string? readingNotesHTML = string.Empty; - - private bool isGenarating = false; - - protected override async Task OnInitializedAsync() - { - if(string.IsNullOrEmpty(number)) - { - readingNotes = await client.CreateReadingNotes(); - } - else - { - readingNotes = await client.GetReadingNotes(number); - } - } - - private async Task OpenUrlInNewWindow(string? url) - { - await jsRuntime.InvokeVoidAsync("open", url, "_blank"); - } - - private void AddExtraNote(string category) - { - readingNotes!.Notes[category].Add(new ReadingNote()); - } - - async Task AddExtraCategory() - { - var newCategory = string.Empty; - var dialogInstance = await DialogService.ShowDialogAsync(@
- -
- , new DialogParameters - { - Title = "New Category", - }); - var result = await dialogInstance.Result; - if (!result.Cancelled) - { - readingNotes!.Notes.Add(newCategory, new List {new ReadingNote()}); - } - } - - private async Task HandleValidSubmit() - { - if(readingNotes is not null){ - var result = await client.SaveReadingNotes(readingNotes); - if(result) - { - ShowConfirmationMessage(); - } - } - } - - private void HandleOnTabChange(FluentTab tab) - { - if(tab.Id == "tabMD") - { - readingNotesMD = readingNotes!.ToMarkDown(); - } - if(tab.Id == "tabHTML") - { - readingNotesHTML = Markdown.ToHtml(readingNotesMD!); - } - changedto = tab; - } - - private void ShowConfirmationMessage() - { - toastService.ShowSuccess("Summary saved successfully!"); - } - - private async Task DeleteCategory(string category) - { - DialogParameters parameters = new() - { - Title = $"Delete category: {category}?", - TrapFocus = true, - Modal = true, - PreventScroll = true - }; - var msg = $"Are you sure you want to delete this category and all ({readingNotes!.Notes[category].Count}) notes in it notes in it?"; - IDialogReference dialog = await DialogService.ShowDialogAsync(@
@msg
, parameters); - DialogResult? result = await dialog.Result; - - if (!result.Cancelled) - { - readingNotes!.Notes.Remove(category); - } - } - - private void DeleteReadingNote(string category, string RowKey) - { - readingNotes!.Notes[category].RemoveAll(x => x.RowKey == RowKey); - } - - private async Task Publish() - { - if (readingNotes is not null) - { - if (string.IsNullOrWhiteSpace(readingNotes.PublishedUrl)) - { - toastService.ShowError("Published Url is required to publish."); - return; - } - - // Save markdown to blob storage only if readingNotesMD is not null or empty - if (!string.IsNullOrWhiteSpace(readingNotesMD)) - { - var markdownSaved = await client.SaveReadingNotesMarkdown(readingNotesMD, readingNotes.Number); - if (!markdownSaved) - { - toastService.ShowError("Failed to save markdown file."); - return; - } - } - - await HandleValidSubmit(); - var settings = await client.GetSettings(); - if (settings is not null) - { - var cnt = Convert.ToInt32(settings!.ReadingNotesCounter); - // Only increment if the current Summary is the most recent one. - if (cnt == Convert.ToInt32(readingNotes!.Number)) - { - cnt++; - settings.ReadingNotesCounter = (cnt).ToString(); - await client.SaveSettings(settings); - } - } - - toastService.ShowSuccess("Reading notes published successfully!"); - } - } - - private async Task GenerateIntroParagraph() - { - isGenarating = true; - var summaryText = readingNotes!.ToMarkDown(); - try{ - string introText = await aiService.GenerateSummaryAsync(summaryText); - readingNotes.Intro = introText; - } - catch(Exception ex) - { - toastService.ShowError($"Oops! Error: {ex.Message}"); - } - finally - { - isGenarating = false; - } - } - -} - +@page "/summaryeditor/{number?}" + +@using Markdig +@using NoteBookmark.Domain +@using NoteBookmark.AIServices + +@inject IDialogService DialogService +@inject PostNoteClient client +@inject IJSRuntime jsRuntime +@inject IToastService toastService +@inject SummaryService aiService + +@rendermode InteractiveServer + +

SummaryEditor

+ +@if (readingNotes == null) +{ +

Loading...

+} +else{ + + + + + + + Save as Draft + + + +
+ +
+
+ +
+
+ +
+
+ +
+ Generate + +
+ @foreach (var note in readingNotes!.Notes) + { + string category = note.Key; + List rnList = note.Value; +
+ +

@category

+ +
+ + @foreach (ReadingNote rn in rnList) + { +
+ + + + + + + + + + + + + +
+ + } + Add Note +
+ } + Add Category +
+ +
+
+ +
+ Publish | + Save as Draft +
+ +
+ + + + + + + + + +
+ +} + + + +@code { + + [Parameter] + public string? number { get; set; } + private ReadingNotes? readingNotes; + private string? activeid = "tabEdit"; + private FluentTab? changedto; + + private string? readingNotesMD = string.Empty; + private string? readingNotesHTML = string.Empty; + + private bool isGenarating = false; + private string rawPrompt = string.Empty; + + protected override async Task OnInitializedAsync() + { + if(string.IsNullOrEmpty(number)) + { + readingNotes = await client.CreateReadingNotes(); + } + else + { + readingNotes = await client.GetReadingNotes(number); + } + + var settings = await client.GetSettings(); + if(settings != null) + { + rawPrompt = settings!.SummaryPrompt ?? string.Empty; + } + } + + private async Task OpenUrlInNewWindow(string? url) + { + await jsRuntime.InvokeVoidAsync("open", url, "_blank"); + } + + private void AddExtraNote(string category) + { + readingNotes!.Notes[category].Add(new ReadingNote()); + } + + async Task AddExtraCategory() + { + var newCategory = string.Empty; + var dialogInstance = await DialogService.ShowDialogAsync(@
+ +
+ , new DialogParameters + { + Title = "New Category", + }); + var result = await dialogInstance.Result; + if (!result.Cancelled) + { + readingNotes!.Notes.Add(newCategory, new List {new ReadingNote()}); + } + } + + private async Task HandleValidSubmit() + { + if(readingNotes is not null){ + var result = await client.SaveReadingNotes(readingNotes); + if(result) + { + ShowConfirmationMessage(); + } + } + } + + private void HandleOnTabChange(FluentTab tab) + { + if(tab.Id == "tabMD") + { + readingNotesMD = readingNotes!.ToMarkDown(); + } + if(tab.Id == "tabHTML") + { + readingNotesHTML = Markdown.ToHtml(readingNotesMD!); + } + changedto = tab; + } + + private void ShowConfirmationMessage() + { + toastService.ShowSuccess("Summary saved successfully!"); + } + + private async Task DeleteCategory(string category) + { + DialogParameters parameters = new() + { + Title = $"Delete category: {category}?", + TrapFocus = true, + Modal = true, + PreventScroll = true + }; + var msg = $"Are you sure you want to delete this category and all ({readingNotes!.Notes[category].Count}) notes in it notes in it?"; + IDialogReference dialog = await DialogService.ShowDialogAsync(@
@msg
, parameters); + DialogResult? result = await dialog.Result; + + if (!result.Cancelled) + { + readingNotes!.Notes.Remove(category); + } + } + + private void DeleteReadingNote(string category, string RowKey) + { + readingNotes!.Notes[category].RemoveAll(x => x.RowKey == RowKey); + } + + private async Task Publish() + { + if (readingNotes is not null) + { + if (string.IsNullOrWhiteSpace(readingNotes.PublishedUrl)) + { + toastService.ShowError("Published Url is required to publish."); + return; + } + + // Save markdown to blob storage only if readingNotesMD is not null or empty + if (!string.IsNullOrWhiteSpace(readingNotesMD)) + { + var markdownSaved = await client.SaveReadingNotesMarkdown(readingNotesMD, readingNotes.Number); + if (!markdownSaved) + { + toastService.ShowError("Failed to save markdown file."); + return; + } + } + + await HandleValidSubmit(); + var settings = await client.GetSettings(); + if (settings is not null) + { + var cnt = Convert.ToInt32(settings!.ReadingNotesCounter); + // Only increment if the current Summary is the most recent one. + if (cnt == Convert.ToInt32(readingNotes!.Number)) + { + cnt++; + settings.ReadingNotesCounter = (cnt).ToString(); + await client.SaveSettings(settings); + } + } + + toastService.ShowSuccess("Reading notes published successfully!"); + } + } + + private async Task GenerateIntroParagraph() + { + isGenarating = true; + var summaryText = readingNotes!.ToMarkDown(); + try{ + string prompt = rawPrompt.Replace("{content}", summaryText); + string introText = await aiService.GenerateSummaryAsync(prompt); + readingNotes.Intro = introText; + } + catch(Exception ex) + { + toastService.ShowError($"Oops! Error: {ex.Message}"); + } + finally + { + isGenarating = false; + } + } + +} + diff --git a/NoteBookmark.BlazorApp/Components/Routes.razor b/src/NoteBookmark.BlazorApp/Components/Routes.razor similarity index 97% rename from NoteBookmark.BlazorApp/Components/Routes.razor rename to src/NoteBookmark.BlazorApp/Components/Routes.razor index 33f2c7a..4d3379c 100644 --- a/NoteBookmark.BlazorApp/Components/Routes.razor +++ b/src/NoteBookmark.BlazorApp/Components/Routes.razor @@ -1,8 +1,8 @@ - - - - - - - - + + + + + + + + diff --git a/NoteBookmark.BlazorApp/Components/Shared/NoteDialog.razor b/src/NoteBookmark.BlazorApp/Components/Shared/NoteDialog.razor similarity index 100% rename from NoteBookmark.BlazorApp/Components/Shared/NoteDialog.razor rename to src/NoteBookmark.BlazorApp/Components/Shared/NoteDialog.razor diff --git a/src/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor b/src/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor new file mode 100644 index 0000000..e07a60d --- /dev/null +++ b/src/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor @@ -0,0 +1,109 @@ +@using NoteBookmark.Domain +@using Microsoft.FluentUI.AspNetCore.Components +@inject IToastService toastService +@inject IDialogService DialogService + +@inject PostNoteClient client + + +

Suggestions

+ + + + + + + + + + + + + + + +   Nothing to see here. Carry on! + + + + +@code { + + + [Parameter] + public List? Suggestions { get; set; } + + private PaginationState pagination = new PaginationState { ItemsPerPage = 20 }; + private string titleFilter = string.Empty; + + IQueryable? filteredUrlList => Suggestions? + .Where(x => x.Title!.Contains(titleFilter, StringComparison.CurrentCultureIgnoreCase)) + .AsQueryable(); + + + private GridSort defSort = GridSort.ByDescending(c => c.PublicationDate); + private string newPostUrl = string.Empty; + private bool showRead = false; + + + + private async Task AddSuggestion(string postURL) + { + if (postURL != null) + { + var result = await client.ExtractPostDetailsAndSave(postURL); + if (result != null) + { + Suggestions!.Remove(Suggestions.First(x => x.Url == postURL)); + StateHasChanged(); + toastService.ShowSuccess("Suggestion added as note successfully!"); + } + else + { + toastService.ShowError("Failed to add suggestion as note. Please try again."); + } + } + else + { + toastService.ShowError("Suggestion not found. Please try again."); + } + } + + private async Task DeleteSuggestion(string postURL) + { + var sug = Suggestions?.FirstOrDefault(x => x.Url == postURL); + if (sug != null) + { + Suggestions!.Remove(sug); + StateHasChanged(); + toastService.ShowSuccess("Suggestion deleted successfully!"); + } + else + { + toastService.ShowError("Failed to delete suggestion. Please try again."); + } + } + + + private void HandleTitleFilter(ChangeEventArgs args) + { + if (args.Value is string value) + { + titleFilter = value; + } + } + + private void HandleClearTitleFilter() + { + if (string.IsNullOrWhiteSpace(titleFilter)) + { + titleFilter = string.Empty; + } + } +} diff --git a/NoteBookmark.BlazorApp/Components/_Imports.razor b/src/NoteBookmark.BlazorApp/Components/_Imports.razor similarity index 97% rename from NoteBookmark.BlazorApp/Components/_Imports.razor rename to src/NoteBookmark.BlazorApp/Components/_Imports.razor index 011c2f3..c9d927d 100644 --- a/NoteBookmark.BlazorApp/Components/_Imports.razor +++ b/src/NoteBookmark.BlazorApp/Components/_Imports.razor @@ -1,12 +1,12 @@ -@using System.Net.Http -@using System.Net.Http.Json -@using Microsoft.AspNetCore.Components.Forms -@using Microsoft.AspNetCore.Components.Routing -@using Microsoft.AspNetCore.Components.Web -@using static Microsoft.AspNetCore.Components.Web.RenderMode -@using Microsoft.AspNetCore.Components.Web.Virtualization -@using Microsoft.FluentUI.AspNetCore.Components -@using Microsoft.JSInterop -@using NoteBookmark.BlazorApp -@using NoteBookmark.BlazorApp.Components +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.FluentUI.AspNetCore.Components +@using Microsoft.JSInterop +@using NoteBookmark.BlazorApp +@using NoteBookmark.BlazorApp.Components @using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons \ No newline at end of file diff --git a/NoteBookmark.BlazorApp/Dockerfile b/src/NoteBookmark.BlazorApp/Dockerfile similarity index 51% rename from NoteBookmark.BlazorApp/Dockerfile rename to src/NoteBookmark.BlazorApp/Dockerfile index 37178f3..5e4db56 100644 --- a/NoteBookmark.BlazorApp/Dockerfile +++ b/src/NoteBookmark.BlazorApp/Dockerfile @@ -5,12 +5,12 @@ EXPOSE 8006 FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /src -COPY ["NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj", "NoteBookmark.BlazorApp/"] -COPY ["NoteBookmark.Domain/NoteBookmark.Domain.csproj", "NoteBookmark.Domain/"] -COPY ["NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj", "NoteBookmark.ServiceDefaults/"] -RUN dotnet restore "NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj" +COPY ["src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj", "src/NoteBookmark.BlazorApp/"] +COPY ["src/NoteBookmark.Domain/NoteBookmark.Domain.csproj", "src/NoteBookmark.Domain/"] +COPY ["src/NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj", "src/NoteBookmark.ServiceDefaults/"] +RUN dotnet restore "src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj" COPY . . -WORKDIR "/src/NoteBookmark.BlazorApp" +WORKDIR "/src/src/NoteBookmark.BlazorApp" RUN dotnet build "NoteBookmark.BlazorApp.csproj" -c Release -o /app/build FROM build AS publish diff --git a/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj b/src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj similarity index 90% rename from NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj rename to src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj index 0bf5ea3..be4bd25 100644 --- a/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj +++ b/src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj @@ -7,7 +7,7 @@ - + diff --git a/NoteBookmark.BlazorApp/PostNoteClient.cs b/src/NoteBookmark.BlazorApp/PostNoteClient.cs similarity index 100% rename from NoteBookmark.BlazorApp/PostNoteClient.cs rename to src/NoteBookmark.BlazorApp/PostNoteClient.cs diff --git a/NoteBookmark.BlazorApp/Program.cs b/src/NoteBookmark.BlazorApp/Program.cs similarity index 56% rename from NoteBookmark.BlazorApp/Program.cs rename to src/NoteBookmark.BlazorApp/Program.cs index d172ab1..896b180 100644 --- a/NoteBookmark.BlazorApp/Program.cs +++ b/src/NoteBookmark.BlazorApp/Program.cs @@ -7,6 +7,26 @@ builder.AddServiceDefaults(); +// Register ResearchService with a manual HttpClient to bypass Aspire resilience policies +// builder.Services.AddTransient(sp => +// { +// var handler = new SocketsHttpHandler +// { +// PooledConnectionLifetime = TimeSpan.FromMinutes(5), +// ConnectTimeout = TimeSpan.FromMinutes(5) +// }; + +// var httpClient = new HttpClient(handler) +// { +// Timeout = TimeSpan.FromMinutes(5) +// }; + +// var logger = sp.GetRequiredService>(); +// var config = sp.GetRequiredService(); + +// return new ResearchService(httpClient, logger, config); +// }); + builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri("https+http://api"); @@ -14,9 +34,14 @@ builder.Services.AddHttpClient(client => { - client.Timeout = TimeSpan.FromSeconds(300); // Set to 5 minutes, adjust as needed + client.Timeout = TimeSpan.FromMinutes(5); }); + +builder.Services.AddHttpClient(); + // .AddStandardResilienceHandler(); + + // Add services to the container. builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); diff --git a/NoteBookmark.BlazorApp/Properties/launchSettings.json b/src/NoteBookmark.BlazorApp/Properties/launchSettings.json similarity index 96% rename from NoteBookmark.BlazorApp/Properties/launchSettings.json rename to src/NoteBookmark.BlazorApp/Properties/launchSettings.json index 94b74b2..620041d 100644 --- a/NoteBookmark.BlazorApp/Properties/launchSettings.json +++ b/src/NoteBookmark.BlazorApp/Properties/launchSettings.json @@ -1,38 +1,38 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:50993", - "sslPort": 44325 - } - }, - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "http://localhost:5031", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "https://localhost:7076;http://localhost:5031", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": false, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } - } +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:50993", + "sslPort": 44325 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5031", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7076;http://localhost:5031", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/NoteBookmark.BlazorApp/appsettings.json b/src/NoteBookmark.BlazorApp/appsettings.json similarity index 94% rename from NoteBookmark.BlazorApp/appsettings.json rename to src/NoteBookmark.BlazorApp/appsettings.json index 4d56694..10f68b8 100644 --- a/NoteBookmark.BlazorApp/appsettings.json +++ b/src/NoteBookmark.BlazorApp/appsettings.json @@ -1,9 +1,9 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/NoteBookmark.BlazorApp/wwwroot/app.css b/src/NoteBookmark.BlazorApp/wwwroot/app.css similarity index 96% rename from NoteBookmark.BlazorApp/wwwroot/app.css rename to src/NoteBookmark.BlazorApp/wwwroot/app.css index 9f0d723..04dbec4 100644 --- a/NoteBookmark.BlazorApp/wwwroot/app.css +++ b/src/NoteBookmark.BlazorApp/wwwroot/app.css @@ -1,198 +1,198 @@ -@import '/_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css'; - -body { - --body-font: "Segoe UI Variable", "Segoe UI", sans-serif; - margin: 0; - padding: 0; - height: 100vh; - font-family: var(--body-font); - font-size: var(--type-ramp-base-font-size); - line-height: var(--type-ramp-base-line-height); - font-weight: var(--font-weight); - color: var(--neutral-foreground-rest); - background: var(--neutral-fill-layer-rest); -} - -.navmenu-icon { - display: none; -} - -.main { - min-height: calc(100dvh - 86px); - color: var(--neutral-foreground-rest); - align-items: stretch !important; -} - -.body-content { - align-self: stretch; - height: calc(100dvh - 86px) !important; - display: flex; -} - -.content { - padding: 0.5rem 1.5rem; - align-self: stretch !important; - width: 100%; -} - -.manage { - width: 100dvw; -} - -footer { - background: var(--neutral-layer-4); - color: var(--neutral-foreground-rest); - align-items: center; - padding: 10px 10px; -} - - footer a { - color: var(--neutral-foreground-rest); - text-decoration: none; - } - - footer a:focus { - outline: 1px dashed; - outline-offset: 3px; - } - - footer a:hover { - text-decoration: underline; - } - -.alert { - border: 1px dashed var(--accent-fill-rest); - padding: 5px; -} - - -#blazor-error-ui { - background: lightyellow; - bottom: 0; - box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); - display: none; - left: 0; - padding: 0.6rem 1.25rem 0.7rem 1.25rem; - position: fixed; - width: 100%; - z-index: 1000; - margin: 20px 0; -} - - #blazor-error-ui .dismiss { - cursor: pointer; - position: absolute; - right: 0.75rem; - top: 0.5rem; - } - -.blazor-error-boundary { - background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; - padding: 1rem 1rem 1rem 3.7rem; - color: white; -} - - .blazor-error-boundary::before { - content: "An error has occurred. " - } - -.loading-progress { - position: relative; - display: block; - width: 8rem; - height: 8rem; - margin: 20vh auto 1rem auto; -} - - .loading-progress circle { - fill: none; - stroke: #e0e0e0; - stroke-width: 0.6rem; - transform-origin: 50% 50%; - transform: rotate(-90deg); - } - - .loading-progress circle:last-child { - stroke: #1b6ec2; - stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; - transition: stroke-dasharray 0.05s ease-in-out; - } - -.loading-progress-text { - position: absolute; - text-align: center; - font-weight: bold; - inset: calc(20vh + 3.25rem) 0 auto 0.2rem; -} - - .loading-progress-text:after { - content: var(--blazor-load-percentage-text, "Loading"); - } - -code { - color: #c02d76; -} - -@media (max-width: 600px) { - .header-gutters { - margin: 0.5rem 3rem 0.5rem 1.5rem !important; - } - - [dir="rtl"] .header-gutters { - margin: 0.5rem 1.5rem 0.5rem 3rem !important; - } - - .main { - flex-direction: column !important; - row-gap: 0 !important; - } - - nav.sitenav { - width: 100%; - height: 100%; - } - - #main-menu { - width: 100% !important; - } - - #main-menu > div:first-child:is(.expander) { - display: none; - } - - .navmenu { - width: 100%; - } - - #navmenu-toggle { - appearance: none; - } - - #navmenu-toggle ~ nav { - display: none; - } - - #navmenu-toggle:checked ~ nav { - display: block; - } - - .navmenu-icon { - cursor: pointer; - z-index: 10; - display: block; - position: absolute; - top: 15px; - left: unset; - right: 20px; - width: 20px; - height: 20px; - border: none; - } - - [dir="rtl"] .navmenu-icon { - left: 20px; - right: unset; - } -} - - +@import '/_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css'; + +body { + --body-font: "Segoe UI Variable", "Segoe UI", sans-serif; + margin: 0; + padding: 0; + height: 100vh; + font-family: var(--body-font); + font-size: var(--type-ramp-base-font-size); + line-height: var(--type-ramp-base-line-height); + font-weight: var(--font-weight); + color: var(--neutral-foreground-rest); + background: var(--neutral-fill-layer-rest); +} + +.navmenu-icon { + display: none; +} + +.main { + min-height: calc(100dvh - 86px); + color: var(--neutral-foreground-rest); + align-items: stretch !important; +} + +.body-content { + align-self: stretch; + height: calc(100dvh - 86px) !important; + display: flex; +} + +.content { + padding: 0.5rem 1.5rem; + align-self: stretch !important; + width: 100%; +} + +.manage { + width: 100dvw; +} + +footer { + background: var(--neutral-layer-4); + color: var(--neutral-foreground-rest); + align-items: center; + padding: 10px 10px; +} + + footer a { + color: var(--neutral-foreground-rest); + text-decoration: none; + } + + footer a:focus { + outline: 1px dashed; + outline-offset: 3px; + } + + footer a:hover { + text-decoration: underline; + } + +.alert { + border: 1px dashed var(--accent-fill-rest); + padding: 5px; +} + + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; + margin: 20px 0; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::before { + content: "An error has occurred. " + } + +.loading-progress { + position: relative; + display: block; + width: 8rem; + height: 8rem; + margin: 20vh auto 1rem auto; +} + + .loading-progress circle { + fill: none; + stroke: #e0e0e0; + stroke-width: 0.6rem; + transform-origin: 50% 50%; + transform: rotate(-90deg); + } + + .loading-progress circle:last-child { + stroke: #1b6ec2; + stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; + transition: stroke-dasharray 0.05s ease-in-out; + } + +.loading-progress-text { + position: absolute; + text-align: center; + font-weight: bold; + inset: calc(20vh + 3.25rem) 0 auto 0.2rem; +} + + .loading-progress-text:after { + content: var(--blazor-load-percentage-text, "Loading"); + } + +code { + color: #c02d76; +} + +@media (max-width: 600px) { + .header-gutters { + margin: 0.5rem 3rem 0.5rem 1.5rem !important; + } + + [dir="rtl"] .header-gutters { + margin: 0.5rem 1.5rem 0.5rem 3rem !important; + } + + .main { + flex-direction: column !important; + row-gap: 0 !important; + } + + nav.sitenav { + width: 100%; + height: 100%; + } + + #main-menu { + width: 100% !important; + } + + #main-menu > div:first-child:is(.expander) { + display: none; + } + + .navmenu { + width: 100%; + } + + #navmenu-toggle { + appearance: none; + } + + #navmenu-toggle ~ nav { + display: none; + } + + #navmenu-toggle:checked ~ nav { + display: block; + } + + .navmenu-icon { + cursor: pointer; + z-index: 10; + display: block; + position: absolute; + top: 15px; + left: unset; + right: 20px; + width: 20px; + height: 20px; + border: none; + } + + [dir="rtl"] .navmenu-icon { + left: 20px; + right: unset; + } +} + + diff --git a/NoteBookmark.BlazorApp/wwwroot/favicon.ico b/src/NoteBookmark.BlazorApp/wwwroot/favicon.ico similarity index 100% rename from NoteBookmark.BlazorApp/wwwroot/favicon.ico rename to src/NoteBookmark.BlazorApp/wwwroot/favicon.ico diff --git a/src/NoteBookmark.Domain/ContainsPlaceholderAttribute.cs b/src/NoteBookmark.Domain/ContainsPlaceholderAttribute.cs new file mode 100644 index 0000000..87dfafa --- /dev/null +++ b/src/NoteBookmark.Domain/ContainsPlaceholderAttribute.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.DataAnnotations; + +namespace NoteBookmark.Domain; + +public class ContainsPlaceholderAttribute : ValidationAttribute +{ + private readonly string _placeholder; + + public ContainsPlaceholderAttribute(string placeholder) + { + _placeholder = placeholder; + ErrorMessage = $"The field must contain '{{{placeholder}}}'."; + } + + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (value == null || string.IsNullOrWhiteSpace(value.ToString())) + { + return ValidationResult.Success; + } + + string stringValue = value.ToString()!; + + if (!stringValue.Contains($"{{{_placeholder}}}")) + { + return new ValidationResult(ErrorMessage); + } + + return ValidationResult.Success; + } +} diff --git a/NoteBookmark.Domain/Note.cs b/src/NoteBookmark.Domain/Note.cs similarity index 100% rename from NoteBookmark.Domain/Note.cs rename to src/NoteBookmark.Domain/Note.cs diff --git a/NoteBookmark.Domain/NoteBookmark.Domain.csproj b/src/NoteBookmark.Domain/NoteBookmark.Domain.csproj similarity index 100% rename from NoteBookmark.Domain/NoteBookmark.Domain.csproj rename to src/NoteBookmark.Domain/NoteBookmark.Domain.csproj diff --git a/NoteBookmark.Domain/NoteCategories.cs b/src/NoteBookmark.Domain/NoteCategories.cs similarity index 97% rename from NoteBookmark.Domain/NoteCategories.cs rename to src/NoteBookmark.Domain/NoteCategories.cs index 8ab2457..383f1c4 100644 --- a/NoteBookmark.Domain/NoteCategories.cs +++ b/src/NoteBookmark.Domain/NoteCategories.cs @@ -1,58 +1,58 @@ -using System; - -namespace NoteBookmark.Domain; - -/// -/// Notes Categories -/// Legacy code from the original ReadingNotres project. -/// -public static class NoteCategories -{ - /// - /// Get a dictionary to change the short version by the long version of category name. - /// - public static string GetCategory(string? category) - { - category = category?.ToLower(); - var categories = new Dictionary - { - {"ai", "AI"}, - {"cloud", "Cloud"}, - {"data", "Data"}, - {"database", "Databases"}, - {"dev", "Programming"}, - {"devops", "DevOps"}, - {"lowcode", "LowCode"}, - {"misc", "Miscellaneous"}, - {"top", "Suggestion of the week"}, - {"oss", "Open Source"}, - {"del", "del"} - }; - if (!String.IsNullOrEmpty(category) && categories.ContainsKey(category)) - { - return categories[category]; - } - else - { - return categories["misc"]; - } - } - - public static List GetCategories() - { - return new List - { - "AI", - "Cloud", - "Data", - "Databases", - "DevOps", - "LowCode", - "Miscellaneous", - "Programming", - "Open Source", - "Suggestion of the week", - "del" - }; - } -} +using System; + +namespace NoteBookmark.Domain; + +/// +/// Notes Categories +/// Legacy code from the original ReadingNotres project. +/// +public static class NoteCategories +{ + /// + /// Get a dictionary to change the short version by the long version of category name. + /// + public static string GetCategory(string? category) + { + category = category?.ToLower(); + var categories = new Dictionary + { + {"ai", "AI"}, + {"cloud", "Cloud"}, + {"data", "Data"}, + {"database", "Databases"}, + {"dev", "Programming"}, + {"devops", "DevOps"}, + {"lowcode", "LowCode"}, + {"misc", "Miscellaneous"}, + {"top", "Suggestion of the week"}, + {"oss", "Open Source"}, + {"del", "del"} + }; + if (!String.IsNullOrEmpty(category) && categories.ContainsKey(category)) + { + return categories[category]; + } + else + { + return categories["misc"]; + } + } + + public static List GetCategories() + { + return new List + { + "AI", + "Cloud", + "Data", + "Databases", + "DevOps", + "LowCode", + "Miscellaneous", + "Programming", + "Open Source", + "Suggestion of the week", + "del" + }; + } +} diff --git a/NoteBookmark.Domain/Post.cs b/src/NoteBookmark.Domain/Post.cs similarity index 100% rename from NoteBookmark.Domain/Post.cs rename to src/NoteBookmark.Domain/Post.cs diff --git a/NoteBookmark.Domain/PostL.cs b/src/NoteBookmark.Domain/PostL.cs similarity index 100% rename from NoteBookmark.Domain/PostL.cs rename to src/NoteBookmark.Domain/PostL.cs diff --git a/src/NoteBookmark.Domain/PostSuggestion.cs b/src/NoteBookmark.Domain/PostSuggestion.cs new file mode 100644 index 0000000..f6fe06a --- /dev/null +++ b/src/NoteBookmark.Domain/PostSuggestion.cs @@ -0,0 +1,55 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace NoteBookmark.Domain; + +public class PostSuggestion +{ + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; + + [JsonPropertyName("author")] + public string? Author { get; set; } + + [JsonPropertyName("summary")] + public string Summary { get; set; } = string.Empty; + + [JsonPropertyName("publication_date")] + [JsonConverter(typeof(DateOnlyJsonConverter))] + public string? PublicationDate { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } = string.Empty; +} + +public class DateOnlyJsonConverter : JsonConverter +{ + private const string DateFormat = "yyyy-MM-dd"; + + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + + var dateString = reader.GetString(); + if (string.IsNullOrEmpty(dateString)) + return null; + + if (DateTime.TryParse(dateString, out var date)) + { + return date.ToString(DateFormat); + } + return dateString; + } + + public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStringValue(value); + } +} diff --git a/src/NoteBookmark.Domain/PostSuggestions.cs b/src/NoteBookmark.Domain/PostSuggestions.cs new file mode 100644 index 0000000..4a71deb --- /dev/null +++ b/src/NoteBookmark.Domain/PostSuggestions.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace NoteBookmark.Domain; + +public class PostSuggestions +{ + [JsonPropertyName("suggestions")] + public List? Suggestions { get; set; } +} diff --git a/NoteBookmark.Domain/ReadingNote.cs b/src/NoteBookmark.Domain/ReadingNote.cs similarity index 100% rename from NoteBookmark.Domain/ReadingNote.cs rename to src/NoteBookmark.Domain/ReadingNote.cs diff --git a/NoteBookmark.Domain/ReadingNotes.cs b/src/NoteBookmark.Domain/ReadingNotes.cs similarity index 96% rename from NoteBookmark.Domain/ReadingNotes.cs rename to src/NoteBookmark.Domain/ReadingNotes.cs index 103e512..77089ab 100644 --- a/NoteBookmark.Domain/ReadingNotes.cs +++ b/src/NoteBookmark.Domain/ReadingNotes.cs @@ -1,91 +1,91 @@ - -using System.Text; - - - -namespace NoteBookmark.Domain; - -public class ReadingNotes -{ - public ReadingNotes(string number) - { - this.Number = number; - this.Title = $"Reading Notes #{number}"; - this.Notes = new Dictionary>(); - } - - - // public ReadingNotes(string jsonFilePath) - // { - // var jsonString = File.ReadAllText(jsonFilePath); - // var readingNotes = JsonSerializer.Deserialize(jsonString); - - // if (readingNotes != null) - // { - // this.Number = readingNotes.Number; - // this.Title = readingNotes.Title; - // this.Tags = readingNotes.Tags; - // this.Intro = readingNotes.Intro; - // this.Notes = readingNotes.Notes; - // } - // } - - public string Number { get; set; } - public string Title { get; set; } = string.Empty; - //public string Filename { get; set; } = string.Empty; - public string PublishedUrl { get; set; } = string.Empty; - public string Tags { get; set; } = string.Empty; - public string Intro { get; set; } = string.Empty; - public Dictionary> Notes { get; set; } - - - public string GetAllUniqueTags(){ - var uniqueTags = new HashSet(); - - foreach (var category in Notes.Values) - { - foreach (var note in category) - { - if (!string.IsNullOrEmpty(note.Tags)) - { - var tags = note.Tags.ToLower().Split('.'); - uniqueTags = uniqueTags.Concat(tags).ToHashSet(); - } - } - } - uniqueTags.Add("readingnotes"); - return String.Join(",", uniqueTags.OrderBy(n => n).ToArray()); - } - - public string ToMarkDown() - { - - var md = new StringBuilder(); - - //== YAML header - md.AppendFormat("---{0}", Environment.NewLine); - md.Append(string.Format("Title: {0}{1}", Title, Environment.NewLine)); - md.Append(string.Format("Tags: {0}{1}", Tags, Environment.NewLine)); - md.AppendFormat("---{0}", Environment.NewLine); - - md.Append(Title + Environment.NewLine); - md.Append('=', Title.Length); - - md.Append(Environment.NewLine + Environment.NewLine + (this.Intro ?? "") + Environment.NewLine); - - //== All Notes - foreach (var key in this.Notes.Keys) - { - - md.AppendFormat("{0}{0}## {1}{0}{0}", Environment.NewLine, key); - - foreach (var note in ((List)Notes[key])) - { - md.Append(note.ToMarkDown() + Environment.NewLine); - } - - } - - return md.ToString(); - } -} + +using System.Text; + + + +namespace NoteBookmark.Domain; + +public class ReadingNotes +{ + public ReadingNotes(string number) + { + this.Number = number; + this.Title = $"Reading Notes #{number}"; + this.Notes = new Dictionary>(); + } + + + // public ReadingNotes(string jsonFilePath) + // { + // var jsonString = File.ReadAllText(jsonFilePath); + // var readingNotes = JsonSerializer.Deserialize(jsonString); + + // if (readingNotes != null) + // { + // this.Number = readingNotes.Number; + // this.Title = readingNotes.Title; + // this.Tags = readingNotes.Tags; + // this.Intro = readingNotes.Intro; + // this.Notes = readingNotes.Notes; + // } + // } + + public string Number { get; set; } + public string Title { get; set; } = string.Empty; + //public string Filename { get; set; } = string.Empty; + public string PublishedUrl { get; set; } = string.Empty; + public string Tags { get; set; } = string.Empty; + public string Intro { get; set; } = string.Empty; + public Dictionary> Notes { get; set; } + + + public string GetAllUniqueTags(){ + var uniqueTags = new HashSet(); + + foreach (var category in Notes.Values) + { + foreach (var note in category) + { + if (!string.IsNullOrEmpty(note.Tags)) + { + var tags = note.Tags.ToLower().Split('.'); + uniqueTags = uniqueTags.Concat(tags).ToHashSet(); + } + } + } + uniqueTags.Add("readingnotes"); + return String.Join(",", uniqueTags.OrderBy(n => n).ToArray()); + } + + public string ToMarkDown() + { + + var md = new StringBuilder(); + + //== YAML header + md.AppendFormat("---{0}", Environment.NewLine); + md.Append(string.Format("Title: {0}{1}", Title, Environment.NewLine)); + md.Append(string.Format("Tags: {0}{1}", Tags, Environment.NewLine)); + md.AppendFormat("---{0}", Environment.NewLine); + + md.Append(Title + Environment.NewLine); + md.Append('=', Title.Length); + + md.Append(Environment.NewLine + Environment.NewLine + (this.Intro ?? "") + Environment.NewLine); + + //== All Notes + foreach (var key in this.Notes.Keys) + { + + md.AppendFormat("{0}{0}## {1}{0}{0}", Environment.NewLine, key); + + foreach (var note in ((List)Notes[key])) + { + md.Append(note.ToMarkDown() + Environment.NewLine); + } + + } + + return md.ToString(); + } +} diff --git a/src/NoteBookmark.Domain/SearchCriterias.cs b/src/NoteBookmark.Domain/SearchCriterias.cs new file mode 100644 index 0000000..458fa8a --- /dev/null +++ b/src/NoteBookmark.Domain/SearchCriterias.cs @@ -0,0 +1,32 @@ +using System; + +namespace NoteBookmark.Domain; + +public class SearchCriterias +{ + public string? SearchTopic { get; set; } + private string SearchPrompt {get;} + public string? AllowedDomains { get; set; } + public string? BlockedDomains { get; set; } + + public SearchCriterias(string searchPrompt) + { + this.SearchPrompt = searchPrompt; + } + + public string[]? GetSplittedAllowedDomains() + { + return AllowedDomains?.Split(',').Select(d => d.Trim()).ToArray(); + } + + public string[]? GetSplittedBlockedDomains() + { + return BlockedDomains?.Split(',').Select(d => d.Trim()).ToArray(); + } + + public string? GetSearchPrompt() + { + var tempPrompt = this.SearchPrompt?.Replace("{topic}", " " + this.SearchTopic + " " ?? ""); + return tempPrompt; + } +} diff --git a/NoteBookmark.Domain/Settings.cs b/src/NoteBookmark.Domain/Settings.cs similarity index 53% rename from NoteBookmark.Domain/Settings.cs rename to src/NoteBookmark.Domain/Settings.cs index fd4d477..fe5e4eb 100644 --- a/NoteBookmark.Domain/Settings.cs +++ b/src/NoteBookmark.Domain/Settings.cs @@ -1,20 +1,40 @@ -using System; -using System.Runtime.Serialization; -using Azure; -using Azure.Data.Tables; - -namespace NoteBookmark.Domain; - -public class Settings: ITableEntity -{ - [DataMember(Name="last_bookmark_date")] - public string? LastBookmarkDate { get; set; } - - - [DataMember(Name="reading_notes_counter")] - public string? ReadingNotesCounter { get; set; } - public required string PartitionKey { get ; set; } - public required string RowKey { get ; set; } - public DateTimeOffset? Timestamp { get; set; } - public ETag ETag { get; set; } -} +using System; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; +using Azure; +using Azure.Data.Tables; + +namespace NoteBookmark.Domain; + +public class Settings: ITableEntity +{ + [DataMember(Name="last_bookmark_date")] + public string? LastBookmarkDate { get; set; } + + + [DataMember(Name="reading_notes_counter")] + public string? ReadingNotesCounter { get; set; } + + + [DataMember(Name="favorite_domains")] + public string? FavoriteDomains { get; set; } + + + [DataMember(Name="blocked_domains")] + public string? BlockedDomains { get; set; } + + + [DataMember(Name="summary_prompt")] + [ContainsPlaceholder("content")] + public string? SummaryPrompt { get; set; } + + + [DataMember(Name="search_prompt")] + [ContainsPlaceholder("topic")] + public string? SearchPrompt { get; set; } + + public required string PartitionKey { get ; set; } + public required string RowKey { get ; set; } + public DateTimeOffset? Timestamp { get; set; } + public ETag ETag { get; set; } +} diff --git a/src/NoteBookmark.Domain/Suggestion.cs b/src/NoteBookmark.Domain/Suggestion.cs new file mode 100644 index 0000000..4115656 --- /dev/null +++ b/src/NoteBookmark.Domain/Suggestion.cs @@ -0,0 +1,29 @@ +using System; +using System.Runtime.Serialization; + +namespace NoteBookmark.Domain; + +public class Suggestion +{ + [DataMember(Name = "id")] + public string? Id { get; set; } + + public string? Title { get; set; } + + public string? Url { get; set; } + + public string? Overview { get; set; } + + public string? SuggestedDate { get; set; } + + + + public required string PartitionKey { get; set; } + + public required string RowKey { get; set; } + + public DateTimeOffset? Timestamp { get; set; } + + public Azure.ETag ETag { get; set; } + +} diff --git a/NoteBookmark.Domain/Summary.cs b/src/NoteBookmark.Domain/Summary.cs similarity index 100% rename from NoteBookmark.Domain/Summary.cs rename to src/NoteBookmark.Domain/Summary.cs diff --git a/NoteBookmark.ServiceDefaults/Extensions.cs b/src/NoteBookmark.ServiceDefaults/Extensions.cs similarity index 91% rename from NoteBookmark.ServiceDefaults/Extensions.cs rename to src/NoteBookmark.ServiceDefaults/Extensions.cs index 2a3f4e0..777e143 100644 --- a/NoteBookmark.ServiceDefaults/Extensions.cs +++ b/src/NoteBookmark.ServiceDefaults/Extensions.cs @@ -26,7 +26,13 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu builder.Services.ConfigureHttpClientDefaults(http => { // Turn on resilience by default - http.AddStandardResilienceHandler(); + http.AddStandardResilienceHandler(options => + { + TimeSpan timeSpan = TimeSpan.FromMinutes(5); + options.AttemptTimeout.Timeout = timeSpan; + options.CircuitBreaker.SamplingDuration = timeSpan * 2; + options.TotalRequestTimeout.Timeout = timeSpan * 3; + }); // Turn on service discovery by default http.AddServiceDiscovery(); diff --git a/NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj b/src/NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj similarity index 89% rename from NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj rename to src/NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj index 145a4ab..233173d 100644 --- a/NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj +++ b/src/NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj @@ -11,12 +11,12 @@ - + - - - + + +