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 44debff..9c4b02d 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor @@ -21,18 +21,18 @@ - +
- - + +
-
+
-
+
@@ -65,18 +65,25 @@ 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() { + Domain.Settings? settings = await client.GetSettings(); + if (settings != null) + { + _criterias = new SearchCriterias(settings.SearchPrompt); + _criterias.AllowedDomains = settings.FavoriteDomains; + _criterias.BlockedDomains = settings.BlockedDomains; + } @* await LoadPosts(); *@ } private async Task FetchSuggestions() { isSearching = true; - if (string.IsNullOrWhiteSpace(_criterias.SearchPrompt)) + if (string.IsNullOrWhiteSpace(_criterias.SearchTopic)) { toastService.ShowError("Please enter a search prompt."); isSearching = false; @@ -84,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(); } @@ -108,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 d96427d..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) {
@@ -41,13 +48,34 @@ - - - + + + + + + - + + + + + + + + + + + + + + + + + + + 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 e12fcc1..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; @@ -13,6 +14,25 @@ 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")] + [ContainsPlaceholder("content")] + public string? SummaryPrompt { get; set; } + + + [DataMember(Name="search_prompt")] + [ContainsPlaceholder("topic")] + public string? SearchPrompt { get; set; } + public required string PartitionKey { get ; set; } public required string RowKey { get ; set; } public DateTimeOffset? Timestamp { get; set; }