From 3e7dce119fb1d6045c390d205979d38ffac45be4 Mon Sep 17 00:00:00 2001 From: Frank Boucher Date: Fri, 24 Oct 2025 10:26:10 -0400 Subject: [PATCH 01/11] 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/11] [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/11] 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/11] 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 b2c7815fa422e99871ccf106b6c19d4ca7ff67da Mon Sep 17 00:00:00 2001 From: fboucher Date: Fri, 14 Nov 2025 10:05:10 -0500 Subject: [PATCH 05/11] 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 06/11] 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 07/11] 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 08/11] 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 09/11] 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 10/11] 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 11/11] 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); + } +}