From 3e7dce119fb1d6045c390d205979d38ffac45be4 Mon Sep 17 00:00:00 2001 From: Frank Boucher Date: Fri, 24 Oct 2025 10:26:10 -0400 Subject: [PATCH 01/16] Bump: Updates package versions Updates various package versions across multiple projects. This ensures the application uses the latest features, security patches, and bug fixes provided by the updated dependencies. --- NoteBookmark.Api.Tests/NoteBookmark.Api.Tests.csproj | 4 ++-- NoteBookmark.Api/NoteBookmark.Api.csproj | 4 ++-- NoteBookmark.AppHost/NoteBookmark.AppHost.csproj | 8 ++++---- NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj | 2 +- .../NoteBookmark.ServiceDefaults.csproj | 8 ++++---- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/NoteBookmark.Api.Tests/NoteBookmark.Api.Tests.csproj b/NoteBookmark.Api.Tests/NoteBookmark.Api.Tests.csproj index 9b25b0a..a69d6ea 100644 --- a/NoteBookmark.Api.Tests/NoteBookmark.Api.Tests.csproj +++ b/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/NoteBookmark.Api.csproj b/NoteBookmark.Api/NoteBookmark.Api.csproj index af0ee3f..e7a66a1 100644 --- a/NoteBookmark.Api/NoteBookmark.Api.csproj +++ b/NoteBookmark.Api/NoteBookmark.Api.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj b/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj index 4412ddf..5522560 100644 --- a/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj +++ b/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.BlazorApp/NoteBookmark.BlazorApp.csproj b/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj index 0bf5ea3..be4bd25 100644 --- a/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj +++ b/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj @@ -7,7 +7,7 @@ - + diff --git a/NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj b/NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj index 145a4ab..233173d 100644 --- a/NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj +++ b/NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj @@ -11,12 +11,12 @@ - + - - - + + + From 2bbe0cada42886edddcbce827b57f34311ae5625 Mon Sep 17 00:00:00 2001 From: Frank Boucher Date: Fri, 24 Oct 2025 11:07:30 -0400 Subject: [PATCH 02/16] [wip] Adds search page and suggestion list Implements a search page with filtering based on search prompt, allowed domains, and blocked domains. Introduces a suggestion list component to display filtered URLs with actions such as opening the URL, editing the note, and creating a note for the post. --- .../Components/Pages/Search.razor | 88 +++++++++++ .../Components/Shared/SuggestionList.razor | 145 ++++++++++++++++++ NoteBookmark.Domain/Suggestion.cs | 29 ++++ 3 files changed, 262 insertions(+) create mode 100644 NoteBookmark.BlazorApp/Components/Pages/Search.razor create mode 100644 NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor create mode 100644 NoteBookmark.Domain/Suggestion.cs diff --git a/NoteBookmark.BlazorApp/Components/Pages/Search.razor b/NoteBookmark.BlazorApp/Components/Pages/Search.razor new file mode 100644 index 0000000..a82383c --- /dev/null +++ b/NoteBookmark.BlazorApp/Components/Pages/Search.razor @@ -0,0 +1,88 @@ +@page "/search" +@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 NavigationManager Navigation +@rendermode InteractiveServer + +Search + +

Search

+ + + + + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+
+ @* + Read Only + UnRead Only + *@ + +
+ + + + +@code { + private IQueryable? posts; + private GridSort defSort = GridSort.ByDescending(c => c.Date_published); + private string newPostUrl = string.Empty; + private bool showRead = false; + + private SearchCriterias _criterias = new SearchCriterias(); + + + protected override async Task OnInitializedAsync() + { + await LoadPosts(); + } + + private async Task LoadPosts() + { + List loadedPosts = showRead ? await client.GetReadPosts(): await client.GetUnreadPosts(); + posts = loadedPosts.AsQueryable(); + } + + @* private async Task OpenUrlInNewWindow(string? url) + { + await jsRuntime.InvokeVoidAsync("open", url, "_blank"); + } *@ + + + + private class SearchCriterias + { + public string? SearchPrompt { get; set; } + public string? AllowedDomains { get; set; } + public string? BlockedDomains { get; set; } + } + + +} diff --git a/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor b/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor new file mode 100644 index 0000000..91f4d32 --- /dev/null +++ b/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor @@ -0,0 +1,145 @@ +@using NoteBookmark.Domain +@using Microsoft.FluentUI.AspNetCore.Components +@inject IToastService toastService +@inject IDialogService DialogService + +@inject PostNoteClient client + + +

Suggestions

