Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/NoteBookmark.AIServices/ResearchService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,18 @@ public class ResearchService(HttpClient client, ILogger<ResearchService> 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<PostSuggestions> SearchSuggestionsAsync(string topic, string[]? allowedDomains, string[]? blockedDomains)
public async Task<PostSuggestions> 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<string, object>
{
["max_uses"] = 3
};

var allowedDomains = searchCriterias.GetSplittedAllowedDomains();
var blockedDomains = searchCriterias.GetSplittedBlockedDomains();

if (allowedDomains != null && allowedDomains.Length > 0)
{
webSearch["allowed_domains"] = allowedDomains;
Expand All @@ -45,7 +47,7 @@ public async Task<PostSuggestions> SearchSuggestionsAsync(string topic, string[]
new
{
role = "user",
content = query
content = searchCriterias.GetSearchPrompt()
}
},
response_format = GetResponseFormat(),
Expand Down
5 changes: 2 additions & 3 deletions src/NoteBookmark.AIServices/SummaryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ public class SummaryService(HttpClient client, ILogger<SummaryService> 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<string> GenerateSummaryAsync(string summaryText)
public async Task<string> GenerateSummaryAsync(string prompt)
{
string introParagraph;
string query = $"write a short introduction paragraph, without using '—', for the blog post: {summaryText}";

_client.Timeout = TimeSpan.FromSeconds(300);

Expand All @@ -32,7 +31,7 @@ public async Task<string> GenerateSummaryAsync(string summaryText)
new
{
role = "user",
content = query
content = prompt
}
}
};
Expand Down
11 changes: 11 additions & 0 deletions src/NoteBookmark.Api/SettingEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ static async Task<Results<Ok<Settings>, 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();
}
}
65 changes: 62 additions & 3 deletions src/NoteBookmark.BlazorApp/Components/Pages/Home.razor
Original file line number Diff line number Diff line change
@@ -1,7 +1,66 @@
@page "/"
@using Microsoft.FluentUI.AspNetCore.Components
@inject NavigationManager Navigation

<PageTitle>Home</PageTitle>
<PageTitle>Home - NoteBookmark</PageTitle>

<h1>Hello, world!</h1>
<FluentStack Orientation="Orientation.Vertical" VerticalGap="20">
<h1>📚 NoteBookmark</h1>

Welcome to your new Fluent Blazor app.
<FluentCard Width="932px">
<p>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.</p>
</FluentCard>

<FluentStack Orientation="Orientation.Horizontal" Wrap="true" HorizontalGap="16">
<FluentCard Width="300px" Height="180px">
<FluentStack Orientation="Orientation.Vertical" VerticalGap="8">
<div style="font-size: 2em;">📝</div>
<h3>Manage Posts</h3>
<p>Collect articles to read and add your notes as you go through them.</p>
</FluentStack>
</FluentCard>

<FluentCard Width="300px" Height="180px">
<FluentStack Orientation="Orientation.Vertical" VerticalGap="8">
<div style="font-size: 2em;">🔍</div>
<h3>AI-Powered Search</h3>
<p>Discover relevant content with intelligent suggestions tailored to your interests.</p>
</FluentStack>
</FluentCard>

<FluentCard Width="300px" Height="180px">
<FluentStack Orientation="Orientation.Vertical" VerticalGap="8">
<div style="font-size: 2em;">✨</div>
<h3>Generate Summaries</h3>
<p>Create beautiful summaries of your reading notes with AI assistance.</p>
</FluentStack>
</FluentCard>
</FluentStack>

<FluentDivider />

