@@ -56,6 +72,14 @@
OnClick="@CancelAsync">
Cancel
+ @if (_isEditMode)
+ {
+
+ Delete
+
+ }
@code {
@@ -68,19 +92,40 @@
private Domain.Note _note = default!;
private List
_categories = NoteCategories.GetCategories();
+ private bool _isEditMode = false;
+
+ private List _currentTags = new();
+ private string _newTagInput = string.Empty;
protected override void OnInitialized()
{
- _note = new Note{PostId = Content.PostId};
+ // Check if we're editing an existing note or creating a new one
+ _isEditMode = !string.IsNullOrEmpty(Content.RowKey) && !Content.RowKey.Equals(Guid.Empty.ToString(), StringComparison.OrdinalIgnoreCase);
+
+ if (_isEditMode)
+ {
+ // Editing mode - use the existing note data
+ _note = Content;
+ }
+ else
+ {
+ // Create mode - create a new note with the PostId
+ _note = new Note { PostId = Content.PostId };
+ }
+
+ ParseTagsFromString();
}
private async Task SaveAsync()
{
- _note.DateAdded = DateTime.UtcNow;
+ if (!_isEditMode)
+ {
+ _note.DateAdded = DateTime.UtcNow;
+ }
if (_note.Validate())
{
- await Dialog.CloseAsync(_note);
+ await Dialog.CloseAsync(new NoteDialogResult { Action = "Save", Note = _note });
}
}
@@ -89,5 +134,57 @@
await Dialog.CancelAsync();
}
+ private async Task DeleteAsync()
+ {
+ await Dialog.CloseAsync(new NoteDialogResult { Action = "Delete", Note = _note });
+ }
+
+ private void ParseTagsFromString()
+ {
+ _currentTags = string.IsNullOrWhiteSpace(_note.Tags)
+ ? new List()
+ : _note.Tags.Split(',')
+ .Select(t => t.Trim().ToLower())
+ .Where(t => !string.IsNullOrWhiteSpace(t))
+ .Distinct()
+ .ToList();
+ }
+
+ private void SerializeTagsToString()
+ {
+ _note.Tags = _currentTags.Any() ? string.Join(", ", _currentTags) : null;
+ }
+
+ private void AddTag(string tag)
+ {
+ if (string.IsNullOrWhiteSpace(tag)) return;
+
+ var normalizedTag = tag.Trim().ToLower();
+ if (!_currentTags.Contains(normalizedTag))
+ {
+ _currentTags.Add(normalizedTag);
+ SerializeTagsToString();
+ _newTagInput = string.Empty;
+ StateHasChanged();
+ }
+ }
+
+ private void RemoveTag(string tag)
+ {
+ _currentTags.Remove(tag);
+ SerializeTagsToString();
+ StateHasChanged();
+ }
+
+ private void HandleKeyDown(KeyboardEventArgs e)
+ {
+ if (e.Key == "Enter")
+ {
+ if (!string.IsNullOrWhiteSpace(_newTagInput))
+ {
+ AddTag(_newTagInput);
+ }
+ }
+ }
}
diff --git a/src/NoteBookmark.BlazorApp/Data/research_response_2026-02-14_15-38.json b/src/NoteBookmark.BlazorApp/Data/research_response_2026-02-14_15-38.json
new file mode 100644
index 0000000..c522dfa
--- /dev/null
+++ b/src/NoteBookmark.BlazorApp/Data/research_response_2026-02-14_15-38.json
@@ -0,0 +1,25 @@
+{
+ "suggestions": [
+ {
+ "title": "5 Ways Your .NET Developers Can Get Started with Azure Machine Learning",
+ "author": "James Serra, Azure Developer Ecosystem blog",
+ "summary": "Azure Machine Learning is a cloud-based platform for building and deploying AI models. In this article we will explore how your .Net developers can get started working with Azure Machine Learning through Visual Studio Code.",
+ "publication_date": {},
+ "url": "https://jamesmicrosoftcom/5-ways-get-started-with-azure-machine-learning-as-a-net-developer/"
+ },
+ {
+ "title": "C# and C++ Machine Learning for .NET Developers and AI Researchers",
+ "author": "Pankaj Dua, aka ‘AI Guy’ on Microsoft’s Developer Community blog.",
+ "summary": "In this article we'll present how to use open-source libraries like Accord.NET to build machine learning models in your choice of languages i.e., C#, F# or even C++. We'll walk through building your 'first ML model' using popular tools that you might have never used.",
+ "publication_date": {},
+ "url": "https://blogs.msdn.microsoft.com/ptgoa/c-cpp-companion-piece-on-ml-in-dot-net-world/"
+ },
+ {
+ "title": ".NET AI – a new home for .NET Machine Learning",
+ "author": "Microsoft .Net blog",
+ "summary": "Find out about latest developments in the world of machine learning on .net, including deep dive into ONNX and .NET Core. ",
+ "publication_date": {},
+ "url": "https://devblogs.microsoft.com/dotnet/net-ai-a-new-home-for-net-machine-learning/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/NoteBookmark.BlazorApp/Dockerfile b/src/NoteBookmark.BlazorApp/Dockerfile
index d286c0e..c16aa42 100644
--- a/src/NoteBookmark.BlazorApp/Dockerfile
+++ b/src/NoteBookmark.BlazorApp/Dockerfile
@@ -1,9 +1,9 @@
-FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
EXPOSE 8004
EXPOSE 8006
-FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["Directory.Build.props", "/src/"]
COPY ["Directory.Packages.props", "/src/"]
diff --git a/src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj b/src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj
index e9f531d..bc0fb7c 100644
--- a/src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj
+++ b/src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj
@@ -1,7 +1,10 @@
+
+
+
diff --git a/src/NoteBookmark.BlazorApp/PostNoteClient.cs b/src/NoteBookmark.BlazorApp/PostNoteClient.cs
index 65e6a49..4ceee52 100644
--- a/src/NoteBookmark.BlazorApp/PostNoteClient.cs
+++ b/src/NoteBookmark.BlazorApp/PostNoteClient.cs
@@ -1,163 +1,181 @@
-using System;
-using NoteBookmark.Domain;
-
-namespace NoteBookmark.BlazorApp;
-
-public class PostNoteClient(HttpClient httpClient)
-{
- public async Task> GetUnreadPosts()
- {
- var posts = await httpClient.GetFromJsonAsync>("api/posts");
- return posts ?? new List();
- }
-
- public async Task> GetReadPosts()
- {
- var posts = await httpClient.GetFromJsonAsync>("api/posts/read");
- return posts ?? new List();
- }
-
- public async Task> GetSummaries()
- {
- var summaries = await httpClient.GetFromJsonAsync>("api/summary");
- return summaries ?? new List();
- }
-
- public async Task CreateNote(Note note)
- {
- var rnCounter = await httpClient.GetStringAsync("api/settings/GetNextReadingNotesCounter");
- note.PartitionKey = rnCounter;
- var response = await httpClient.PostAsJsonAsync("api/notes/note", note);
- response.EnsureSuccessStatusCode();
- }
-
- public async Task CreateReadingNotes()
- {
- var rnCounter = await httpClient.GetStringAsync("api/settings/GetNextReadingNotesCounter");
- var readingNotes = new ReadingNotes(rnCounter);
-
- //Get all unused notes
- var unsortedNotes = await httpClient.GetFromJsonAsync>($"api/notes/GetNotesForSummary/{rnCounter}");
-
- if(unsortedNotes == null || unsortedNotes.Count == 0){
- return readingNotes;
- }
-
- Dictionary> sortedNotes = GroupNotesByCategory(unsortedNotes);
-
- readingNotes.Notes = sortedNotes;
- readingNotes.Tags = readingNotes.GetAllUniqueTags();
-
- return readingNotes;
- }
-
- public async Task GetReadingNotes(string number)
- {
- ReadingNotes? readingNotes;
- readingNotes = await httpClient.GetFromJsonAsync($"api/summary/{number}");
-
- return readingNotes;
- }
-
-
- private Dictionary> GroupNotesByCategory(List notes)
- {
- var sortedNotes = new Dictionary>();
-
- foreach (var note in notes)
- {
- var tags = note.Tags?.ToLower().Split(',') ?? Array.Empty();
-
- if(string.IsNullOrEmpty(note.Category)){
- note.Category = NoteCategories.GetCategory(tags[0]);
- }
-
- string category = note.Category;
- if (sortedNotes.ContainsKey(category))
- {
- sortedNotes[category].Add(note);
- }
- else
- {
- sortedNotes.Add(category, new List {note});
- }
- }
-
- return sortedNotes;
- }
-
- public async Task SaveReadingNotes(ReadingNotes readingNotes)
- {
- var response = await httpClient.PostAsJsonAsync("api/notes/SaveReadingNotes", readingNotes);
-
- string jsonURL = ((string)await response.Content.ReadAsStringAsync()).Replace("\"", "");
-
- if (response.IsSuccessStatusCode && !string.IsNullOrEmpty(jsonURL))
- {
- var summary = new Summary
- {
- PartitionKey = readingNotes.Number,
- RowKey = readingNotes.Number,
- Title = readingNotes.Title,
- Id = readingNotes.Number,
- IsGenerated = "true",
- PublishedURL = readingNotes.PublishedUrl,
- FileName = jsonURL
- };
-
- var summaryResponse = await httpClient.PostAsJsonAsync("api/summary/summary", summary);
- return summaryResponse.IsSuccessStatusCode;
- }
-
- return false;
- }
-
-
- public async Task GetPost(string id)
- {
- var post = await httpClient.GetFromJsonAsync($"api/posts/{id}");
- return post;
- }
-
-
- public async Task SavePost(Post post)
- {
- var response = await httpClient.PostAsJsonAsync("api/posts", post);
- return response.IsSuccessStatusCode;
- }
-
- public async Task GetSettings()
- {
- var settings = await httpClient.GetFromJsonAsync("api/settings");
- return settings;
- }
-
- public async Task SaveSettings(Settings settings)
- {
- var response = await httpClient.PostAsJsonAsync("api/settings/SaveSettings", settings);
- return response.IsSuccessStatusCode;
- }
-
- public async Task ExtractPostDetailsAndSave(string url)
- {
- //var encodedUrl = System.Net.WebUtility.UrlEncode(url);
- var requestBody = new {url = url};
-
- var response = await httpClient.PostAsJsonAsync($"api/posts/extractPostDetails", requestBody);
- // var response = await httpClient.PostAsJsonAsync($"api/posts/extractPostDetails?url={encodedUrl}", url);
- return response.IsSuccessStatusCode;
- }
-
- public async Task DeletePost(string id)
- {
- var response = await httpClient.DeleteAsync($"api/posts/{id}");
- return response.IsSuccessStatusCode;
- }
-
- public async Task SaveReadingNotesMarkdown(string markdown, string number)
- {
- var request = new { Markdown = markdown };
- var response = await httpClient.PostAsJsonAsync($"api/summary/{number}/markdown", request);
- return response.IsSuccessStatusCode;
- }
-}
+using System;
+using NoteBookmark.Domain;
+
+namespace NoteBookmark.BlazorApp;
+
+public class PostNoteClient(HttpClient httpClient)
+{
+ public async Task> GetUnreadPosts()
+ {
+ var posts = await httpClient.GetFromJsonAsync>("api/posts");
+ return posts ?? new List();
+ }
+
+ public async Task> GetReadPosts()
+ {
+ var posts = await httpClient.GetFromJsonAsync>("api/posts/read");
+ return posts ?? new List();
+ }
+
+ public async Task> GetSummaries()
+ {
+ var summaries = await httpClient.GetFromJsonAsync>("api/summary");
+ return summaries ?? new List();
+ }
+
+ public async Task CreateNote(Note note)
+ {
+ var rnCounter = await httpClient.GetStringAsync("api/settings/GetNextReadingNotesCounter");
+ note.PartitionKey = rnCounter;
+ var response = await httpClient.PostAsJsonAsync("api/notes/note", note);
+ response.EnsureSuccessStatusCode();
+ }
+
+ public async Task GetNote(string noteId)
+ {
+ var note = await httpClient.GetFromJsonAsync($"api/notes/note/{noteId}");
+ return note;
+ }
+
+ public async Task UpdateNote(Note note)
+ {
+ var response = await httpClient.PutAsJsonAsync("api/notes/note", note);
+ return response.IsSuccessStatusCode;
+ }
+
+ public async Task DeleteNote(string noteId)
+ {
+ var response = await httpClient.DeleteAsync($"api/notes/note/{noteId}");
+ return response.IsSuccessStatusCode;
+ }
+
+ public async Task CreateReadingNotes()
+ {
+ var rnCounter = await httpClient.GetStringAsync("api/settings/GetNextReadingNotesCounter");
+ var readingNotes = new ReadingNotes(rnCounter);
+
+ //Get all unused notes
+ var unsortedNotes = await httpClient.GetFromJsonAsync>($"api/notes/GetNotesForSummary/{rnCounter}");
+
+ if(unsortedNotes == null || unsortedNotes.Count == 0){
+ return readingNotes;
+ }
+
+ Dictionary> sortedNotes = GroupNotesByCategory(unsortedNotes);
+
+ readingNotes.Notes = sortedNotes;
+ readingNotes.Tags = readingNotes.GetAllUniqueTags();
+
+ return readingNotes;
+ }
+
+ public async Task GetReadingNotes(string number)
+ {
+ ReadingNotes? readingNotes;
+ readingNotes = await httpClient.GetFromJsonAsync($"api/summary/{number}");
+
+ return readingNotes;
+ }
+
+
+ private Dictionary> GroupNotesByCategory(List notes)
+ {
+ var sortedNotes = new Dictionary>();
+
+ foreach (var note in notes)
+ {
+ var tags = note.Tags?.ToLower().Split(',') ?? Array.Empty();
+
+ if(string.IsNullOrEmpty(note.Category)){
+ note.Category = NoteCategories.GetCategory(tags[0]);
+ }
+
+ string category = note.Category;
+ if (sortedNotes.ContainsKey(category))
+ {
+ sortedNotes[category].Add(note);
+ }
+ else
+ {
+ sortedNotes.Add(category, new List {note});
+ }
+ }
+
+ return sortedNotes;
+ }
+
+ public async Task SaveReadingNotes(ReadingNotes readingNotes)
+ {
+ var response = await httpClient.PostAsJsonAsync("api/notes/SaveReadingNotes", readingNotes);
+
+ string jsonURL = ((string)await response.Content.ReadAsStringAsync()).Replace("\"", "");
+
+ if (response.IsSuccessStatusCode && !string.IsNullOrEmpty(jsonURL))
+ {
+ var summary = new Summary
+ {
+ PartitionKey = readingNotes.Number,
+ RowKey = readingNotes.Number,
+ Title = readingNotes.Title,
+ Id = readingNotes.Number,
+ IsGenerated = "true",
+ PublishedURL = readingNotes.PublishedUrl,
+ FileName = jsonURL
+ };
+
+ var summaryResponse = await httpClient.PostAsJsonAsync("api/summary/summary", summary);
+ return summaryResponse.IsSuccessStatusCode;
+ }
+
+ return false;
+ }
+
+
+ public async Task GetPost(string id)
+ {
+ var post = await httpClient.GetFromJsonAsync($"api/posts/{id}");
+ return post;
+ }
+
+
+ public async Task SavePost(Post post)
+ {
+ var response = await httpClient.PostAsJsonAsync("api/posts", post);
+ return response.IsSuccessStatusCode;
+ }
+
+ public async Task GetSettings()
+ {
+ var settings = await httpClient.GetFromJsonAsync("api/settings");
+ return settings;
+ }
+
+ public async Task SaveSettings(Settings settings)
+ {
+ var response = await httpClient.PostAsJsonAsync("api/settings/SaveSettings", settings);
+ return response.IsSuccessStatusCode;
+ }
+
+ public async Task ExtractPostDetailsAndSave(string url)
+ {
+ //var encodedUrl = System.Net.WebUtility.UrlEncode(url);
+ var requestBody = new {url = url};
+
+ var response = await httpClient.PostAsJsonAsync($"api/posts/extractPostDetails", requestBody);
+ // var response = await httpClient.PostAsJsonAsync($"api/posts/extractPostDetails?url={encodedUrl}", url);
+ return response.IsSuccessStatusCode;
+ }
+
+ public async Task DeletePost(string id)
+ {
+ var response = await httpClient.DeleteAsync($"api/posts/{id}");
+ return response.IsSuccessStatusCode;
+ }
+
+ public async Task SaveReadingNotesMarkdown(string markdown, string number)
+ {
+ var request = new { Markdown = markdown };
+ var response = await httpClient.PostAsJsonAsync($"api/summary/{number}/markdown", request);
+ return response.IsSuccessStatusCode;
+ }
+}
diff --git a/src/NoteBookmark.BlazorApp/Program.cs b/src/NoteBookmark.BlazorApp/Program.cs
index 896b180..5404ab5 100644
--- a/src/NoteBookmark.BlazorApp/Program.cs
+++ b/src/NoteBookmark.BlazorApp/Program.cs
@@ -1,3 +1,6 @@
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.FluentUI.AspNetCore.Components;
using NoteBookmark.AIServices;
using NoteBookmark.BlazorApp;
@@ -6,41 +9,132 @@
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
+builder.AddAzureTableClient("nb-tables");
-// 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);
-// });
-
+// Add HTTP client for API calls
builder.Services.AddHttpClient(client =>
{
client.BaseAddress = new Uri("https+http://api");
});
-builder.Services.AddHttpClient(client =>
+// Register server-side AI settings provider (direct database access, unmasked)
+builder.Services.AddScoped();
+
+// Register AI services with settings provider that reads directly from database
+builder.Services.AddTransient(sp =>
{
- client.Timeout = TimeSpan.FromMinutes(5);
+ var logger = sp.GetRequiredService>();
+ var settingsProvider = sp.GetRequiredService();
+
+ // Settings provider that fetches directly from database (server-side, unmasked)
+ Func> provider = async () =>
+ {
+ var settings = await settingsProvider.GetAISettingsAsync();
+ return (
+ settings.ApiKey,
+ settings.BaseUrl,
+ settings.ModelName
+ );
+ };
+
+ return new SummaryService(logger, provider);
});
+builder.Services.AddHttpClient(nameof(ResearchService));
+
+builder.Services.AddTransient(sp =>
+{
+ var logger = sp.GetRequiredService>();
+ var settingsProvider = sp.GetRequiredService();
+ var httpClientFactory = sp.GetRequiredService();
+ var client = httpClientFactory.CreateClient(nameof(ResearchService));
+
+ // Settings provider that fetches directly from database (server-side, unmasked)
+ Func> provider = async () =>
+ {
+ var settings = await settingsProvider.GetAISettingsAsync();
+ return (
+ settings.ApiKey,
+ settings.BaseUrl,
+ settings.ModelName
+ );
+ };
+
+ return new ResearchService(client, logger, provider);
+});
+
+
+// Add authentication
+builder.Services.AddAuthentication(options =>
+{
+ options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
+})
+.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
+.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
+{
+ var authority = builder.Configuration["Keycloak:Authority"];
+ options.Authority = authority;
+ options.ClientId = builder.Configuration["Keycloak:ClientId"];
+ options.ClientSecret = builder.Configuration["Keycloak:ClientSecret"];
+ options.ResponseType = "code";
+ options.SaveTokens = true;
+ options.GetClaimsFromUserInfoEndpoint = true;
+
+ // Allow overriding RequireHttpsMetadata via configuration for development/docker scenarios.
+ // If not explicitly configured, relax the requirement when running in a container against HTTP Keycloak.
+ var requireHttpsConfigured = builder.Configuration.GetValue("Keycloak:RequireHttpsMetadata");
+ var isRunningInContainer = string.Equals(
+ System.Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER"),
+ "true",
+ StringComparison.OrdinalIgnoreCase);
-builder.Services.AddHttpClient();
- // .AddStandardResilienceHandler();
+ if (requireHttpsConfigured.HasValue)
+ {
+ options.RequireHttpsMetadata = requireHttpsConfigured.Value;
+ }
+ else
+ {
+ var defaultRequireHttps = !builder.Environment.IsDevelopment();
+ if (isRunningInContainer &&
+ !string.IsNullOrEmpty(authority) &&
+ authority.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
+ {
+ defaultRequireHttps = false;
+ }
+
+ options.RequireHttpsMetadata = defaultRequireHttps;
+ }
+
+ options.Scope.Clear();
+ options.Scope.Add("openid");
+ options.Scope.Add("profile");
+ options.Scope.Add("email");
+
+ options.TokenValidationParameters = new()
+ {
+ NameClaimType = "preferred_username",
+ RoleClaimType = "roles"
+ };
+
+ // Configure logout to properly pass id_token_hint to Keycloak
+ options.Events = new OpenIdConnectEvents
+ {
+ OnRedirectToIdentityProviderForSignOut = async context =>
+ {
+ // Get the id_token from saved tokens
+ var idToken = await context.HttpContext.GetTokenAsync("id_token");
+ if (!string.IsNullOrEmpty(idToken))
+ {
+ context.ProtocolMessage.IdTokenHint = idToken;
+ }
+ }
+ };
+});
+builder.Services.AddAuthorization();
+builder.Services.AddCascadingAuthenticationState();
+builder.Services.AddHttpContextAccessor();
// Add services to the container.
builder.Services.AddRazorComponents()
@@ -63,7 +157,30 @@
app.UseStaticFiles();
app.UseAntiforgery();
+app.UseAuthentication();
+app.UseAuthorization();
+
app.MapRazorComponents()
.AddInteractiveServerRenderMode();
+// Authentication endpoints
+app.MapGet("/authentication/login", async (HttpContext context, string? returnUrl) =>
+{
+ var authProperties = new AuthenticationProperties
+ {
+ RedirectUri = returnUrl ?? "/"
+ };
+ await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, authProperties);
+});
+
+app.MapGet("/authentication/logout", async (HttpContext context) =>
+{
+ var authProperties = new AuthenticationProperties
+ {
+ RedirectUri = "/"
+ };
+ await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
+ await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, authProperties);
+});
+
app.Run();
diff --git a/src/NoteBookmark.Domain/NoteDialogResult.cs b/src/NoteBookmark.Domain/NoteDialogResult.cs
new file mode 100644
index 0000000..691f609
--- /dev/null
+++ b/src/NoteBookmark.Domain/NoteDialogResult.cs
@@ -0,0 +1,7 @@
+namespace NoteBookmark.Domain;
+
+public class NoteDialogResult
+{
+ public string Action { get; set; } = "Save";
+ public Note? Note { get; set; }
+}
diff --git a/src/NoteBookmark.Domain/PostSuggestion.cs b/src/NoteBookmark.Domain/PostSuggestion.cs
index f6fe06a..54cd6b3 100644
--- a/src/NoteBookmark.Domain/PostSuggestion.cs
+++ b/src/NoteBookmark.Domain/PostSuggestion.cs
@@ -31,15 +31,57 @@ public class DateOnlyJsonConverter : JsonConverter
if (reader.TokenType == JsonTokenType.Null)
return null;
- var dateString = reader.GetString();
- if (string.IsNullOrEmpty(dateString))
- return null;
+ try
+ {
+ // Handle different JSON token types the AI might return
+ switch (reader.TokenType)
+ {
+ case JsonTokenType.String:
+ var dateString = reader.GetString();
+ if (string.IsNullOrEmpty(dateString))
+ return null;
+
+ // Try to parse as DateTime and format to yyyy-MM-dd
+ if (DateTime.TryParse(dateString, out var date))
+ {
+ return date.ToString(DateFormat);
+ }
+ // If parsing fails, return the original string
+ return dateString;
+
+ case JsonTokenType.Number:
+ // Handle Unix timestamp (seconds or milliseconds)
+ if (reader.TryGetInt64(out var timestamp))
+ {
+ DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+ // Assume milliseconds if > year 2100 in seconds (2147483647)
+ var dateTime = timestamp > 2147483647
+ ? epoch.AddMilliseconds(timestamp)
+ : epoch.AddSeconds(timestamp);
+ return dateTime.ToString(DateFormat);
+ }
+ break;
- if (DateTime.TryParse(dateString, out var date))
+ case JsonTokenType.True:
+ case JsonTokenType.False:
+ // Handle unexpected boolean - convert to string
+ return reader.GetBoolean().ToString();
+
+ case JsonTokenType.StartObject:
+ case JsonTokenType.StartArray:
+ // Handle complex types - skip and return null
+ reader.Skip();
+ return null;
+ }
+ }
+ catch
{
- return date.ToString(DateFormat);
+ // If any parsing fails, skip the value and return null to gracefully degrade
+ try { reader.Skip(); } catch { /* ignore */ }
+ return null;
}
- return dateString;
+
+ return null;
}
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
diff --git a/src/NoteBookmark.Domain/Settings.cs b/src/NoteBookmark.Domain/Settings.cs
index fe5e4eb..f37d026 100644
--- a/src/NoteBookmark.Domain/Settings.cs
+++ b/src/NoteBookmark.Domain/Settings.cs
@@ -1,40 +1,52 @@
-using System;
-using System.ComponentModel.DataAnnotations;
-using System.Runtime.Serialization;
-using Azure;
-using Azure.Data.Tables;
-
-namespace NoteBookmark.Domain;
-
-public class Settings: ITableEntity
-{
- [DataMember(Name="last_bookmark_date")]
- public string? LastBookmarkDate { get; set; }
-
-
- [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; }
- public ETag ETag { get; set; }
-}
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Runtime.Serialization;
+using Azure;
+using Azure.Data.Tables;
+
+namespace NoteBookmark.Domain;
+
+public class Settings: ITableEntity
+{
+ [DataMember(Name="last_bookmark_date")]
+ public string? LastBookmarkDate { get; set; }
+
+
+ [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; }
+
+
+ [DataMember(Name="ai_api_key")]
+ public string? AiApiKey { get; set; }
+
+
+ [DataMember(Name="ai_base_url")]
+ public string? AiBaseUrl { get; set; }
+
+
+ [DataMember(Name="ai_model_name")]
+ public string? AiModelName { get; set; }
+
+ public required string PartitionKey { get ; set; }
+ public required string RowKey { get ; set; }
+ public DateTimeOffset? Timestamp { get; set; }
+ public ETag ETag { get; set; }
+}