+ + + + + + + @if (String.IsNullOrEmpty(context!.NoteId)) + { + + } + else + { + + } + + + + + + + + + + + + +   Nothing to see here. Carry on! + + + + +@code { + + private PaginationState pagination = new PaginationState { ItemsPerPage = 20 }; + private string titleFilter = string.Empty; + + IQueryable? filteredUrlList => posts?.Where(x => x.Title!.Contains(titleFilter, StringComparison.CurrentCultureIgnoreCase)); + + private IQueryable? posts; + private GridSort defSort = GridSort.ByDescending(c => c.Date_published); + private string newPostUrl = string.Empty; + private bool showRead = false; + + + private async Task CreateNoteForPost(string postId) + { + var newNote = new Note { PostId = postId }; + + IDialogReference dialog = await DialogService.ShowDialogAsync(newNote, new DialogParameters(){ + Title = "Add a note", + PreventDismissOnOverlayClick = true, + PreventScroll = true, + + }); + + var result = await dialog.Result; + if (!result.Cancelled && result.Data != null) + { + var note = (Note)result.Data; + await client.CreateNote(note); + ShowConfirmationMessage(); + //await LoadPosts(); + } + } + + private void ShowConfirmationMessage() + { + toastService.ShowSuccess("Note created successfully!"); + } + + private void EditNote(string postId) + { + Navigation.NavigateTo($"posteditor/{postId}"); + } + + private async Task AddNewPost() + { + if (!string.IsNullOrEmpty(newPostUrl)) + { + var result = await client.ExtractPostDetailsAndSave(newPostUrl); + if (result) + { + await LoadPosts(); + newPostUrl = string.Empty; + toastService.ShowSuccess("Post added successfully!"); + } + else + { + toastService.ShowError("Failed to add post. Please try again."); + } + } + } + + private async Task DeletePost(string postId) + { + var result = await client.DeletePost(postId); + if (result) + { + await LoadPosts(); + toastService.ShowSuccess("Post deleted successfully!"); + } + else + { + toastService.ShowError("Failed to delete post. Please try again."); + } + } + + // Add handler to reload posts when toggle changes + private async Task OnShowReadChanged(bool value) + { + showRead = value; + await LoadPosts(); + } + + 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.Domain/Suggestion.cs b/NoteBookmark.Domain/Suggestion.cs new file mode 100644 index 0000000..4115656 --- /dev/null +++ b/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; } + +} From 1b9d4618f5082d2e0f80a064d5a4a34dc87e765b Mon Sep 17 00:00:00 2001 From: fboucher Date: Fri, 31 Oct 2025 10:52:25 -0400 Subject: [PATCH 03/16] Adds search page and suggestion list component It does nothing but it's there Adds search functionality to the navigation menu. Implements the ability to delete suggestions and displays success/error messages upon completion. --- .../Components/Layout/NavMenu.razor | 1 + .../Components/Pages/Search.razor | 4 +- .../Components/Shared/SuggestionList.razor | 76 ++----------------- 3 files changed, 11 insertions(+), 70 deletions(-) diff --git a/NoteBookmark.BlazorApp/Components/Layout/NavMenu.razor b/NoteBookmark.BlazorApp/Components/Layout/NavMenu.razor index 3fa220f..fa00510 100644 --- a/NoteBookmark.BlazorApp/Components/Layout/NavMenu.razor +++ b/NoteBookmark.BlazorApp/Components/Layout/NavMenu.razor @@ -9,6 +9,7 @@ Generate Summary Summaries Posts + Search diff --git a/NoteBookmark.BlazorApp/Components/Pages/Search.razor b/NoteBookmark.BlazorApp/Components/Pages/Search.razor index a82383c..4696402 100644 --- a/NoteBookmark.BlazorApp/Components/Pages/Search.razor +++ b/NoteBookmark.BlazorApp/Components/Pages/Search.razor @@ -61,10 +61,10 @@ protected override async Task OnInitializedAsync() { - await LoadPosts(); + @* await LoadPosts(); *@ } - private async Task LoadPosts() + private async Task FetchSuggestions() { List loadedPosts = showRead ? await client.GetReadPosts(): await client.GetUnreadPosts(); posts = loadedPosts.AsQueryable(); diff --git a/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor b/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor index 91f4d32..05380f2 100644 --- a/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor +++ b/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor @@ -11,16 +11,8 @@ - - - @if (String.IsNullOrEmpty(context!.NoteId)) - { - - } - else - { - - } + + @@ -34,9 +26,6 @@ Sortable="true" SortBy="@defSort" IsDefaultSortColumn="true" Width="125px"/> - - -   Nothing to see here. Carry on! @@ -57,75 +46,26 @@ private bool showRead = false; - private async Task CreateNoteForPost(string postId) - { - var newNote = new Note { PostId = postId }; - - IDialogReference dialog = await DialogService.ShowDialogAsync(newNote, new DialogParameters(){ - Title = "Add a note", - PreventDismissOnOverlayClick = true, - PreventScroll = true, - - }); - - var result = await dialog.Result; - if (!result.Cancelled && result.Data != null) - { - var note = (Note)result.Data; - await client.CreateNote(note); - ShowConfirmationMessage(); - //await LoadPosts(); - } - } - - private void ShowConfirmationMessage() - { - toastService.ShowSuccess("Note created successfully!"); - } - - private void EditNote(string postId) - { - Navigation.NavigateTo($"posteditor/{postId}"); - } - private async Task AddNewPost() + private async Task AddSuggestion(string postId) { - if (!string.IsNullOrEmpty(newPostUrl)) - { - var result = await client.ExtractPostDetailsAndSave(newPostUrl); - if (result) - { - await LoadPosts(); - newPostUrl = string.Empty; - toastService.ShowSuccess("Post added successfully!"); - } - else - { - toastService.ShowError("Failed to add post. Please try again."); - } - } + } - private async Task DeletePost(string postId) + private async Task DeleteSuggestion(string postId) { var result = await client.DeletePost(postId); if (result) { - await LoadPosts(); - toastService.ShowSuccess("Post deleted successfully!"); + @* await LoadSuggestions(); *@ + toastService.ShowSuccess("Suggestion deleted successfully!"); } else { - toastService.ShowError("Failed to delete post. Please try again."); + toastService.ShowError("Failed to delete suggestion. Please try again."); } } - // Add handler to reload posts when toggle changes - private async Task OnShowReadChanged(bool value) - { - showRead = value; - await LoadPosts(); - } private void HandleTitleFilter(ChangeEventArgs args) { From 9548ffc47bc2355df57f5fbcd6ec78d36c06dc19 Mon Sep 17 00:00:00 2001 From: fboucher Date: Fri, 31 Oct 2025 11:27:59 -0400 Subject: [PATCH 04/16] Enables AI-powered research suggestions. Adds an AI service to provide research suggestions based on a user-defined topic. Integrates the AI service into the search page, allowing users to fetch suggestions using the Reka AI API. Displays the loading state during the search, and handles errors gracefully with toast notifications. --- NoteBookmark.AIServices/ResearchService.cs | 70 +++++++++++++++++++ .../Components/Pages/Search.razor | 44 ++++++++++-- NoteBookmark.BlazorApp/Program.cs | 5 ++ 3 files changed, 112 insertions(+), 7 deletions(-) create mode 100644 NoteBookmark.AIServices/ResearchService.cs diff --git a/NoteBookmark.AIServices/ResearchService.cs b/NoteBookmark.AIServices/ResearchService.cs new file mode 100644 index 0000000..f12a0da --- /dev/null +++ b/NoteBookmark.AIServices/ResearchService.cs @@ -0,0 +1,70 @@ +using System.Text; +using System.Text.Json; +using System.IO; +using System.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +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(string topic, string[]? allowedDomains, string[]? blockedDomains) + { + string introParagraph; + string query = $"Provide a concise research summary on the topic: '{topic}'. Use credible sources only."; + + _client.Timeout = TimeSpan.FromSeconds(300); + + var requestPayload = new + { + model = MODEL_NAME, + + messages = new[] + { + new + { + role = "user", + content = query + } + } + }; + + var jsonPayload = JsonSerializer.Serialize(requestPayload, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + HttpResponseMessage? response = null; + + 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(); + + var rekaResponse = JsonSerializer.Deserialize(responseContent); + + if (response.IsSuccessStatusCode) + { + var textContent = rekaResponse!.Responses![0]!.Message!.Content! + .FirstOrDefault(c => c.Type == "text"); + + introParagraph = textContent?.Text ?? String.Empty; + } + else + { + throw new Exception($"Request failed with status code: {response.StatusCode}. Response: {responseContent}"); + } + + return introParagraph; + } + +} \ No newline at end of file diff --git a/NoteBookmark.BlazorApp/Components/Pages/Search.razor b/NoteBookmark.BlazorApp/Components/Pages/Search.razor index 4696402..6cf6e17 100644 --- a/NoteBookmark.BlazorApp/Components/Pages/Search.razor +++ b/NoteBookmark.BlazorApp/Components/Pages/Search.razor @@ -1,11 +1,13 @@ @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 IToastService toastService +@* @inject IDialogService DialogService *@ +@inject ResearchService aiService @inject NavigationManager Navigation @rendermode InteractiveServer @@ -21,20 +23,26 @@
- +
- +
- +
+
+ + @(isSearching ? "Searching..." : "Search") + +
+
@@ -55,6 +63,7 @@ private GridSort defSort = GridSort.ByDescending(c => c.Date_published); private string newPostUrl = string.Empty; private bool showRead = false; + private bool isSearching = false; private SearchCriterias _criterias = new SearchCriterias(); @@ -66,8 +75,29 @@ private async Task FetchSuggestions() { - List loadedPosts = showRead ? await client.GetReadPosts(): await client.GetUnreadPosts(); - posts = loadedPosts.AsQueryable(); + isSearching = true; + if (string.IsNullOrWhiteSpace(_criterias.SearchPrompt)) + { + toastService.ShowError("Please enter a search prompt."); + isSearching = false; + return; + } + + try{ + var allowedDomains = _criterias.AllowedDomains?.Split(',').Select(d => d.Trim()).ToArray(); + var blockedDomains = _criterias.BlockedDomains?.Split(',').Select(d => d.Trim()).ToArray(); + + string introText = await aiService.SearchSuggestionsAsync(_criterias.SearchPrompt, allowedDomains, blockedDomains); + @* readingNotes.Intro = introText; *@ + } + catch(Exception ex) + { + toastService.ShowError($"Oops! Error: {ex.Message}"); + } + finally + { + isSearching = false; + } } @* private async Task OpenUrlInNewWindow(string? url) diff --git a/NoteBookmark.BlazorApp/Program.cs b/NoteBookmark.BlazorApp/Program.cs index d172ab1..abe6b4a 100644 --- a/NoteBookmark.BlazorApp/Program.cs +++ b/NoteBookmark.BlazorApp/Program.cs @@ -17,6 +17,11 @@ client.Timeout = TimeSpan.FromSeconds(300); // Set to 5 minutes, adjust as needed }); +builder.Services.AddHttpClient(client => +{ + client.Timeout = TimeSpan.FromSeconds(300); // Set to 5 minutes, adjust as needed +}); + // Add services to the container. builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); From 882c406b902400e24619b5573d966679c7b4255b Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 1 Nov 2025 10:59:58 -0400 Subject: [PATCH 05/16] feat(post-list): Improves row size in post list Reduces the row size in the post list data grid to improve visual appearance and address potential issues with overly large buttons. Related to #70 --- NoteBookmark.BlazorApp/Components/Pages/Posts.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NoteBookmark.BlazorApp/Components/Pages/Posts.razor b/NoteBookmark.BlazorApp/Components/Pages/Posts.razor index 78500d9..7ad04c7 100644 --- a/NoteBookmark.BlazorApp/Components/Pages/Posts.razor +++ b/NoteBookmark.BlazorApp/Components/Pages/Posts.razor @@ -24,7 +24,7 @@ - + From 2b5b11b600d2002d146726f1667c901b4f9b1e31 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 1 Nov 2025 11:20:37 -0400 Subject: [PATCH 06/16] move code path and ref unchanged --- .../NoteBookmark.AIServices}/Choice.cs | 0 .../NoteBookmark.AIServices}/ContentItem.cs | 0 .../NoteBookmark.AIServices}/Message.cs | 0 .../NoteBookmark.AIServices.csproj | 0 .../NoteBookmark.AIServices}/ReasoningStep.cs | 0 .../RekaChatResponse.cs | 0 .../NoteBookmark.AIServices}/RekaMessage.cs | 0 .../NoteBookmark.AIServices}/RekaResponse.cs | 0 .../NoteBookmark.AIServices}/RekaUsage.cs | 0 .../NoteBookmark.AIServices}/ResponseItem.cs | 0 .../SummaryService.cs | 0 .../NoteBookmark.AIServices}/ToolCall.cs | 0 .../NoteBookmark.AIServices}/Usage.cs | 0 .../Domain/NoteTests.cs | 0 .../Domain/PostLTests.cs | 0 .../Domain/PostTests.cs | 0 .../Domain/ReadingNoteTests.cs | 0 .../Domain/ReadingNotesTests.cs | 0 .../Domain/SettingsTests.cs | 0 .../Domain/SummaryTests.cs | 0 .../Endpoints/NoteEndpointsTests.cs | 0 .../Endpoints/PostEndpointsTests.cs | 0 .../Endpoints/SettingEndpointsTests.cs | 0 .../Endpoints/SummaryEndpointsTests.cs | 0 .../Fixtures/AzureStorageTestFixture.cs | 0 .../Fixtures/NoteBookmarkApiTestFactory.cs | 0 .../Helpers/TestDataBuilder.cs | 0 .../Integration/NoteBookmarkWorkflowTests.cs | 0 .../NoteBookmark.Api.Tests.csproj | 0 .../Services/DataStorageServiceTests.cs | 0 .../NoteBookmark.Api}/DataStorageService.cs | 0 .../NoteBookmark.Api}/Dockerfile | 0 .../NoteBookmark.Api}/IDataStorageService.cs | 0 .../NoteBookmark.Api}/NoteBookmark.Api.csproj | 0 .../NoteBookmark.Api}/NoteBookmark.Api.http | 0 .../NoteBookmark.Api}/NoteEnpoints.cs | 236 ++++---- .../NoteBookmark.Api}/PostEndpoints.cs | 0 .../NoteBookmark.Api}/Program.cs | 0 .../Properties/launchSettings.json | 0 .../NoteBookmark.Api}/SettingEndpoints.cs | 0 .../NoteBookmark.Api}/SummaryEndpoints.cs | 0 .../NoteBookmark.Api}/appsettings.json | 0 .../NoteBookmark.AppHost}/AppHost.cs | 0 .../NoteBookmark.AppHost.csproj | 0 .../Properties/launchSettings.json | 0 .../NoteBookmark.AppHost}/appsettings.json | 0 .../NoteBookmark.AppHost}/infra/api.tmpl.yaml | 0 .../infra/blazor-app.tmpl.yaml | 0 .../Components/App.razor | 44 +- .../Components/Layout/MainLayout.razor | 90 +-- .../Components/Layout/MinimalLayout.razor | 0 .../Components/Layout/NavMenu.razor | 0 .../Components/Pages/Error.razor | 72 +-- .../Components/Pages/Home.razor | 12 +- .../Components/Pages/PostEditor.razor | 0 .../Components/Pages/PostEditorLight.razor | 0 .../Components/Pages/Posts.razor | 0 .../Components/Pages/Settings.razor | 0 .../Components/Pages/Summaries.razor | 0 .../Components/Pages/SummaryEditor.razor | 542 +++++++++--------- .../Components/Routes.razor | 16 +- .../Components/Shared/NoteDialog.razor | 0 .../Components/_Imports.razor | 22 +- .../NoteBookmark.BlazorApp}/Dockerfile | 0 .../NoteBookmark.BlazorApp.csproj | 40 +- .../NoteBookmark.BlazorApp}/PostNoteClient.cs | 0 .../NoteBookmark.BlazorApp}/Program.cs | 88 +-- .../Properties/launchSettings.json | 76 +-- .../NoteBookmark.BlazorApp}/appsettings.json | 18 +- .../NoteBookmark.BlazorApp}/wwwroot/app.css | 396 ++++++------- .../wwwroot/favicon.ico | Bin .../NoteBookmark.Domain}/Note.cs | 0 .../NoteBookmark.Domain.csproj | 0 .../NoteBookmark.Domain}/NoteCategories.cs | 116 ++-- .../NoteBookmark.Domain}/Post.cs | 0 .../NoteBookmark.Domain}/PostL.cs | 0 .../NoteBookmark.Domain}/ReadingNote.cs | 0 .../NoteBookmark.Domain}/ReadingNotes.cs | 182 +++--- .../NoteBookmark.Domain}/Settings.cs | 40 +- .../NoteBookmark.Domain}/Summary.cs | 0 .../Extensions.cs | 0 .../NoteBookmark.ServiceDefaults.csproj | 0 82 files changed, 995 insertions(+), 995 deletions(-) rename {NoteBookmark.AIServices => src/NoteBookmark.AIServices}/Choice.cs (100%) rename {NoteBookmark.AIServices => src/NoteBookmark.AIServices}/ContentItem.cs (100%) rename {NoteBookmark.AIServices => src/NoteBookmark.AIServices}/Message.cs (100%) rename {NoteBookmark.AIServices => src/NoteBookmark.AIServices}/NoteBookmark.AIServices.csproj (100%) rename {NoteBookmark.AIServices => src/NoteBookmark.AIServices}/ReasoningStep.cs (100%) rename {NoteBookmark.AIServices => src/NoteBookmark.AIServices}/RekaChatResponse.cs (100%) rename {NoteBookmark.AIServices => src/NoteBookmark.AIServices}/RekaMessage.cs (100%) rename {NoteBookmark.AIServices => src/NoteBookmark.AIServices}/RekaResponse.cs (100%) rename {NoteBookmark.AIServices => src/NoteBookmark.AIServices}/RekaUsage.cs (100%) rename {NoteBookmark.AIServices => src/NoteBookmark.AIServices}/ResponseItem.cs (100%) rename {NoteBookmark.AIServices => src/NoteBookmark.AIServices}/SummaryService.cs (100%) rename {NoteBookmark.AIServices => src/NoteBookmark.AIServices}/ToolCall.cs (100%) rename {NoteBookmark.AIServices => src/NoteBookmark.AIServices}/Usage.cs (100%) rename {NoteBookmark.Api.Tests => src/NoteBookmark.Api.Tests}/Domain/NoteTests.cs (100%) rename {NoteBookmark.Api.Tests => src/NoteBookmark.Api.Tests}/Domain/PostLTests.cs (100%) rename {NoteBookmark.Api.Tests => src/NoteBookmark.Api.Tests}/Domain/PostTests.cs (100%) rename {NoteBookmark.Api.Tests => src/NoteBookmark.Api.Tests}/Domain/ReadingNoteTests.cs (100%) rename {NoteBookmark.Api.Tests => src/NoteBookmark.Api.Tests}/Domain/ReadingNotesTests.cs (100%) rename {NoteBookmark.Api.Tests => src/NoteBookmark.Api.Tests}/Domain/SettingsTests.cs (100%) rename {NoteBookmark.Api.Tests => src/NoteBookmark.Api.Tests}/Domain/SummaryTests.cs (100%) rename {NoteBookmark.Api.Tests => src/NoteBookmark.Api.Tests}/Endpoints/NoteEndpointsTests.cs (100%) rename {NoteBookmark.Api.Tests => src/NoteBookmark.Api.Tests}/Endpoints/PostEndpointsTests.cs (100%) rename {NoteBookmark.Api.Tests => src/NoteBookmark.Api.Tests}/Endpoints/SettingEndpointsTests.cs (100%) rename {NoteBookmark.Api.Tests => src/NoteBookmark.Api.Tests}/Endpoints/SummaryEndpointsTests.cs (100%) rename {NoteBookmark.Api.Tests => src/NoteBookmark.Api.Tests}/Fixtures/AzureStorageTestFixture.cs (100%) rename {NoteBookmark.Api.Tests => src/NoteBookmark.Api.Tests}/Fixtures/NoteBookmarkApiTestFactory.cs (100%) rename {NoteBookmark.Api.Tests => src/NoteBookmark.Api.Tests}/Helpers/TestDataBuilder.cs (100%) rename {NoteBookmark.Api.Tests => src/NoteBookmark.Api.Tests}/Integration/NoteBookmarkWorkflowTests.cs (100%) rename {NoteBookmark.Api.Tests => src/NoteBookmark.Api.Tests}/NoteBookmark.Api.Tests.csproj (100%) rename {NoteBookmark.Api.Tests => src/NoteBookmark.Api.Tests}/Services/DataStorageServiceTests.cs (100%) rename {NoteBookmark.Api => src/NoteBookmark.Api}/DataStorageService.cs (100%) rename {NoteBookmark.Api => src/NoteBookmark.Api}/Dockerfile (100%) rename {NoteBookmark.Api => src/NoteBookmark.Api}/IDataStorageService.cs (100%) rename {NoteBookmark.Api => src/NoteBookmark.Api}/NoteBookmark.Api.csproj (100%) rename {NoteBookmark.Api => src/NoteBookmark.Api}/NoteBookmark.Api.http (100%) rename {NoteBookmark.Api => src/NoteBookmark.Api}/NoteEnpoints.cs (96%) rename {NoteBookmark.Api => src/NoteBookmark.Api}/PostEndpoints.cs (100%) rename {NoteBookmark.Api => src/NoteBookmark.Api}/Program.cs (100%) rename {NoteBookmark.Api => src/NoteBookmark.Api}/Properties/launchSettings.json (100%) rename {NoteBookmark.Api => src/NoteBookmark.Api}/SettingEndpoints.cs (100%) rename {NoteBookmark.Api => src/NoteBookmark.Api}/SummaryEndpoints.cs (100%) rename {NoteBookmark.Api => src/NoteBookmark.Api}/appsettings.json (100%) rename {NoteBookmark.AppHost => src/NoteBookmark.AppHost}/AppHost.cs (100%) rename {NoteBookmark.AppHost => src/NoteBookmark.AppHost}/NoteBookmark.AppHost.csproj (100%) rename {NoteBookmark.AppHost => src/NoteBookmark.AppHost}/Properties/launchSettings.json (100%) rename {NoteBookmark.AppHost => src/NoteBookmark.AppHost}/appsettings.json (100%) rename {NoteBookmark.AppHost => src/NoteBookmark.AppHost}/infra/api.tmpl.yaml (100%) rename {NoteBookmark.AppHost => src/NoteBookmark.AppHost}/infra/blazor-app.tmpl.yaml (100%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/Components/App.razor (96%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/Components/Layout/MainLayout.razor (96%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/Components/Layout/MinimalLayout.razor (100%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/Components/Layout/NavMenu.razor (100%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/Components/Pages/Error.razor (97%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/Components/Pages/Home.razor (94%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/Components/Pages/PostEditor.razor (100%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/Components/Pages/PostEditorLight.razor (100%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/Components/Pages/Posts.razor (100%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/Components/Pages/Settings.razor (100%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/Components/Pages/Summaries.razor (100%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/Components/Pages/SummaryEditor.razor (97%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/Components/Routes.razor (97%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/Components/Shared/NoteDialog.razor (100%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/Components/_Imports.razor (97%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/Dockerfile (100%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/NoteBookmark.BlazorApp.csproj (97%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/PostNoteClient.cs (100%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/Program.cs (96%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/Properties/launchSettings.json (96%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/appsettings.json (94%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/wwwroot/app.css (96%) rename {NoteBookmark.BlazorApp => src/NoteBookmark.BlazorApp}/wwwroot/favicon.ico (100%) rename {NoteBookmark.Domain => src/NoteBookmark.Domain}/Note.cs (100%) rename {NoteBookmark.Domain => src/NoteBookmark.Domain}/NoteBookmark.Domain.csproj (100%) rename {NoteBookmark.Domain => src/NoteBookmark.Domain}/NoteCategories.cs (97%) rename {NoteBookmark.Domain => src/NoteBookmark.Domain}/Post.cs (100%) rename {NoteBookmark.Domain => src/NoteBookmark.Domain}/PostL.cs (100%) rename {NoteBookmark.Domain => src/NoteBookmark.Domain}/ReadingNote.cs (100%) rename {NoteBookmark.Domain => src/NoteBookmark.Domain}/ReadingNotes.cs (96%) rename {NoteBookmark.Domain => src/NoteBookmark.Domain}/Settings.cs (96%) rename {NoteBookmark.Domain => src/NoteBookmark.Domain}/Summary.cs (100%) rename {NoteBookmark.ServiceDefaults => src/NoteBookmark.ServiceDefaults}/Extensions.cs (100%) rename {NoteBookmark.ServiceDefaults => src/NoteBookmark.ServiceDefaults}/NoteBookmark.ServiceDefaults.csproj (100%) diff --git a/NoteBookmark.AIServices/Choice.cs b/src/NoteBookmark.AIServices/Choice.cs similarity index 100% rename from NoteBookmark.AIServices/Choice.cs rename to src/NoteBookmark.AIServices/Choice.cs diff --git a/NoteBookmark.AIServices/ContentItem.cs b/src/NoteBookmark.AIServices/ContentItem.cs similarity index 100% rename from NoteBookmark.AIServices/ContentItem.cs rename to src/NoteBookmark.AIServices/ContentItem.cs diff --git a/NoteBookmark.AIServices/Message.cs b/src/NoteBookmark.AIServices/Message.cs similarity index 100% rename from NoteBookmark.AIServices/Message.cs rename to src/NoteBookmark.AIServices/Message.cs diff --git a/NoteBookmark.AIServices/NoteBookmark.AIServices.csproj b/src/NoteBookmark.AIServices/NoteBookmark.AIServices.csproj similarity index 100% rename from NoteBookmark.AIServices/NoteBookmark.AIServices.csproj rename to src/NoteBookmark.AIServices/NoteBookmark.AIServices.csproj diff --git a/NoteBookmark.AIServices/ReasoningStep.cs b/src/NoteBookmark.AIServices/ReasoningStep.cs similarity index 100% rename from NoteBookmark.AIServices/ReasoningStep.cs rename to src/NoteBookmark.AIServices/ReasoningStep.cs diff --git a/NoteBookmark.AIServices/RekaChatResponse.cs b/src/NoteBookmark.AIServices/RekaChatResponse.cs similarity index 100% rename from NoteBookmark.AIServices/RekaChatResponse.cs rename to src/NoteBookmark.AIServices/RekaChatResponse.cs diff --git a/NoteBookmark.AIServices/RekaMessage.cs b/src/NoteBookmark.AIServices/RekaMessage.cs similarity index 100% rename from NoteBookmark.AIServices/RekaMessage.cs rename to src/NoteBookmark.AIServices/RekaMessage.cs diff --git a/NoteBookmark.AIServices/RekaResponse.cs b/src/NoteBookmark.AIServices/RekaResponse.cs similarity index 100% rename from NoteBookmark.AIServices/RekaResponse.cs rename to src/NoteBookmark.AIServices/RekaResponse.cs diff --git a/NoteBookmark.AIServices/RekaUsage.cs b/src/NoteBookmark.AIServices/RekaUsage.cs similarity index 100% rename from NoteBookmark.AIServices/RekaUsage.cs rename to src/NoteBookmark.AIServices/RekaUsage.cs diff --git a/NoteBookmark.AIServices/ResponseItem.cs b/src/NoteBookmark.AIServices/ResponseItem.cs similarity index 100% rename from NoteBookmark.AIServices/ResponseItem.cs rename to src/NoteBookmark.AIServices/ResponseItem.cs diff --git a/NoteBookmark.AIServices/SummaryService.cs b/src/NoteBookmark.AIServices/SummaryService.cs similarity index 100% rename from NoteBookmark.AIServices/SummaryService.cs rename to src/NoteBookmark.AIServices/SummaryService.cs diff --git a/NoteBookmark.AIServices/ToolCall.cs b/src/NoteBookmark.AIServices/ToolCall.cs similarity index 100% rename from NoteBookmark.AIServices/ToolCall.cs rename to src/NoteBookmark.AIServices/ToolCall.cs diff --git a/NoteBookmark.AIServices/Usage.cs b/src/NoteBookmark.AIServices/Usage.cs similarity index 100% rename from NoteBookmark.AIServices/Usage.cs rename to src/NoteBookmark.AIServices/Usage.cs 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/NoteBookmark.Api.Tests/Endpoints/SummaryEndpointsTests.cs b/src/NoteBookmark.Api.Tests/Endpoints/SummaryEndpointsTests.cs similarity index 100% rename from NoteBookmark.Api.Tests/Endpoints/SummaryEndpointsTests.cs rename to src/NoteBookmark.Api.Tests/Endpoints/SummaryEndpointsTests.cs diff --git a/NoteBookmark.Api.Tests/Fixtures/AzureStorageTestFixture.cs b/src/NoteBookmark.Api.Tests/Fixtures/AzureStorageTestFixture.cs similarity index 100% rename from NoteBookmark.Api.Tests/Fixtures/AzureStorageTestFixture.cs rename to src/NoteBookmark.Api.Tests/Fixtures/AzureStorageTestFixture.cs diff --git a/NoteBookmark.Api.Tests/Fixtures/NoteBookmarkApiTestFactory.cs b/src/NoteBookmark.Api.Tests/Fixtures/NoteBookmarkApiTestFactory.cs similarity index 100% rename from NoteBookmark.Api.Tests/Fixtures/NoteBookmarkApiTestFactory.cs rename to src/NoteBookmark.Api.Tests/Fixtures/NoteBookmarkApiTestFactory.cs 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 100% rename from NoteBookmark.Api.Tests/NoteBookmark.Api.Tests.csproj rename to src/NoteBookmark.Api.Tests/NoteBookmark.Api.Tests.csproj 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/NoteBookmark.Api/Dockerfile b/src/NoteBookmark.Api/Dockerfile similarity index 100% rename from NoteBookmark.Api/Dockerfile rename to src/NoteBookmark.Api/Dockerfile 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 100% rename from NoteBookmark.Api/NoteBookmark.Api.csproj rename to src/NoteBookmark.Api/NoteBookmark.Api.csproj 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 100% rename from NoteBookmark.Api/SettingEndpoints.cs rename to src/NoteBookmark.Api/SettingEndpoints.cs 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 100% rename from NoteBookmark.AppHost/NoteBookmark.AppHost.csproj rename to src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj 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 100% rename from NoteBookmark.BlazorApp/Components/Layout/NavMenu.razor rename to src/NoteBookmark.BlazorApp/Components/Layout/NavMenu.razor 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/NoteBookmark.BlazorApp/Components/Pages/Home.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Home.razor similarity index 94% rename from NoteBookmark.BlazorApp/Components/Pages/Home.razor rename to src/NoteBookmark.BlazorApp/Components/Pages/Home.razor index 858da0c..96714a2 100644 --- a/NoteBookmark.BlazorApp/Components/Pages/Home.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Home.razor @@ -1,7 +1,7 @@ -@page "/" - -Home - -

Hello, world!

- +@page "/" + +Home + +

Hello, world!

+ Welcome to your new Fluent Blazor app. \ 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 100% rename from NoteBookmark.BlazorApp/Components/Pages/Posts.razor rename to src/NoteBookmark.BlazorApp/Components/Pages/Posts.razor diff --git a/NoteBookmark.BlazorApp/Components/Pages/Settings.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor similarity index 100% rename from NoteBookmark.BlazorApp/Components/Pages/Settings.razor rename to src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor 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..abd1b19 100644 --- a/NoteBookmark.BlazorApp/Components/Pages/SummaryEditor.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/SummaryEditor.razor @@ -1,271 +1,271 @@ -@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; + + 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; + } + } + +} + 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/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 100% rename from NoteBookmark.BlazorApp/Dockerfile rename to src/NoteBookmark.BlazorApp/Dockerfile diff --git a/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj b/src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj similarity index 97% rename from NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj rename to src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj index 0bf5ea3..27fb224 100644 --- a/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj +++ b/src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj @@ -1,20 +1,20 @@ - - - - net9.0 - enable - enable - - - - - - - - - - - - - - + + + + net9.0 + enable + enable + + + + + + + + + + + + + + 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 96% rename from NoteBookmark.BlazorApp/Program.cs rename to src/NoteBookmark.BlazorApp/Program.cs index d172ab1..ee2df68 100644 --- a/NoteBookmark.BlazorApp/Program.cs +++ b/src/NoteBookmark.BlazorApp/Program.cs @@ -1,44 +1,44 @@ -using Microsoft.FluentUI.AspNetCore.Components; -using NoteBookmark.AIServices; -using NoteBookmark.BlazorApp; -using NoteBookmark.BlazorApp.Components; - -var builder = WebApplication.CreateBuilder(args); - -builder.AddServiceDefaults(); - -builder.Services.AddHttpClient(client => - { - client.BaseAddress = new Uri("https+http://api"); - }); - -builder.Services.AddHttpClient(client => -{ - client.Timeout = TimeSpan.FromSeconds(300); // Set to 5 minutes, adjust as needed -}); - -// Add services to the container. -builder.Services.AddRazorComponents() - .AddInteractiveServerComponents(); -builder.Services.AddFluentUIComponents(); - -var app = builder.Build(); -app.MapDefaultEndpoints(); - -// Configure the HTTP request pipeline. -if (!app.Environment.IsDevelopment()) -{ - app.UseExceptionHandler("/Error", createScopeForErrors: true); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); -} - -app.UseHttpsRedirection(); - -app.UseStaticFiles(); -app.UseAntiforgery(); - -app.MapRazorComponents() - .AddInteractiveServerRenderMode(); - -app.Run(); +using Microsoft.FluentUI.AspNetCore.Components; +using NoteBookmark.AIServices; +using NoteBookmark.BlazorApp; +using NoteBookmark.BlazorApp.Components; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.Services.AddHttpClient(client => + { + client.BaseAddress = new Uri("https+http://api"); + }); + +builder.Services.AddHttpClient(client => +{ + client.Timeout = TimeSpan.FromSeconds(300); // Set to 5 minutes, adjust as needed +}); + +// Add services to the container. +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); +builder.Services.AddFluentUIComponents(); + +var app = builder.Build(); +app.MapDefaultEndpoints(); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); + +app.UseStaticFiles(); +app.UseAntiforgery(); + +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +app.Run(); 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/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/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/NoteBookmark.Domain/Settings.cs b/src/NoteBookmark.Domain/Settings.cs similarity index 96% rename from NoteBookmark.Domain/Settings.cs rename to src/NoteBookmark.Domain/Settings.cs index fd4d477..e12fcc1 100644 --- a/NoteBookmark.Domain/Settings.cs +++ b/src/NoteBookmark.Domain/Settings.cs @@ -1,20 +1,20 @@ -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.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; } +} 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 100% rename from NoteBookmark.ServiceDefaults/Extensions.cs rename to src/NoteBookmark.ServiceDefaults/Extensions.cs diff --git a/NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj b/src/NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj similarity index 100% rename from NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj rename to src/NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj From 4a2d71998d1803ac1a99080e29f5dd83a8eff6a1 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 1 Nov 2025 13:46:53 -0400 Subject: [PATCH 07/16] feat: Moves code into a `src` folder Moves the project's code into a `src` folder. This change organizes the project structure by introducing a `src` directory to house the main source code. Updates paths in various configuration files (azure.yaml, Dockerfiles, .sln) and scripts to reflect the new directory structure. Fixes #74 --- .github/workflows/docker-publish.yml | 4 ++-- .../Endpoints/SummaryEndpointsTests.cs | 0 NoteBookmark.sln | 14 +++++++------- azure.yaml | 2 +- docker-compose/build-and-push.ps1 | 4 ++-- .../Fixtures/AzureStorageTestFixture.cs | 2 ++ .../Fixtures/NoteBookmarkApiTestFactory.cs | 2 ++ src/NoteBookmark.Api/Dockerfile | 11 ++++++----- src/NoteBookmark.BlazorApp/Dockerfile | 10 +++++----- 9 files changed, 27 insertions(+), 22 deletions(-) create mode 100644 NoteBookmark.Api.Tests/Endpoints/SummaryEndpointsTests.cs 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/NoteBookmark.Api.Tests/Endpoints/SummaryEndpointsTests.cs b/NoteBookmark.Api.Tests/Endpoints/SummaryEndpointsTests.cs new file mode 100644 index 0000000..e69de29 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/src/NoteBookmark.Api.Tests/Fixtures/AzureStorageTestFixture.cs b/src/NoteBookmark.Api.Tests/Fixtures/AzureStorageTestFixture.cs index 59ec531..54325a2 100644 --- a/src/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/src/NoteBookmark.Api.Tests/Fixtures/NoteBookmarkApiTestFactory.cs b/src/NoteBookmark.Api.Tests/Fixtures/NoteBookmarkApiTestFactory.cs index 1f023cb..da53256 100644 --- a/src/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/src/NoteBookmark.Api/Dockerfile b/src/NoteBookmark.Api/Dockerfile index aca8267..fc0996e 100644 --- a/src/NoteBookmark.Api/Dockerfile +++ b/src/NoteBookmark.Api/Dockerfile @@ -5,12 +5,13 @@ 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 ["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/NoteBookmark.Api" +WORKDIR "/src/src/NoteBookmark.Api" RUN dotnet build "NoteBookmark.Api.csproj" -c Release -o /app/build FROM build AS publish diff --git a/src/NoteBookmark.BlazorApp/Dockerfile b/src/NoteBookmark.BlazorApp/Dockerfile index 37178f3..5e4db56 100644 --- a/src/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 From b2c7815fa422e99871ccf106b6c19d4ca7ff67da Mon Sep 17 00:00:00 2001 From: fboucher Date: Fri, 14 Nov 2025 10:05:10 -0500 Subject: [PATCH 08/16] Removes the src.sln file Removes the solution file. The project is now managed by a different build system. --- src.sln | 60 --------------------------------------------------------- 1 file changed, 60 deletions(-) delete mode 100644 src.sln diff --git a/src.sln b/src.sln deleted file mode 100644 index 793cee2..0000000 --- a/src.sln +++ /dev/null @@ -1,60 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.2.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.AIServices", "NoteBookmark.AIServices\NoteBookmark.AIServices.csproj", "{182F861D-2F61-B57A-DAA4-3A40A2E081F3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.Api", "NoteBookmark.Api\NoteBookmark.Api.csproj", "{152016F4-5D9B-0AFE-16DB-789E848AC423}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.Api.Tests", "NoteBookmark.Api.Tests\NoteBookmark.Api.Tests.csproj", "{06607A75-B77A-5C4A-0480-FD0D6577487C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.AppHost", "NoteBookmark.AppHost\NoteBookmark.AppHost.csproj", "{B2959C55-3813-9BB4-B036-8EDE6103844B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.BlazorApp", "NoteBookmark.BlazorApp\NoteBookmark.BlazorApp.csproj", "{5F85B69B-39F5-3F8B-7A06-DAE023E1569D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.Domain", "NoteBookmark.Domain\NoteBookmark.Domain.csproj", "{8AEFAE4B-7750-7F14-F43A-ABE58ABFF6BE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteBookmark.ServiceDefaults", "NoteBookmark.ServiceDefaults\NoteBookmark.ServiceDefaults.csproj", "{432B421B-FDD6-EF3B-1BED-5DF2CCC84516}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {182F861D-2F61-B57A-DAA4-3A40A2E081F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {182F861D-2F61-B57A-DAA4-3A40A2E081F3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {182F861D-2F61-B57A-DAA4-3A40A2E081F3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {182F861D-2F61-B57A-DAA4-3A40A2E081F3}.Release|Any CPU.Build.0 = Release|Any CPU - {152016F4-5D9B-0AFE-16DB-789E848AC423}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {152016F4-5D9B-0AFE-16DB-789E848AC423}.Debug|Any CPU.Build.0 = Debug|Any CPU - {152016F4-5D9B-0AFE-16DB-789E848AC423}.Release|Any CPU.ActiveCfg = Release|Any CPU - {152016F4-5D9B-0AFE-16DB-789E848AC423}.Release|Any CPU.Build.0 = Release|Any CPU - {06607A75-B77A-5C4A-0480-FD0D6577487C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {06607A75-B77A-5C4A-0480-FD0D6577487C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {06607A75-B77A-5C4A-0480-FD0D6577487C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {06607A75-B77A-5C4A-0480-FD0D6577487C}.Release|Any CPU.Build.0 = Release|Any CPU - {B2959C55-3813-9BB4-B036-8EDE6103844B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B2959C55-3813-9BB4-B036-8EDE6103844B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B2959C55-3813-9BB4-B036-8EDE6103844B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B2959C55-3813-9BB4-B036-8EDE6103844B}.Release|Any CPU.Build.0 = Release|Any CPU - {5F85B69B-39F5-3F8B-7A06-DAE023E1569D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5F85B69B-39F5-3F8B-7A06-DAE023E1569D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5F85B69B-39F5-3F8B-7A06-DAE023E1569D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5F85B69B-39F5-3F8B-7A06-DAE023E1569D}.Release|Any CPU.Build.0 = Release|Any CPU - {8AEFAE4B-7750-7F14-F43A-ABE58ABFF6BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8AEFAE4B-7750-7F14-F43A-ABE58ABFF6BE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8AEFAE4B-7750-7F14-F43A-ABE58ABFF6BE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8AEFAE4B-7750-7F14-F43A-ABE58ABFF6BE}.Release|Any CPU.Build.0 = Release|Any CPU - {432B421B-FDD6-EF3B-1BED-5DF2CCC84516}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {432B421B-FDD6-EF3B-1BED-5DF2CCC84516}.Debug|Any CPU.Build.0 = Debug|Any CPU - {432B421B-FDD6-EF3B-1BED-5DF2CCC84516}.Release|Any CPU.ActiveCfg = Release|Any CPU - {432B421B-FDD6-EF3B-1BED-5DF2CCC84516}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {09C4CC02-49C4-48DC-9A1E-8378C7557203} - EndGlobalSection -EndGlobal From da8e400ad104641f25d3b338b314fa0bcd216561 Mon Sep 17 00:00:00 2001 From: fboucher Date: Fri, 14 Nov 2025 11:02:19 -0500 Subject: [PATCH 09/16] Enables structured AI research responses. Configures the AI service to return research results in a structured JSON format. This allows for easier parsing and utilization of the AI-generated suggestions by defining a JSON schema and modifies the query to exclude explicit instructions about credible sources, since that is now managed by the research tool configuration. Also, introduces domain filtering (allowed/blocked domains) for web searches to refine research scope. --- src/NoteBookmark.AIServices/PostSuggestion.cs | 10 +++ .../PostSuggestions.cs | 6 ++ .../ResearchService.cs | 61 ++++++++++++++++++- 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 src/NoteBookmark.AIServices/PostSuggestion.cs create mode 100644 src/NoteBookmark.AIServices/PostSuggestions.cs diff --git a/src/NoteBookmark.AIServices/PostSuggestion.cs b/src/NoteBookmark.AIServices/PostSuggestion.cs new file mode 100644 index 0000000..f216bc1 --- /dev/null +++ b/src/NoteBookmark.AIServices/PostSuggestion.cs @@ -0,0 +1,10 @@ +namespace NoteBookmark.AIServices; + +public class PostSuggestion +{ + public string Title { get; set; } = string.Empty; + public string? Author { get; set; } + public string Summary { get; set; } = string.Empty; + public string? PublicationDate { get; set; } + public string Url { get; set; } = string.Empty; +} diff --git a/src/NoteBookmark.AIServices/PostSuggestions.cs b/src/NoteBookmark.AIServices/PostSuggestions.cs new file mode 100644 index 0000000..9fa55f8 --- /dev/null +++ b/src/NoteBookmark.AIServices/PostSuggestions.cs @@ -0,0 +1,6 @@ +namespace NoteBookmark.AIServices; + +public class PostSuggestions +{ + public List Events { get; set; } = new(); +} diff --git a/src/NoteBookmark.AIServices/ResearchService.cs b/src/NoteBookmark.AIServices/ResearchService.cs index f12a0da..6a850a7 100644 --- a/src/NoteBookmark.AIServices/ResearchService.cs +++ b/src/NoteBookmark.AIServices/ResearchService.cs @@ -18,10 +18,24 @@ public class ResearchService(HttpClient client, ILogger logger, public async Task SearchSuggestionsAsync(string topic, string[]? allowedDomains, string[]? blockedDomains) { string introParagraph; - string query = $"Provide a concise research summary on the topic: '{topic}'. Use credible sources only."; + string query = $"Provide a concise research summary on the topic: '{topic}'."; _client.Timeout = TimeSpan.FromSeconds(300); + var webSearch = new Dictionary + { + ["max_uses"] = 3 + }; + + 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, @@ -33,7 +47,12 @@ public async Task SearchSuggestionsAsync(string topic, string[]? allowed role = "user", content = query } - } + }, + response_format = GetResponseFormat(), + research = new + { + web_search = webSearch + }, }; var jsonPayload = JsonSerializer.Serialize(requestPayload, new JsonSerializerOptions @@ -67,4 +86,42 @@ public async Task SearchSuggestionsAsync(string topic, string[]? allowed return introParagraph; } + + 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" }, + publication_date = new { type = "string" }, + url = new { type = "string" } + }, + required = new[] { "title", "summary", "url" } + } + } + }, + required = new[] { "post_suggestions" } + } + } + }; + } + } \ No newline at end of file From 7b2ed978d86d8ddfcfb809bacdc343f2149531d3 Mon Sep 17 00:00:00 2001 From: fboucher Date: Wed, 19 Nov 2025 17:44:14 -0500 Subject: [PATCH 10/16] Improves AI research suggestions functionality. Enhances the AI research suggestion feature by improving error handling, logging, and data persistence for debugging. Updates the Reka API base URL and simplifies the query construction. Moves domain models to a shared project. Fixes: #72 --- .gitignore | 2 + .../ResearchService.cs | 64 ++++++++++++------- .../Components/Shared/SuggestionList.razor | 12 ++-- src/NoteBookmark.BlazorApp/Program.cs | 2 +- .../PostSuggestion.cs | 2 +- .../PostSuggestions.cs | 2 +- 6 files changed, 53 insertions(+), 31 deletions(-) rename src/{NoteBookmark.AIServices => NoteBookmark.Domain}/PostSuggestion.cs (89%) rename src/{NoteBookmark.AIServices => NoteBookmark.Domain}/PostSuggestions.cs (73%) diff --git a/.gitignore b/.gitignore index 906e9dd..01dd0ae 100644 --- a/.gitignore +++ b/.gitignore @@ -493,3 +493,5 @@ NoteBookmark.BlazorApp/appsettings.Development.json .azure NoteBookmark.AppHost/appsettings.Development.json + +src/NoteBookmark.AppHost/appsettings.Development.json diff --git a/src/NoteBookmark.AIServices/ResearchService.cs b/src/NoteBookmark.AIServices/ResearchService.cs index 6a850a7..e8e568f 100644 --- a/src/NoteBookmark.AIServices/ResearchService.cs +++ b/src/NoteBookmark.AIServices/ResearchService.cs @@ -11,16 +11,14 @@ public class ResearchService(HttpClient client, ILogger logger, { 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 BASE_URL = "http://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(string topic, string[]? allowedDomains, string[]? blockedDomains) { string introParagraph; - string query = $"Provide a concise research summary on the topic: '{topic}'."; - - _client.Timeout = TimeSpan.FromSeconds(300); + string query = $"Provide a concise research summary on the topic: {topic}."; var webSearch = new Dictionary { @@ -60,27 +58,39 @@ public async Task SearchSuggestionsAsync(string topic, string[]? allowed PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - HttpResponseMessage? response = null; - - 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(); - - var rekaResponse = JsonSerializer.Deserialize(responseContent); - - if (response.IsSuccessStatusCode) + try { - var textContent = rekaResponse!.Responses![0]!.Message!.Content! - .FirstOrDefault(c => c.Type == "text"); - - introParagraph = textContent?.Text ?? String.Empty; + HttpResponseMessage? response = null; + + using var request = new HttpRequestMessage(HttpMethod.Post, BASE_URL); + request.Headers.Add("Authorization", $"Bearer {_apiKey}"); + request.Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); + + await SaveToFile("research_request", jsonPayload); + + response = await _client.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + + await SaveToFile("research_response", responseContent); + + var rekaResponse = JsonSerializer.Deserialize(responseContent); + + if (response.IsSuccessStatusCode) + { + var textContent = rekaResponse!.Responses![0]!.Message!.Content! + .FirstOrDefault(c => c.Type == "text"); + + introParagraph = textContent?.Text ?? String.Empty; + } + else + { + throw new Exception($"Request failed with status code: {response.StatusCode}. Response: {responseContent}"); + } } - else + catch (Exception ex) { - throw new Exception($"Request failed with status code: {response.StatusCode}. Response: {responseContent}"); + _logger.LogError(ex, "Error occurred while fetching research suggestions."); + throw new Exception("An error occurred while fetching research suggestions.", ex); } return introParagraph; @@ -124,4 +134,14 @@ private object GetResponseFormat() }; } + 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/src/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor b/src/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor index 05380f2..dea3af6 100644 --- a/src/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor +++ b/src/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor @@ -11,8 +11,8 @@ - - + + @@ -22,7 +22,7 @@ @@ -38,10 +38,10 @@ private PaginationState pagination = new PaginationState { ItemsPerPage = 20 }; private string titleFilter = string.Empty; - IQueryable? filteredUrlList => posts?.Where(x => x.Title!.Contains(titleFilter, StringComparison.CurrentCultureIgnoreCase)); + IQueryable? filteredUrlList => posts?.Where(x => x.Title!.Contains(titleFilter, StringComparison.CurrentCultureIgnoreCase)); - private IQueryable? posts; - private GridSort defSort = GridSort.ByDescending(c => c.Date_published); + private IQueryable? posts; + private GridSort defSort = GridSort.ByDescending(c => c.PublicationDate); private string newPostUrl = string.Empty; private bool showRead = false; diff --git a/src/NoteBookmark.BlazorApp/Program.cs b/src/NoteBookmark.BlazorApp/Program.cs index abe6b4a..16d2b15 100644 --- a/src/NoteBookmark.BlazorApp/Program.cs +++ b/src/NoteBookmark.BlazorApp/Program.cs @@ -19,7 +19,7 @@ builder.Services.AddHttpClient(client => { - client.Timeout = TimeSpan.FromSeconds(300); // Set to 5 minutes, adjust as needed + client.Timeout = TimeSpan.FromMinutes(5); }); // Add services to the container. diff --git a/src/NoteBookmark.AIServices/PostSuggestion.cs b/src/NoteBookmark.Domain/PostSuggestion.cs similarity index 89% rename from src/NoteBookmark.AIServices/PostSuggestion.cs rename to src/NoteBookmark.Domain/PostSuggestion.cs index f216bc1..693c97f 100644 --- a/src/NoteBookmark.AIServices/PostSuggestion.cs +++ b/src/NoteBookmark.Domain/PostSuggestion.cs @@ -1,4 +1,4 @@ -namespace NoteBookmark.AIServices; +namespace NoteBookmark.Domain; public class PostSuggestion { diff --git a/src/NoteBookmark.AIServices/PostSuggestions.cs b/src/NoteBookmark.Domain/PostSuggestions.cs similarity index 73% rename from src/NoteBookmark.AIServices/PostSuggestions.cs rename to src/NoteBookmark.Domain/PostSuggestions.cs index 9fa55f8..c488df0 100644 --- a/src/NoteBookmark.AIServices/PostSuggestions.cs +++ b/src/NoteBookmark.Domain/PostSuggestions.cs @@ -1,4 +1,4 @@ -namespace NoteBookmark.AIServices; +namespace NoteBookmark.Domain; public class PostSuggestions { From 429500550982164bb9ab3a641e56a501dac61abe Mon Sep 17 00:00:00 2001 From: Frank Boucher Date: Thu, 20 Nov 2025 16:59:02 -0500 Subject: [PATCH 11/16] Configures research suggestions endpoint Updates the research service to fetch blog posts about a topic and configures the http client with resilience policies. --- .../ResearchService.cs | 17 +++++------ src/NoteBookmark.BlazorApp/Program.cs | 30 +++++++++++++++---- .../Extensions.cs | 8 ++++- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/NoteBookmark.AIServices/ResearchService.cs b/src/NoteBookmark.AIServices/ResearchService.cs index e8e568f..a381157 100644 --- a/src/NoteBookmark.AIServices/ResearchService.cs +++ b/src/NoteBookmark.AIServices/ResearchService.cs @@ -11,14 +11,14 @@ public class ResearchService(HttpClient client, ILogger logger, { private readonly HttpClient _client = client; private readonly ILogger _logger = logger; - private const string BASE_URL = "http://api.reka.ai/v1/chat/completions"; + 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(string topic, string[]? allowedDomains, string[]? blockedDomains) { - string introParagraph; - string query = $"Provide a concise research summary on the topic: {topic}."; + string introParagraph = string.Empty; + string query = $"Provide interesting a list of blog posts, published recently, that talks about the topic: {topic}."; var webSearch = new Dictionary { @@ -58,16 +58,16 @@ public async Task SearchSuggestionsAsync(string topic, string[]? allowed PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + await SaveToFile("research_request", jsonPayload); + + HttpResponseMessage? response = null; + try { - HttpResponseMessage? response = null; - using var request = new HttpRequestMessage(HttpMethod.Post, BASE_URL); request.Headers.Add("Authorization", $"Bearer {_apiKey}"); request.Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); - await SaveToFile("research_request", jsonPayload); - response = await _client.SendAsync(request); var responseContent = await response.Content.ReadAsStringAsync(); @@ -89,8 +89,7 @@ public async Task SearchSuggestionsAsync(string topic, string[]? allowed } catch (Exception ex) { - _logger.LogError(ex, "Error occurred while fetching research suggestions."); - throw new Exception("An error occurred while fetching research suggestions.", ex); + _logger.LogError($"An error occurred while fetching research suggestions: {ex.Message}"); } return introParagraph; diff --git a/src/NoteBookmark.BlazorApp/Program.cs b/src/NoteBookmark.BlazorApp/Program.cs index 16d2b15..896b180 100644 --- a/src/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,13 +34,13 @@ builder.Services.AddHttpClient(client => { - client.Timeout = TimeSpan.FromSeconds(300); // Set to 5 minutes, adjust as needed + client.Timeout = TimeSpan.FromMinutes(5); }); -builder.Services.AddHttpClient(client => -{ - client.Timeout = TimeSpan.FromMinutes(5); -}); + +builder.Services.AddHttpClient(); + // .AddStandardResilienceHandler(); + // Add services to the container. builder.Services.AddRazorComponents() diff --git a/src/NoteBookmark.ServiceDefaults/Extensions.cs b/src/NoteBookmark.ServiceDefaults/Extensions.cs index 2a3f4e0..777e143 100644 --- a/src/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(); From 2d4f72f6af0aa7daf5baf90af99f4c80da6d8ba5 Mon Sep 17 00:00:00 2001 From: fboucher Date: Fri, 28 Nov 2025 11:34:39 -0500 Subject: [PATCH 12/16] Consumes Reka SDK for research suggestions Refactors the research service to use the Reka SDK for fetching blog post suggestions. Changes the return type of SearchSuggestionsAsync to PostSuggestions DTO. Adapts the Blazor UI to consume the new PostSuggestions data structure and display the blog post suggestions in the SuggestionList component. Fixes #72 --- src/NoteBookmark.AIServices/Choice.cs | 18 ---------- src/NoteBookmark.AIServices/ContentItem.cs | 12 ------- src/NoteBookmark.AIServices/Message.cs | 34 ------------------- .../NoteBookmark.AIServices.csproj | 6 ++++ src/NoteBookmark.AIServices/ReasoningStep.cs | 21 ------------ .../RekaChatResponse.cs | 18 ---------- src/NoteBookmark.AIServices/RekaMessage.cs | 15 -------- src/NoteBookmark.AIServices/RekaResponse.cs | 31 ----------------- src/NoteBookmark.AIServices/RekaUsage.cs | 15 -------- .../ResearchService.cs | 21 ++++++------ src/NoteBookmark.AIServices/ResponseItem.cs | 12 ------- src/NoteBookmark.AIServices/SummaryService.cs | 1 + src/NoteBookmark.AIServices/ToolCall.cs | 15 -------- src/NoteBookmark.AIServices/Usage.cs | 21 ------------ .../Components/Pages/Search.razor | 11 +++--- .../Components/Shared/SuggestionList.razor | 8 +++-- src/NoteBookmark.Domain/PostSuggestion.cs | 11 ++++++ src/NoteBookmark.Domain/PostSuggestions.cs | 5 ++- 18 files changed, 44 insertions(+), 231 deletions(-) delete mode 100644 src/NoteBookmark.AIServices/Choice.cs delete mode 100644 src/NoteBookmark.AIServices/ContentItem.cs delete mode 100644 src/NoteBookmark.AIServices/Message.cs delete mode 100644 src/NoteBookmark.AIServices/ReasoningStep.cs delete mode 100644 src/NoteBookmark.AIServices/RekaChatResponse.cs delete mode 100644 src/NoteBookmark.AIServices/RekaMessage.cs delete mode 100644 src/NoteBookmark.AIServices/RekaResponse.cs delete mode 100644 src/NoteBookmark.AIServices/RekaUsage.cs delete mode 100644 src/NoteBookmark.AIServices/ResponseItem.cs delete mode 100644 src/NoteBookmark.AIServices/ToolCall.cs delete mode 100644 src/NoteBookmark.AIServices/Usage.cs diff --git a/src/NoteBookmark.AIServices/Choice.cs b/src/NoteBookmark.AIServices/Choice.cs deleted file mode 100644 index a6e4451..0000000 --- a/src/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/src/NoteBookmark.AIServices/ContentItem.cs b/src/NoteBookmark.AIServices/ContentItem.cs deleted file mode 100644 index 613dad9..0000000 --- a/src/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/src/NoteBookmark.AIServices/Message.cs b/src/NoteBookmark.AIServices/Message.cs deleted file mode 100644 index 23f6616..0000000 --- a/src/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/src/NoteBookmark.AIServices/NoteBookmark.AIServices.csproj b/src/NoteBookmark.AIServices/NoteBookmark.AIServices.csproj index fd72d67..a9bdbe7 100644 --- a/src/NoteBookmark.AIServices/NoteBookmark.AIServices.csproj +++ b/src/NoteBookmark.AIServices/NoteBookmark.AIServices.csproj @@ -9,6 +9,12 @@ + + + + + + diff --git a/src/NoteBookmark.AIServices/ReasoningStep.cs b/src/NoteBookmark.AIServices/ReasoningStep.cs deleted file mode 100644 index 80cdf76..0000000 --- a/src/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/src/NoteBookmark.AIServices/RekaChatResponse.cs b/src/NoteBookmark.AIServices/RekaChatResponse.cs deleted file mode 100644 index 9e2f020..0000000 --- a/src/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/src/NoteBookmark.AIServices/RekaMessage.cs b/src/NoteBookmark.AIServices/RekaMessage.cs deleted file mode 100644 index 596537c..0000000 --- a/src/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/src/NoteBookmark.AIServices/RekaResponse.cs b/src/NoteBookmark.AIServices/RekaResponse.cs deleted file mode 100644 index ffcbecf..0000000 --- a/src/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/src/NoteBookmark.AIServices/RekaUsage.cs b/src/NoteBookmark.AIServices/RekaUsage.cs deleted file mode 100644 index 558311d..0000000 --- a/src/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/src/NoteBookmark.AIServices/ResearchService.cs b/src/NoteBookmark.AIServices/ResearchService.cs index a381157..16b73b0 100644 --- a/src/NoteBookmark.AIServices/ResearchService.cs +++ b/src/NoteBookmark.AIServices/ResearchService.cs @@ -4,6 +4,8 @@ using System.Linq; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Reka.SDK; +using NoteBookmark.Domain; namespace NoteBookmark.AIServices; @@ -15,10 +17,10 @@ public class ResearchService(HttpClient client, ILogger logger, 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(string topic, string[]? allowedDomains, string[]? blockedDomains) + public async Task SearchSuggestionsAsync(string topic, string[]? allowedDomains, string[]? blockedDomains) { - string introParagraph = string.Empty; - string query = $"Provide interesting a list of blog posts, published recently, that talks about the topic: {topic}."; + PostSuggestions suggestions = new PostSuggestions(); + string query = $"Provide interesting a list of 3 blog posts, published recently, that talks about the topic: {topic}."; var webSearch = new Dictionary { @@ -58,7 +60,7 @@ public async Task SearchSuggestionsAsync(string topic, string[]? allowed PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - await SaveToFile("research_request", jsonPayload); + // await SaveToFile("research_request", jsonPayload); HttpResponseMessage? response = null; @@ -73,14 +75,11 @@ public async Task SearchSuggestionsAsync(string topic, string[]? allowed await SaveToFile("research_response", responseContent); - var rekaResponse = JsonSerializer.Deserialize(responseContent); + var rekaResponse = JsonSerializer.Deserialize(responseContent); if (response.IsSuccessStatusCode) { - var textContent = rekaResponse!.Responses![0]!.Message!.Content! - .FirstOrDefault(c => c.Type == "text"); - - introParagraph = textContent?.Text ?? String.Empty; + suggestions = JsonSerializer.Deserialize(rekaResponse!.Choices![0].Message!.Content!)!; } else { @@ -92,7 +91,7 @@ public async Task SearchSuggestionsAsync(string topic, string[]? allowed _logger.LogError($"An error occurred while fetching research suggestions: {ex.Message}"); } - return introParagraph; + return suggestions; } @@ -119,7 +118,7 @@ private object GetResponseFormat() { title = new { type = "string" }, author = new { type = "string" }, - summary = new { type = "string" }, + summary = new { type = "string", maxLength = 100 }, publication_date = new { type = "string" }, url = new { type = "string" } }, diff --git a/src/NoteBookmark.AIServices/ResponseItem.cs b/src/NoteBookmark.AIServices/ResponseItem.cs deleted file mode 100644 index 5780d97..0000000 --- a/src/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/src/NoteBookmark.AIServices/SummaryService.cs b/src/NoteBookmark.AIServices/SummaryService.cs index e04b95c..363d211 100644 --- a/src/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; diff --git a/src/NoteBookmark.AIServices/ToolCall.cs b/src/NoteBookmark.AIServices/ToolCall.cs deleted file mode 100644 index 38f0d12..0000000 --- a/src/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/src/NoteBookmark.AIServices/Usage.cs b/src/NoteBookmark.AIServices/Usage.cs deleted file mode 100644 index 1f49ca4..0000000 --- a/src/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/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor index 6cf6e17..c8c35a9 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor @@ -52,15 +52,15 @@ Read Only UnRead Only *@ - + @code { - private IQueryable? posts; - private GridSort defSort = GridSort.ByDescending(c => c.Date_published); + private IQueryable? suggestions; + private GridSort defSort = GridSort.ByDescending(c => c.PublicationDate); private string newPostUrl = string.Empty; private bool showRead = false; private bool isSearching = false; @@ -87,8 +87,9 @@ var allowedDomains = _criterias.AllowedDomains?.Split(',').Select(d => d.Trim()).ToArray(); var blockedDomains = _criterias.BlockedDomains?.Split(',').Select(d => d.Trim()).ToArray(); - string introText = await aiService.SearchSuggestionsAsync(_criterias.SearchPrompt, allowedDomains, blockedDomains); - @* readingNotes.Intro = introText; *@ + PostSuggestions result = await aiService.SearchSuggestionsAsync(_criterias.SearchPrompt, allowedDomains, blockedDomains); + suggestions = result.Suggestions.AsQueryable(); + StateHasChanged(); } catch(Exception ex) { diff --git a/src/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor b/src/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor index dea3af6..1d0eed2 100644 --- a/src/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor +++ b/src/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor @@ -35,12 +35,16 @@ @code { + + [Parameter] + public IQueryable? Suggestions {get; set;} + private PaginationState pagination = new PaginationState { ItemsPerPage = 20 }; private string titleFilter = string.Empty; - IQueryable? filteredUrlList => posts?.Where(x => x.Title!.Contains(titleFilter, StringComparison.CurrentCultureIgnoreCase)); + IQueryable? filteredUrlList => Suggestions?.Where(x => x.Title!.Contains(titleFilter, StringComparison.CurrentCultureIgnoreCase)); + - private IQueryable? posts; private GridSort defSort = GridSort.ByDescending(c => c.PublicationDate); private string newPostUrl = string.Empty; private bool showRead = false; diff --git a/src/NoteBookmark.Domain/PostSuggestion.cs b/src/NoteBookmark.Domain/PostSuggestion.cs index 693c97f..5531bf4 100644 --- a/src/NoteBookmark.Domain/PostSuggestion.cs +++ b/src/NoteBookmark.Domain/PostSuggestion.cs @@ -1,10 +1,21 @@ +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")] public string? PublicationDate { get; set; } + + [JsonPropertyName("url")] public string Url { get; set; } = string.Empty; } diff --git a/src/NoteBookmark.Domain/PostSuggestions.cs b/src/NoteBookmark.Domain/PostSuggestions.cs index c488df0..4a71deb 100644 --- a/src/NoteBookmark.Domain/PostSuggestions.cs +++ b/src/NoteBookmark.Domain/PostSuggestions.cs @@ -1,6 +1,9 @@ +using System.Text.Json.Serialization; + namespace NoteBookmark.Domain; public class PostSuggestions { - public List Events { get; set; } = new(); + [JsonPropertyName("suggestions")] + public List? Suggestions { get; set; } } From 0458bdd9ba1585c37cf926656c73d32a5f4d532d Mon Sep 17 00:00:00 2001 From: Frank Boucher Date: Wed, 10 Dec 2025 15:35:28 -0500 Subject: [PATCH 13/16] Updates Aspire packages to v13 Updates Aspire packages to version 13.0.2 in the API and AppHost projects. This upgrade brings in the latest features, performance improvements, and bug fixes provided by the Aspire framework. The .gitignore file is updated to correctly ignore development settings files. Fixes #72 --- .aspire/settings.json | 3 +++ .gitignore | 2 +- src/NoteBookmark.Api/NoteBookmark.Api.csproj | 4 ++-- src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj | 8 ++++---- 4 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 .aspire/settings.json 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/.gitignore b/.gitignore index 01dd0ae..a694ee9 100644 --- a/.gitignore +++ b/.gitignore @@ -494,4 +494,4 @@ NoteBookmark.BlazorApp/appsettings.Development.json NoteBookmark.AppHost/appsettings.Development.json -src/NoteBookmark.AppHost/appsettings.Development.json +src/NoteBookmark.AppHost/appsettings.[Dd]evelopment.json diff --git a/src/NoteBookmark.Api/NoteBookmark.Api.csproj b/src/NoteBookmark.Api/NoteBookmark.Api.csproj index e7a66a1..be07b08 100644 --- a/src/NoteBookmark.Api/NoteBookmark.Api.csproj +++ b/src/NoteBookmark.Api/NoteBookmark.Api.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj b/src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj index 5522560..1f81dbe 100644 --- a/src/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 - - - + + + From 1e48843642e192b91934484126d43cde064a0aab Mon Sep 17 00:00:00 2001 From: Frank Boucher Date: Wed, 10 Dec 2025 16:42:32 -0500 Subject: [PATCH 14/16] Improves search suggestion functionality. Adds the PostSuggestion domain model to represent search suggestions. Modifies the AI service to format the publication date. Updates the Search and SuggestionList components to handle and display search suggestions, allowing users to add them as notes. Fixes a bug related to IQueryable and replace to List collection. --- .gitignore | 2 + .../ResearchService.cs | 2 +- .../Components/Pages/Posts.razor | 2 +- .../Components/Pages/Search.razor | 8 ++-- .../Components/Shared/SuggestionList.razor | 38 ++++++++++++++----- src/NoteBookmark.Domain/PostSuggestion.cs | 34 +++++++++++++++++ 6 files changed, 71 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index a694ee9..7684431 100644 --- a/.gitignore +++ b/.gitignore @@ -495,3 +495,5 @@ NoteBookmark.BlazorApp/appsettings.Development.json NoteBookmark.AppHost/appsettings.Development.json src/NoteBookmark.AppHost/appsettings.[Dd]evelopment.json + +src/NoteBookmark.AppHost/appsettings.json diff --git a/src/NoteBookmark.AIServices/ResearchService.cs b/src/NoteBookmark.AIServices/ResearchService.cs index 16b73b0..32a73a6 100644 --- a/src/NoteBookmark.AIServices/ResearchService.cs +++ b/src/NoteBookmark.AIServices/ResearchService.cs @@ -119,7 +119,7 @@ private object GetResponseFormat() title = new { type = "string" }, author = new { type = "string" }, summary = new { type = "string", maxLength = 100 }, - publication_date = new { type = "string" }, + publication_date = new { type = "string", format = "date" }, url = new { type = "string" } }, required = new[] { "title", "summary", "url" } diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/Posts.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Posts.razor index 7ad04c7..e43e68f 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/Posts.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Posts.razor @@ -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 index c8c35a9..44debff 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor @@ -19,11 +19,11 @@ - +
- +
@@ -59,7 +59,7 @@ @code { - private IQueryable? suggestions; + private List? suggestions; private GridSort defSort = GridSort.ByDescending(c => c.PublicationDate); private string newPostUrl = string.Empty; private bool showRead = false; @@ -88,7 +88,7 @@ var blockedDomains = _criterias.BlockedDomains?.Split(',').Select(d => d.Trim()).ToArray(); PostSuggestions result = await aiService.SearchSuggestionsAsync(_criterias.SearchPrompt, allowedDomains, blockedDomains); - suggestions = result.Suggestions.AsQueryable(); + suggestions = result.Suggestions ?? []; StateHasChanged(); } catch(Exception ex) diff --git a/src/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor b/src/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor index 1d0eed2..e07a60d 100644 --- a/src/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor +++ b/src/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor @@ -8,7 +8,7 @@

Suggestions

- + @@ -37,12 +37,14 @@ [Parameter] - public IQueryable? Suggestions {get; set;} + 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)); + IQueryable? filteredUrlList => Suggestions? + .Where(x => x.Title!.Contains(titleFilter, StringComparison.CurrentCultureIgnoreCase)) + .AsQueryable(); private GridSort defSort = GridSort.ByDescending(c => c.PublicationDate); @@ -51,17 +53,35 @@ - private async Task AddSuggestion(string postId) + 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 postId) + private async Task DeleteSuggestion(string postURL) { - var result = await client.DeletePost(postId); - if (result) + var sug = Suggestions?.FirstOrDefault(x => x.Url == postURL); + if (sug != null) { - @* await LoadSuggestions(); *@ + Suggestions!.Remove(sug); + StateHasChanged(); toastService.ShowSuccess("Suggestion deleted successfully!"); } else diff --git a/src/NoteBookmark.Domain/PostSuggestion.cs b/src/NoteBookmark.Domain/PostSuggestion.cs index 5531bf4..f6fe06a 100644 --- a/src/NoteBookmark.Domain/PostSuggestion.cs +++ b/src/NoteBookmark.Domain/PostSuggestion.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using System.Text.Json.Serialization; namespace NoteBookmark.Domain; @@ -14,8 +15,41 @@ public class PostSuggestion 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); + } +} From b8b3a33bcb9bab8942ae5a0088072901e5edc7ac Mon Sep 17 00:00:00 2001 From: Frank Boucher Date: Wed, 10 Dec 2025 17:28:24 -0500 Subject: [PATCH 15/16] Adds prompt and domain settings Adds settings for summary and search prompts, as well as favorite and blocked domains. These settings are loaded when the search page is initialized, allowing users to customize their search experience. Relates to #76 --- .../Components/Pages/Search.razor | 6 +++++ .../Components/Pages/Settings.razor | 25 ++++++++++++++++--- src/NoteBookmark.Domain/Settings.cs | 17 +++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor index 44debff..8b457b0 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor @@ -70,6 +70,12 @@ protected override async Task OnInitializedAsync() { + var settings = await client.GetSettings(); + if (settings != null) + { + _criterias.AllowedDomains = settings.FavoriteDomains; + _criterias.BlockedDomains = settings.BlockedDomains; + } @* await LoadPosts(); *@ } diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor index d96427d..192bc2e 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor @@ -41,13 +41,32 @@ - - + + + + + - + + + + + + + + + + + + + + + + + Save diff --git a/src/NoteBookmark.Domain/Settings.cs b/src/NoteBookmark.Domain/Settings.cs index e12fcc1..fe55575 100644 --- a/src/NoteBookmark.Domain/Settings.cs +++ b/src/NoteBookmark.Domain/Settings.cs @@ -13,6 +13,23 @@ public class Settings: ITableEntity [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")] + public string? SummaryPrompt { get; set; } + + + [DataMember(Name="search_prompt")] + public string? SearchPrompt { get; set; } + public required string PartitionKey { get ; set; } public required string RowKey { get ; set; } public DateTimeOffset? Timestamp { get; set; } From 8c0e420748e0a7b5f89796ef8dc7ab62025249d8 Mon Sep 17 00:00:00 2001 From: Frank Boucher Date: Fri, 12 Dec 2025 11:19:59 -0500 Subject: [PATCH 16/16] Enables customizable prompts for AI services Moves the search and summary prompts to application settings, allowing users to customize the AI's behavior. Adds default prompts to settings if they are not yet defined. Adds validation to settings to ensure that prompts contain required placeholders. --- .../ResearchService.cs | 8 ++- src/NoteBookmark.AIServices/SummaryService.cs | 5 +- src/NoteBookmark.Api/SettingEndpoints.cs | 11 ++++ .../Components/Pages/Home.razor | 65 ++++++++++++++++++- .../Components/Pages/Search.razor | 30 ++++----- .../Components/Pages/Settings.razor | 51 +++++++++------ .../Components/Pages/SummaryEditor.razor | 10 ++- .../ContainsPlaceholderAttribute.cs | 31 +++++++++ src/NoteBookmark.Domain/SearchCriterias.cs | 32 +++++++++ src/NoteBookmark.Domain/Settings.cs | 3 + 10 files changed, 196 insertions(+), 50 deletions(-) create mode 100644 src/NoteBookmark.Domain/ContainsPlaceholderAttribute.cs create mode 100644 src/NoteBookmark.Domain/SearchCriterias.cs diff --git a/src/NoteBookmark.AIServices/ResearchService.cs b/src/NoteBookmark.AIServices/ResearchService.cs index 32a73a6..bedd895 100644 --- a/src/NoteBookmark.AIServices/ResearchService.cs +++ b/src/NoteBookmark.AIServices/ResearchService.cs @@ -17,16 +17,18 @@ public class ResearchService(HttpClient client, ILogger logger, 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(string topic, string[]? allowedDomains, string[]? blockedDomains) + public async Task SearchSuggestionsAsync(SearchCriterias searchCriterias) { PostSuggestions suggestions = new PostSuggestions(); - string query = $"Provide interesting a list of 3 blog posts, published recently, that talks about the topic: {topic}."; 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; @@ -45,7 +47,7 @@ public async Task SearchSuggestionsAsync(string topic, string[] new { role = "user", - content = query + content = searchCriterias.GetSearchPrompt() } }, response_format = GetResponseFormat(), diff --git a/src/NoteBookmark.AIServices/SummaryService.cs b/src/NoteBookmark.AIServices/SummaryService.cs index 363d211..9257aa3 100644 --- a/src/NoteBookmark.AIServices/SummaryService.cs +++ b/src/NoteBookmark.AIServices/SummaryService.cs @@ -16,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); @@ -32,7 +31,7 @@ public async Task GenerateSummaryAsync(string summaryText) new { role = "user", - content = query + content = prompt } } }; diff --git a/src/NoteBookmark.Api/SettingEndpoints.cs b/src/NoteBookmark.Api/SettingEndpoints.cs index c64d3ac..6301b92 100644 --- a/src/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/src/NoteBookmark.BlazorApp/Components/Pages/Home.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Home.razor index 96714a2..6f7e2e6 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/Home.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Home.razor @@ -1,7 +1,66 @@ @page "/" +@using Microsoft.FluentUI.AspNetCore.Components +@inject NavigationManager Navigation -Home +Home - NoteBookmark -