<FluentStack Orientation="Orientation.Vertical" VerticalGap="8">
<h3>Built with Modern Tech</h3>
<FluentStack Orientation="Orientation.Horizontal" Wrap="true" HorizontalGap="12">
<a href="https://dotnet.microsoft.com" target="_blank" style="text-decoration: none;">
<FluentBadge Appearance="Appearance.Accent">.NET 9</FluentBadge>
</a>
<a href="https://blazor.net" target="_blank" style="text-decoration: none;">
<FluentBadge Appearance="Appearance.Accent">Blazor</FluentBadge>
</a>
<a href="https://fluentui-blazor.net" target="_blank" style="text-decoration: none;">
<FluentBadge Appearance="Appearance.Accent">Fluent UI Blazor</FluentBadge>
</a>
<a href="https://aspire.dev" target="_blank" style="text-decoration: none;">
<FluentBadge Appearance="Appearance.Accent">Aspire</FluentBadge>
</a>
<a href="https://azure.microsoft.com/services/storage/tables" target="_blank" style="text-decoration: none;">
<FluentBadge Appearance="Appearance.Accent">Azure Table Storage</FluentBadge>
</a>
<a href="https://reka.ai" target="_blank" style="text-decoration: none;">
<FluentBadge Appearance="Appearance.Accent">Reka AI</FluentBadge>
</a>
</FluentStack>
</FluentStack>
</FluentStack>
34 changes: 16 additions & 18 deletions src/NoteBookmark.BlazorApp/Components/Pages/Search.razor
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@

<FluentStack Orientation="Orientation.Vertical" Width="100%">

<FluentValidationMessage For="@(() => _criterias.SearchPrompt)" />
<FluentValidationMessage For="@(() => _criterias.SearchTopic)" />
<div>
<FluentTextArea Name="Search Prompt" Label="Search Prompt" @bind-Value="_criterias.SearchPrompt" Required="true" Rows="3" Cols="80"/>
<FluentValidationMessage For="@(() => _criterias.SearchPrompt)" />
<FluentTextArea Name="Search Topic" Label="Search Topic" @bind-Value="_criterias.SearchTopic" Required="true" Rows="3" Cols="80"/>
<FluentValidationMessage For="@(() => _criterias.SearchTopic)" />
</div>

<div>
<div style="width: 100%;">
<FluentTextField Name="Allowed Domains" Label="Allowed Domains (Comma Separated)" @bind-Value="_criterias.AllowedDomains" style="width: 80%;" />
<FluentValidationMessage For="@(() => _criterias.AllowedDomains)" />
</div>

<div>
<div style="width: 100%;">
<FluentTextField Name="Blocked Domains" Label="Blocked Domains (Comma Separated)" @bind-Value="_criterias.BlockedDomains" style="width: 80%;" />
<FluentValidationMessage For="@(() => _criterias.BlockedDomains)" />
</div>
Expand Down Expand Up @@ -62,32 +62,37 @@
private List<PostSuggestion>? suggestions;
private GridSort<PostSuggestion> defSort = GridSort<PostSuggestion>.ByDescending(c => c.PublicationDate);
private string newPostUrl = string.Empty;
private bool showRead = false;

Check warning on line 65 in src/NoteBookmark.BlazorApp/Components/Pages/Search.razor

View workflow job for this annotation

GitHub Actions / Run Unit Tests

The field 'Search.showRead' is assigned but its value is never used
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);

Check warning on line 76 in src/NoteBookmark.BlazorApp/Components/Pages/Search.razor

View workflow job for this annotation

GitHub Actions / Run Unit Tests

Possible null reference argument for parameter 'searchPrompt' in 'SearchCriterias.SearchCriterias(string 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;
return;
}

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();
}
Expand All @@ -108,12 +113,5 @@



private class SearchCriterias
{
public string? SearchPrompt { get; set; }
public string? AllowedDomains { get; set; }
public string? BlockedDomains { get; set; }
}


}
64 changes: 46 additions & 18 deletions src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,65 @@

<div>
<FluentStack Orientation="Orientation.Vertical">
<FluentSelect Label="Theme" Width="150px"
Items="@(Enum.GetValues<DesignThemeModes>())"
@bind-SelectedOption="@Mode" />
<FluentSelect Label="Color"
Items="@(Enum.GetValues<OfficeColor>().Select(i => (OfficeColor?)i))"
Height="200px" Width="250px" @bind-SelectedOption="@OfficeColor">
<OptionTemplate>
<FluentStack>
<FluentIcon Value="@(new Icons.Filled.Size20.RectangleLandscape())" Color="Color.Custom"
CustomColor="@(@context.ToAttributeValue() != "default" ? context.ToAttributeValue() : "#036ac4" )" />
<FluentLabel>@context</FluentLabel>
</FluentStack>
</OptionTemplate>
</FluentSelect>
<FluentStack Orientation="Orientation.Horizontal" Width="100%">
<FluentSelect Label="Theme" Width="150px"
Items="@(Enum.GetValues<DesignThemeModes>())"
@bind-SelectedOption="@Mode" />
</FluentStack>

