Skip to content

Commit 8c9f920

Browse files
authored
Merge pull request #81 from fboucher/76-make-the-prompt-part-of-the-settings
Enables customizable AI prompts via settings
2 parents f027c1d + 8c0e420 commit 8c9f920

File tree

10 files changed

+234
-46
lines changed

10 files changed

+234
-46
lines changed

src/NoteBookmark.AIServices/ResearchService.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,18 @@ public class ResearchService(HttpClient client, ILogger<ResearchService> logger,
1717
private const string MODEL_NAME = "reka-flash-research";
1818
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.");
1919

20-
public async Task<PostSuggestions> SearchSuggestionsAsync(string topic, string[]? allowedDomains, string[]? blockedDomains)
20+
public async Task<PostSuggestions> SearchSuggestionsAsync(SearchCriterias searchCriterias)
2121
{
2222
PostSuggestions suggestions = new PostSuggestions();
23-
string query = $"Provide interesting a list of 3 blog posts, published recently, that talks about the topic: {topic}.";
2423

2524
var webSearch = new Dictionary<string, object>
2625
{
2726
["max_uses"] = 3
2827
};
2928

29+
var allowedDomains = searchCriterias.GetSplittedAllowedDomains();
30+
var blockedDomains = searchCriterias.GetSplittedBlockedDomains();
31+
3032
if (allowedDomains != null && allowedDomains.Length > 0)
3133
{
3234
webSearch["allowed_domains"] = allowedDomains;
@@ -45,7 +47,7 @@ public async Task<PostSuggestions> SearchSuggestionsAsync(string topic, string[]
4547
new
4648
{
4749
role = "user",
48-
content = query
50+
content = searchCriterias.GetSearchPrompt()
4951
}
5052
},
5153
response_format = GetResponseFormat(),

src/NoteBookmark.AIServices/SummaryService.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@ public class SummaryService(HttpClient client, ILogger<SummaryService> logger, I
1616
private const string MODEL_NAME = "reka-flash-3.1";
1717
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.");
1818

19-
public async Task<string> GenerateSummaryAsync(string summaryText)
19+
public async Task<string> GenerateSummaryAsync(string prompt)
2020
{
2121
string introParagraph;
22-
string query = $"write a short introduction paragraph, without using '—', for the blog post: {summaryText}";
2322

2423
_client.Timeout = TimeSpan.FromSeconds(300);
2524

@@ -32,7 +31,7 @@ public async Task<string> GenerateSummaryAsync(string summaryText)
3231
new
3332
{
3433
role = "user",
35-
content = query
34+
content = prompt
3635
}
3736
}
3837
};

src/NoteBookmark.Api/SettingEndpoints.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ static async Task<Results<Ok<Settings>, BadRequest>> GetSettings(TableServiceCli
6060
{
6161
var dataStorageService = new DataStorageService(tblClient, blobClient);
6262
var settings = await dataStorageService.GetSettings();
63+
64+
if(settings!.SearchPrompt == null)
65+
{
66+
settings.SearchPrompt = "Provide interesting a list of 3 blog posts, published recently, that talks about the topic: {topic}.";
67+
}
68+
69+
if(settings.SummaryPrompt == null)
70+
{
71+
settings.SummaryPrompt = "write a short introduction paragraph, without using '—', for the blog post: {content}";
72+
}
73+
6374
return settings != null ? TypedResults.Ok(settings) : TypedResults.BadRequest();
6475
}
6576
}
Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,66 @@
11
@page "/"
2+
@using Microsoft.FluentUI.AspNetCore.Components
3+
@inject NavigationManager Navigation
24

3-
<PageTitle>Home</PageTitle>
5+
<PageTitle>Home - NoteBookmark</PageTitle>
46

5-
<h1>Hello, world!</h1>
7+
<FluentStack Orientation="Orientation.Vertical" VerticalGap="20">
8+
<h1>📚 NoteBookmark</h1>
69

7-
Welcome to your new Fluent Blazor app.
10+
<FluentCard Width="932px">
11+
<p>Your personal reading companion for capturing thoughts and insights from articles and blog posts.
12+
Transform your reading notes into polished summaries, perfect for sharing your weekly discoveries.</p>
13+
</FluentCard>
14+
15+
<FluentStack Orientation="Orientation.Horizontal" Wrap="true" HorizontalGap="16">
16+
<FluentCard Width="300px" Height="180px">
17+
<FluentStack Orientation="Orientation.Vertical" VerticalGap="8">
18+
<div style="font-size: 2em;">📝</div>
19+
<h3>Manage Posts</h3>
20+
<p>Collect articles to read and add your notes as you go through them.</p>
21+
</FluentStack>
22+
</FluentCard>
23+
24+
<FluentCard Width="300px" Height="180px">
25+
<FluentStack Orientation="Orientation.Vertical" VerticalGap="8">
26+
<div style="font-size: 2em;">🔍</div>
27+
<h3>AI-Powered Search</h3>
28+
<p>Discover relevant content with intelligent suggestions tailored to your interests.</p>
29+
</FluentStack>
30+
</FluentCard>
31+
32+
<FluentCard Width="300px" Height="180px">
33+
<FluentStack Orientation="Orientation.Vertical" VerticalGap="8">
34+
<div style="font-size: 2em;">✨</div>
35+
<h3>Generate Summaries</h3>
36+
<p>Create beautiful summaries of your reading notes with AI assistance.</p>
37+
</FluentStack>
38+
</FluentCard>
39+
</FluentStack>
40+
41+
<FluentDivider />
42+
43+
<FluentStack Orientation="Orientation.Vertical" VerticalGap="8">
44+
<h3>Built with Modern Tech</h3>
45+
<FluentStack Orientation="Orientation.Horizontal" Wrap="true" HorizontalGap="12">
46+
<a href="https://dotnet.microsoft.com" target="_blank" style="text-decoration: none;">
47+
<FluentBadge Appearance="Appearance.Accent">.NET 9</FluentBadge>
48+
</a>
49+
<a href="https://blazor.net" target="_blank" style="text-decoration: none;">
50+
<FluentBadge Appearance="Appearance.Accent">Blazor</FluentBadge>
51+
</a>
52+
<a href="https://fluentui-blazor.net" target="_blank" style="text-decoration: none;">
53+
<FluentBadge Appearance="Appearance.Accent">Fluent UI Blazor</FluentBadge>
54+
</a>
55+
<a href="https://aspire.dev" target="_blank" style="text-decoration: none;">
56+
<FluentBadge Appearance="Appearance.Accent">Aspire</FluentBadge>
57+
</a>
58+
<a href="https://azure.microsoft.com/services/storage/tables" target="_blank" style="text-decoration: none;">
59+
<FluentBadge Appearance="Appearance.Accent">Azure Table Storage</FluentBadge>
60+
</a>
61+
<a href="https://reka.ai" target="_blank" style="text-decoration: none;">
62+
<FluentBadge Appearance="Appearance.Accent">Reka AI</FluentBadge>
63+
</a>
64+
</FluentStack>
65+
</FluentStack>
66+
</FluentStack>

src/NoteBookmark.BlazorApp/Components/Pages/Search.razor

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,18 @@
2121

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

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

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

35-
<div>
35+
<div style="width: 100%;">
3636
<FluentTextField Name="Blocked Domains" Label="Blocked Domains (Comma Separated)" @bind-Value="_criterias.BlockedDomains" style="width: 80%;" />
3737
<FluentValidationMessage For="@(() => _criterias.BlockedDomains)" />
3838
</div>
@@ -65,29 +65,34 @@
6565
private bool showRead = false;
6666
private bool isSearching = false;
6767

68-
private SearchCriterias _criterias = new SearchCriterias();
68+
private SearchCriterias _criterias = new SearchCriterias(string.Empty);
6969

7070

7171
protected override async Task OnInitializedAsync()
7272
{
73+
Domain.Settings? settings = await client.GetSettings();
74+
if (settings != null)
75+
{
76+
_criterias = new SearchCriterias(settings.SearchPrompt);
77+
_criterias.AllowedDomains = settings.FavoriteDomains;
78+
_criterias.BlockedDomains = settings.BlockedDomains;
79+
}
7380
@* await LoadPosts(); *@
7481
}
7582

7683
private async Task FetchSuggestions()
7784
{
7885
isSearching = true;
79-
if (string.IsNullOrWhiteSpace(_criterias.SearchPrompt))
86+
if (string.IsNullOrWhiteSpace(_criterias.SearchTopic))
8087
{
8188
toastService.ShowError("Please enter a search prompt.");
8289
isSearching = false;
8390
return;
8491
}
8592

8693
try{
87-
var allowedDomains = _criterias.AllowedDomains?.Split(',').Select(d => d.Trim()).ToArray();
88-
var blockedDomains = _criterias.BlockedDomains?.Split(',').Select(d => d.Trim()).ToArray();
89-
90-
PostSuggestions result = await aiService.SearchSuggestionsAsync(_criterias.SearchPrompt, allowedDomains, blockedDomains);
94+
95+
PostSuggestions result = await aiService.SearchSuggestionsAsync(_criterias);
9196
suggestions = result.Suggestions ?? [];
9297
StateHasChanged();
9398
}
@@ -108,12 +113,5 @@
108113

109114

110115

111-
private class SearchCriterias
112-
{
113-
public string? SearchPrompt { get; set; }
114-
public string? AllowedDomains { get; set; }
115-
public string? BlockedDomains { get; set; }
116-
}
117-
118116

119117
}

src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,37 +17,65 @@
1717

1818
<div>
1919
<FluentStack Orientation="Orientation.Vertical">
20-
<FluentSelect Label="Theme" Width="150px"
21-
Items="@(Enum.GetValues<DesignThemeModes>())"
22-
@bind-SelectedOption="@Mode" />
23-
<FluentSelect Label="Color"
24-
Items="@(Enum.GetValues<OfficeColor>().Select(i => (OfficeColor?)i))"
25-
Height="200px" Width="250px" @bind-SelectedOption="@OfficeColor">
26-
<OptionTemplate>
27-
<FluentStack>
28-
<FluentIcon Value="@(new Icons.Filled.Size20.RectangleLandscape())" Color="Color.Custom"
29-
CustomColor="@(@context.ToAttributeValue() != "default" ? context.ToAttributeValue() : "#036ac4" )" />
30-
<FluentLabel>@context</FluentLabel>
31-
</FluentStack>
32-
</OptionTemplate>
33-
</FluentSelect>
20+
<FluentStack Orientation="Orientation.Horizontal" Width="100%">
21+
<FluentSelect Label="Theme" Width="150px"
22+
Items="@(Enum.GetValues<DesignThemeModes>())"
23+
@bind-SelectedOption="@Mode" />
24+
</FluentStack>
25+
26+
<FluentStack Orientation="Orientation.Horizontal" Width="100%">
27+
<FluentSelect Label="Color"
28+
Items="@(Enum.GetValues<OfficeColor>().Select(i => (OfficeColor?)i))"
29+
Height="200px" Width="250px" @bind-SelectedOption="@OfficeColor">
30+
<OptionTemplate>
31+
<FluentStack>
32+
<FluentIcon Value="@(new Icons.Filled.Size20.RectangleLandscape())" Color="Color.Custom"
33+
CustomColor="@(@context.ToAttributeValue() != "default" ? context.ToAttributeValue() : "#036ac4" )" />
34+
<FluentLabel>@context</FluentLabel>
35+
</FluentStack>
36+
</OptionTemplate>
37+
</FluentSelect>
38+
</FluentStack>
3439
</FluentStack>
3540
</div>
3641

42+
<br/>
43+
3744
@if( settings != null)
3845
{
3946
<div>
4047
<EditForm Model="@settings" OnValidSubmit="SaveSettings">
4148
<DataAnnotationsValidator />
4249
<ValidationSummary />
4350
<FluentStack Orientation="Orientation.Vertical" Width="100%">
44-
<FluentTextField Label="Last Bookmark Date" @bind-Value="settings!.LastBookmarkDate" />
45-
46-
<FluentStack Orientation="Orientation.Horizontal">
51+
52+
<FluentStack Orientation="Orientation.Horizontal" Width="100%" VerticalAlignment="VerticalAlignment.Center">
53+
<FluentTextField Label="Last Bookmark Date" @bind-Value="settings!.LastBookmarkDate" />
54+
</FluentStack>
55+
56+
<FluentStack Orientation="Orientation.Horizontal" Width="100%" VerticalAlignment="VerticalAlignment.Center">
4757
<FluentTextField Label="Reading Notes Counter" @bind-Value="settings!.ReadingNotesCounter" />
4858
<FluentButton OnClick="IncrementCounter" Appearance="Appearance.Accent" IconEnd="@(new Icons.Regular.Size16.Add())"/>
4959
</FluentStack>
50-
60+
61+
<FluentStack Orientation="Orientation.Horizontal" Width="100%">
62+
<FluentTextField Label="Favorite Domains" @bind-Value="settings!.FavoriteDomains" Style="width:80%" />
63+
</FluentStack>
64+
65+
<FluentStack Orientation="Orientation.Horizontal" Width="100%">
66+
<FluentTextField Label="Blocked Domains" @bind-Value="settings!.BlockedDomains" Style="width:80%" />
67+
</FluentStack>
68+
69+
<FluentStack Orientation="Orientation.Horizontal" Width="100%">
70+
<FluentTextArea Label="Summary Prompt - must contain {content}." @bind-Value="settings!.SummaryPrompt" Cols="80" Rows="3"/>
71+
<FluentValidationMessage For="@(() => settings!.SummaryPrompt)" />
72+
</FluentStack>
73+
74+
<FluentStack Orientation="Orientation.Horizontal" Width="100%">
75+
<FluentTextArea Label="Search Prompt - must contain {topic}. Will be replace at search time" @bind-Value="settings!.SearchPrompt" Cols="80" Rows="3" />
76+
<FluentValidationMessage For="@(() => settings!.SearchPrompt)" />
77+
</FluentStack>
78+
5179
<FluentButton Type="ButtonType.Submit" Appearance="Appearance.Accent">Save</FluentButton>
5280

5381
</FluentStack>

src/NoteBookmark.BlazorApp/Components/Pages/SummaryEditor.razor

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ else{
117117
private string? readingNotesHTML = string.Empty;
118118

119119
private bool isGenarating = false;
120+
private string rawPrompt = string.Empty;
120121

121122
protected override async Task OnInitializedAsync()
122123
{
@@ -128,6 +129,12 @@ else{
128129
{
129130
readingNotes = await client.GetReadingNotes(number);
130131
}
132+
133+
var settings = await client.GetSettings();
134+
if(settings != null)
135+
{
136+
rawPrompt = settings!.SummaryPrompt ?? string.Empty;
137+
}
131138
}
132139

133140
private async Task OpenUrlInNewWindow(string? url)
@@ -254,7 +261,8 @@ else{
254261
isGenarating = true;
255262
var summaryText = readingNotes!.ToMarkDown();
256263
try{
257-
string introText = await aiService.GenerateSummaryAsync(summaryText);
264+
string prompt = rawPrompt.Replace("{content}", summaryText);
265+
string introText = await aiService.GenerateSummaryAsync(prompt);
258266
readingNotes.Intro = introText;
259267
}
260268
catch(Exception ex)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace NoteBookmark.Domain;
4+
5+
public class ContainsPlaceholderAttribute : ValidationAttribute
6+
{
7+
private readonly string _placeholder;
8+
9+
public ContainsPlaceholderAttribute(string placeholder)
10+
{
11+
_placeholder = placeholder;
12+
ErrorMessage = $"The field must contain '{{{placeholder}}}'.";
13+
}
14+
15+
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
16+
{
17+
if (value == null || string.IsNullOrWhiteSpace(value.ToString()))
18+
{
19+
return ValidationResult.Success;
20+
}
21+
22+
string stringValue = value.ToString()!;
23+
24+
if (!stringValue.Contains($"{{{_placeholder}}}"))
25+
{
26+
return new ValidationResult(ErrorMessage);
27+
}
28+
29+
return ValidationResult.Success;
30+
}
31+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
3+
namespace NoteBookmark.Domain;
4+
5+
public class SearchCriterias
6+
{
7+
public string? SearchTopic { get; set; }
8+
private string SearchPrompt {get;}
9+
public string? AllowedDomains { get; set; }
10+
public string? BlockedDomains { get; set; }
11+
12+
public SearchCriterias(string searchPrompt)
13+
{
14+
this.SearchPrompt = searchPrompt;
15+
}
16+
17+
public string[]? GetSplittedAllowedDomains()
18+
{
19+
return AllowedDomains?.Split(',').Select(d => d.Trim()).ToArray();
20+
}
21+
22+
public string[]? GetSplittedBlockedDomains()
23+
{
24+
return BlockedDomains?.Split(',').Select(d => d.Trim()).ToArray();
25+
}
26+
27+
public string? GetSearchPrompt()
28+
{
29+
var tempPrompt = this.SearchPrompt?.Replace("{topic}", " " + this.SearchTopic + " " ?? "");
30+
return tempPrompt;
31+
}
32+
}

0 commit comments

Comments
 (0)