Hello, world!

+ +

📚 NoteBookmark

-Welcome to your new Fluent Blazor app. \ No newline at end of file + +

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/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor index 8b457b0..9c4b02d 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor @@ -21,18 +21,18 @@ - +
- - + +
-
+
-
+
@@ -65,14 +65,15 @@ private bool showRead = false; private bool isSearching = false; - private SearchCriterias _criterias = new SearchCriterias(); + private SearchCriterias _criterias = new SearchCriterias(string.Empty); protected override async Task OnInitializedAsync() { - var settings = await client.GetSettings(); + Domain.Settings? settings = await client.GetSettings(); if (settings != null) { + _criterias = new SearchCriterias(settings.SearchPrompt); _criterias.AllowedDomains = settings.FavoriteDomains; _criterias.BlockedDomains = settings.BlockedDomains; } @@ -82,7 +83,7 @@ private async Task FetchSuggestions() { isSearching = true; - if (string.IsNullOrWhiteSpace(_criterias.SearchPrompt)) + if (string.IsNullOrWhiteSpace(_criterias.SearchTopic)) { toastService.ShowError("Please enter a search prompt."); isSearching = false; @@ -90,10 +91,8 @@ } try{ - var allowedDomains = _criterias.AllowedDomains?.Split(',').Select(d => d.Trim()).ToArray(); - var blockedDomains = _criterias.BlockedDomains?.Split(',').Select(d => d.Trim()).ToArray(); - - PostSuggestions result = await aiService.SearchSuggestionsAsync(_criterias.SearchPrompt, allowedDomains, blockedDomains); + + PostSuggestions result = await aiService.SearchSuggestionsAsync(_criterias); suggestions = result.Suggestions ?? []; StateHasChanged(); } @@ -114,12 +113,5 @@ - private class SearchCriterias - { - public string? SearchPrompt { get; set; } - public string? AllowedDomains { get; set; } - public string? BlockedDomains { get; set; } - } - } diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor index 192bc2e..4c57b86 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor @@ -17,23 +17,30 @@
- - - - - - @context - - - + + + + + + + + + + @context + + + +
+
+ @if( settings != null) {
@@ -42,29 +49,31 @@ - + - + - + - + - + + - - + + + Save diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/SummaryEditor.razor b/src/NoteBookmark.BlazorApp/Components/Pages/SummaryEditor.razor index abd1b19..c9bfd49 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/SummaryEditor.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/SummaryEditor.razor @@ -117,6 +117,7 @@ else{ private string? readingNotesHTML = string.Empty; private bool isGenarating = false; + private string rawPrompt = string.Empty; protected override async Task OnInitializedAsync() { @@ -128,6 +129,12 @@ 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) @@ -254,7 +261,8 @@ else{ isGenarating = true; var summaryText = readingNotes!.ToMarkDown(); try{ - string introText = await aiService.GenerateSummaryAsync(summaryText); + string prompt = rawPrompt.Replace("{content}", summaryText); + string introText = await aiService.GenerateSummaryAsync(prompt); readingNotes.Intro = introText; } catch(Exception ex) 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/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/src/NoteBookmark.Domain/Settings.cs b/src/NoteBookmark.Domain/Settings.cs index fe55575..fe5e4eb 100644 --- a/src/NoteBookmark.Domain/Settings.cs +++ b/src/NoteBookmark.Domain/Settings.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Azure; using Azure.Data.Tables; @@ -24,10 +25,12 @@ public class Settings: ITableEntity [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; }