From b8b3a33bcb9bab8942ae5a0088072901e5edc7ac Mon Sep 17 00:00:00 2001 From: Frank Boucher Date: Wed, 10 Dec 2025 17:28:24 -0500 Subject: [PATCH 1/2] 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 2/2] 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; }