<FluentStack Orientation="Orientation.Horizontal" Width="100%">
<FluentSelect Label="Color"
Items="@(Enum.GetValues<OfficeColor>().Select(i => (OfficeColor?)i))"
Height="200px" Width="250px" @bind-SelectedOption="@OfficeColor">
<OptionTemplate>
<FluentStack>
<FluentIcon Value="@(new Icons.Filled.Size20.RectangleLandscape())" Color="Color.Custom"
CustomColor="@(@context.ToAttributeValue() != "default" ? context.ToAttributeValue() : "#036ac4" )" />
<FluentLabel>@context</FluentLabel>
</FluentStack>
</OptionTemplate>
</FluentSelect>
</FluentStack>
</FluentStack>
</div>

<br/>

@if( settings != null)
{
<div>
<EditForm Model="@settings" OnValidSubmit="SaveSettings">
<DataAnnotationsValidator />
<ValidationSummary />
<FluentStack Orientation="Orientation.Vertical" Width="100%">
<FluentTextField Label="Last Bookmark Date" @bind-Value="settings!.LastBookmarkDate" />

<FluentStack Orientation="Orientation.Horizontal">

<FluentStack Orientation="Orientation.Horizontal" Width="100%" VerticalAlignment="VerticalAlignment.Center">
<FluentTextField Label="Last Bookmark Date" @bind-Value="settings!.LastBookmarkDate" />
</FluentStack>

<FluentStack Orientation="Orientation.Horizontal" Width="100%" VerticalAlignment="VerticalAlignment.Center">
<FluentTextField Label="Reading Notes Counter" @bind-Value="settings!.ReadingNotesCounter" />
<FluentButton OnClick="IncrementCounter" Appearance="Appearance.Accent" IconEnd="@(new Icons.Regular.Size16.Add())"/>
</FluentStack>


<FluentStack Orientation="Orientation.Horizontal" Width="100%">
<FluentTextField Label="Favorite Domains" @bind-Value="settings!.FavoriteDomains" Style="width:80%" />
</FluentStack>

<FluentStack Orientation="Orientation.Horizontal" Width="100%">
<FluentTextField Label="Blocked Domains" @bind-Value="settings!.BlockedDomains" Style="width:80%" />
</FluentStack>

<FluentStack Orientation="Orientation.Horizontal" Width="100%">
<FluentTextArea Label="Summary Prompt - must contain {content}." @bind-Value="settings!.SummaryPrompt" Cols="80" Rows="3"/>
<FluentValidationMessage For="@(() => settings!.SummaryPrompt)" />
</FluentStack>

<FluentStack Orientation="Orientation.Horizontal" Width="100%">
<FluentTextArea Label="Search Prompt - must contain {topic}. Will be replace at search time" @bind-Value="settings!.SearchPrompt" Cols="80" Rows="3" />
<FluentValidationMessage For="@(() => settings!.SearchPrompt)" />
</FluentStack>

<FluentButton Type="ButtonType.Submit" Appearance="Appearance.Accent">Save</FluentButton>

</FluentStack>
Expand Down
10 changes: 9 additions & 1 deletion src/NoteBookmark.BlazorApp/Components/Pages/SummaryEditor.razor
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ else{
private string? readingNotesHTML = string.Empty;

private bool isGenarating = false;
private string rawPrompt = string.Empty;

protected override async Task OnInitializedAsync()
{
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
31 changes: 31 additions & 0 deletions src/NoteBookmark.Domain/ContainsPlaceholderAttribute.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
32 changes: 32 additions & 0 deletions src/NoteBookmark.Domain/SearchCriterias.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading