diff --git a/.aspire/settings.json b/.aspire/settings.json new file mode 100644 index 0000000..2ece20b --- /dev/null +++ b/.aspire/settings.json @@ -0,0 +1,3 @@ +{ + "appHostPath": "../src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 906e9dd..7684431 100644 --- a/.gitignore +++ b/.gitignore @@ -493,3 +493,7 @@ NoteBookmark.BlazorApp/appsettings.Development.json .azure NoteBookmark.AppHost/appsettings.Development.json + +src/NoteBookmark.AppHost/appsettings.[Dd]evelopment.json + +src/NoteBookmark.AppHost/appsettings.json diff --git a/NoteBookmark.Api.Tests/Endpoints/SummaryEndpointsTests.cs b/NoteBookmark.Api.Tests/Endpoints/SummaryEndpointsTests.cs deleted file mode 100644 index e69de29..0000000 diff --git a/src/NoteBookmark.AIServices/Choice.cs b/src/NoteBookmark.AIServices/Choice.cs deleted file mode 100644 index a6e4451..0000000 --- a/src/NoteBookmark.AIServices/Choice.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class Choice -{ - [JsonPropertyName("finish_reason")] - public string? FinishReason { get; set; } - - [JsonPropertyName("index")] - public int Index { get; set; } - - [JsonPropertyName("logprobs")] - public object? Logprobs { get; set; } - - [JsonPropertyName("message")] - public Message? Message { get; set; } -} \ No newline at end of file diff --git a/src/NoteBookmark.AIServices/ContentItem.cs b/src/NoteBookmark.AIServices/ContentItem.cs deleted file mode 100644 index 613dad9..0000000 --- a/src/NoteBookmark.AIServices/ContentItem.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class ContentItem -{ - [JsonPropertyName("type")] - public string? Type { get; set; } - - [JsonPropertyName("text")] - public string? Text { get; set; } -} \ No newline at end of file diff --git a/src/NoteBookmark.AIServices/Message.cs b/src/NoteBookmark.AIServices/Message.cs deleted file mode 100644 index 23f6616..0000000 --- a/src/NoteBookmark.AIServices/Message.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class Message -{ - [JsonPropertyName("content")] - public string? Content { get; set; } - - [JsonPropertyName("refusal")] - public object? Refusal { get; set; } - - [JsonPropertyName("role")] - public string? Role { get; set; } - - [JsonPropertyName("annotations")] - public object? Annotations { get; set; } - - [JsonPropertyName("audio")] - public object? Audio { get; set; } - - [JsonPropertyName("function_call")] - public object? FunctionCall { get; set; } - - [JsonPropertyName("tool_calls")] - public object? ToolCalls { get; set; } - - [JsonPropertyName("reasoning_content")] - public string? ReasoningContent { get; set; } - - [JsonPropertyName("reasoning_steps")] - public List? ReasoningSteps { get; set; } -} \ No newline at end of file diff --git a/src/NoteBookmark.AIServices/NoteBookmark.AIServices.csproj b/src/NoteBookmark.AIServices/NoteBookmark.AIServices.csproj index fd72d67..a9bdbe7 100644 --- a/src/NoteBookmark.AIServices/NoteBookmark.AIServices.csproj +++ b/src/NoteBookmark.AIServices/NoteBookmark.AIServices.csproj @@ -9,6 +9,12 @@ + + + + + + diff --git a/src/NoteBookmark.AIServices/ReasoningStep.cs b/src/NoteBookmark.AIServices/ReasoningStep.cs deleted file mode 100644 index 80cdf76..0000000 --- a/src/NoteBookmark.AIServices/ReasoningStep.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class ReasoningStep -{ - [JsonPropertyName("role")] - public string? Role { get; set; } - - [JsonPropertyName("content")] - public object? Content { get; set; } - - [JsonPropertyName("reasoning_content")] - public string? ReasoningContent { get; set; } - - [JsonPropertyName("tool_calls")] - public List? ToolCalls { get; set; } - - [JsonPropertyName("tool_call_id")] - public string? ToolCallId { get; set; } -} \ No newline at end of file diff --git a/src/NoteBookmark.AIServices/RekaChatResponse.cs b/src/NoteBookmark.AIServices/RekaChatResponse.cs deleted file mode 100644 index 9e2f020..0000000 --- a/src/NoteBookmark.AIServices/RekaChatResponse.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class RekaChatResponse -{ - [JsonPropertyName("id")] - public string? Id { get; set; } - - [JsonPropertyName("model")] - public string? Model { get; set; } - - [JsonPropertyName("usage")] - public RekaUsage? Usage { get; set; } - - [JsonPropertyName("responses")] - public List? Responses { get; set; } -} \ No newline at end of file diff --git a/src/NoteBookmark.AIServices/RekaMessage.cs b/src/NoteBookmark.AIServices/RekaMessage.cs deleted file mode 100644 index 596537c..0000000 --- a/src/NoteBookmark.AIServices/RekaMessage.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class RekaMessage -{ - [JsonPropertyName("role")] - public string? Role { get; set; } - - [JsonPropertyName("content")] - public List? Content { get; set; } - - [JsonPropertyName("in_reasoning")] - public bool InReasoning { get; set; } -} \ No newline at end of file diff --git a/src/NoteBookmark.AIServices/RekaResponse.cs b/src/NoteBookmark.AIServices/RekaResponse.cs deleted file mode 100644 index ffcbecf..0000000 --- a/src/NoteBookmark.AIServices/RekaResponse.cs +++ /dev/null @@ -1,31 +0,0 @@ - -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class RekaResponse -{ - [JsonPropertyName("id")] - public string? Id { get; set; } - - [JsonPropertyName("choices")] - public List? Choices { get; set; } - - [JsonPropertyName("created")] - public long Created { get; set; } - - [JsonPropertyName("model")] - public string? Model { get; set; } - - [JsonPropertyName("object")] - public string? Object { get; set; } - - [JsonPropertyName("service_tier")] - public string? ServiceTier { get; set; } - - [JsonPropertyName("system_fingerprint")] - public string? SystemFingerprint { get; set; } - - [JsonPropertyName("usage")] - public Usage? Usage { get; set; } -} \ No newline at end of file diff --git a/src/NoteBookmark.AIServices/RekaUsage.cs b/src/NoteBookmark.AIServices/RekaUsage.cs deleted file mode 100644 index 558311d..0000000 --- a/src/NoteBookmark.AIServices/RekaUsage.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class RekaUsage -{ - [JsonPropertyName("input_tokens")] - public int InputTokens { get; set; } - - [JsonPropertyName("output_tokens")] - public int OutputTokens { get; set; } - - [JsonPropertyName("reasoning_tokens")] - public int ReasoningTokens { get; set; } -} \ No newline at end of file diff --git a/src/NoteBookmark.AIServices/ResearchService.cs b/src/NoteBookmark.AIServices/ResearchService.cs new file mode 100644 index 0000000..32a73a6 --- /dev/null +++ b/src/NoteBookmark.AIServices/ResearchService.cs @@ -0,0 +1,145 @@ +using System.Text; +using System.Text.Json; +using System.IO; +using System.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Reka.SDK; +using NoteBookmark.Domain; + +namespace NoteBookmark.AIServices; + +public class ResearchService(HttpClient client, ILogger logger, IConfiguration config) +{ + private readonly HttpClient _client = client; + private readonly ILogger _logger = logger; + private const string BASE_URL = "https://api.reka.ai/v1/chat/completions"; + 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) + { + 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 + }; + + if (allowedDomains != null && allowedDomains.Length > 0) + { + webSearch["allowed_domains"] = allowedDomains; + } + else if (blockedDomains != null && blockedDomains.Length > 0) + { + webSearch["blocked_domains"] = blockedDomains; + } + + var requestPayload = new + { + model = MODEL_NAME, + + messages = new[] + { + new + { + role = "user", + content = query + } + }, + response_format = GetResponseFormat(), + research = new + { + web_search = webSearch + }, + }; + + var jsonPayload = JsonSerializer.Serialize(requestPayload, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + // await SaveToFile("research_request", jsonPayload); + + HttpResponseMessage? response = null; + + try + { + using var request = new HttpRequestMessage(HttpMethod.Post, BASE_URL); + request.Headers.Add("Authorization", $"Bearer {_apiKey}"); + request.Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); + + response = await _client.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + + await SaveToFile("research_response", responseContent); + + var rekaResponse = JsonSerializer.Deserialize(responseContent); + + if (response.IsSuccessStatusCode) + { + suggestions = JsonSerializer.Deserialize(rekaResponse!.Choices![0].Message!.Content!)!; + } + else + { + throw new Exception($"Request failed with status code: {response.StatusCode}. Response: {responseContent}"); + } + } + catch (Exception ex) + { + _logger.LogError($"An error occurred while fetching research suggestions: {ex.Message}"); + } + + return suggestions; + } + + + private object GetResponseFormat() + { + return new + { + type = "json_schema", + json_schema = new + { + name = "post_suggestions", + schema = new + { + type = "object", + properties = new + { + suggestions = new + { + type = "array", + items = new + { + type = "object", + properties = new + { + title = new { type = "string" }, + author = new { type = "string" }, + summary = new { type = "string", maxLength = 100 }, + publication_date = new { type = "string", format = "date" }, + url = new { type = "string" } + }, + required = new[] { "title", "summary", "url" } + } + } + }, + required = new[] { "post_suggestions" } + } + } + }; + } + + private async Task SaveToFile(string prefix, string responseContent) + { + string datetime = DateTime.Now.ToString("yyyy-MM-dd_HH-mm"); + string fileName = $"{prefix}_{datetime}.json"; + string folderPath = "Data"; + Directory.CreateDirectory(folderPath); + string filePath = Path.Combine(folderPath, fileName); + await File.WriteAllTextAsync(filePath, responseContent); + } + +} \ No newline at end of file diff --git a/src/NoteBookmark.AIServices/ResponseItem.cs b/src/NoteBookmark.AIServices/ResponseItem.cs deleted file mode 100644 index 5780d97..0000000 --- a/src/NoteBookmark.AIServices/ResponseItem.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class ResponseItem -{ - [JsonPropertyName("finish_reason")] - public string? FinishReason { get; set; } - - [JsonPropertyName("message")] - public RekaMessage? Message { get; set; } -} \ No newline at end of file diff --git a/src/NoteBookmark.AIServices/SummaryService.cs b/src/NoteBookmark.AIServices/SummaryService.cs index e04b95c..363d211 100644 --- a/src/NoteBookmark.AIServices/SummaryService.cs +++ b/src/NoteBookmark.AIServices/SummaryService.cs @@ -4,6 +4,7 @@ using System.Linq; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Reka.SDK; namespace NoteBookmark.AIServices; diff --git a/src/NoteBookmark.AIServices/ToolCall.cs b/src/NoteBookmark.AIServices/ToolCall.cs deleted file mode 100644 index 38f0d12..0000000 --- a/src/NoteBookmark.AIServices/ToolCall.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class ToolCall -{ - [JsonPropertyName("id")] - public string? Id { get; set; } - - [JsonPropertyName("name")] - public string? Name { get; set; } - - [JsonPropertyName("args")] - public object? Args { get; set; } -} \ No newline at end of file diff --git a/src/NoteBookmark.AIServices/Usage.cs b/src/NoteBookmark.AIServices/Usage.cs deleted file mode 100644 index 1f49ca4..0000000 --- a/src/NoteBookmark.AIServices/Usage.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NoteBookmark.AIServices; - -public class Usage -{ - [JsonPropertyName("completion_tokens")] - public int CompletionTokens { get; set; } - - [JsonPropertyName("prompt_tokens")] - public int PromptTokens { get; set; } - - [JsonPropertyName("total_tokens")] - public int TotalTokens { get; set; } - - [JsonPropertyName("completion_tokens_details")] - public object? CompletionTokensDetails { get; set; } - - [JsonPropertyName("prompt_tokens_details")] - public object? PromptTokensDetails { get; set; } -} \ No newline at end of file diff --git a/src/NoteBookmark.Api.Tests/Endpoints/SummaryEndpointsTests.cs b/src/NoteBookmark.Api.Tests/Endpoints/SummaryEndpointsTests.cs index 1f4a4db..e69de29 100644 --- a/src/NoteBookmark.Api.Tests/Endpoints/SummaryEndpointsTests.cs +++ b/src/NoteBookmark.Api.Tests/Endpoints/SummaryEndpointsTests.cs @@ -1,226 +0,0 @@ -using FluentAssertions; -using NoteBookmark.Api.Tests.Fixtures; -using NoteBookmark.Domain; -using System.Net; -using System.Net.Http.Json; -using System.Text; -using Xunit; - -namespace NoteBookmark.Api.Tests.Endpoints; - -public class SummaryEndpointsTests : IClassFixture -{ - private readonly NoteBookmarkApiTestFactory _factory; - private readonly HttpClient _client; - - public SummaryEndpointsTests(NoteBookmarkApiTestFactory factory) - { - _factory = factory; - _client = _factory.CreateClient(); - } - - [Fact] - public async Task GetSummaries_ReturnsAllSummaries() - { - // Arrange - await SeedTestSummaries(); - - // Act - var response = await _client.GetAsync("/api/summary/"); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.OK); - - var summaries = await response.Content.ReadFromJsonAsync>(); - summaries.Should().NotBeNull(); - summaries.Should().NotBeEmpty(); - } - - [Fact] - public async Task SaveSummary_WithValidSummary_ReturnsCreated() - { - // Arrange - var testSummary = CreateTestSummary(); - - // Act - var response = await _client.PostAsJsonAsync("/api/summary/summary", testSummary); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.Created); - var createdSummary = await response.Content.ReadFromJsonAsync(); - createdSummary.Should().NotBeNull(); - createdSummary!.RowKey.Should().Be(testSummary.RowKey); - createdSummary.Title.Should().Be(testSummary.Title); - } - - [Fact] - public async Task SaveSummary_WithInvalidSummary_ReturnsBadRequest() - { - // Arrange - var invalidSummary = new Summary - { - PartitionKey = "summaries", - RowKey = "invalid-summary" - }; // Missing required fields - - // Act - var response = await _client.PostAsJsonAsync("/api/summary/summary", invalidSummary); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - } - - [Fact] - public async Task GetReadingNotes_WhenExists_ReturnsOkWithReadingNotes() - { - // Arrange - var readingNotes = CreateTestReadingNotes(); - await SeedReadingNotes(readingNotes); - - // Act - var response = await _client.GetAsync($"/api/summary/{readingNotes.Number}"); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.OK); - - var retrievedReadingNotes = await response.Content.ReadFromJsonAsync(); - retrievedReadingNotes.Should().NotBeNull(); - retrievedReadingNotes!.Number.Should().Be(readingNotes.Number); - retrievedReadingNotes.Title.Should().Be(readingNotes.Title); - } - - [Fact] - public async Task GetReadingNotes_WhenDoesNotExist_ReturnsNotFound() - { - // Arrange - var nonExistentNumber = "999"; - - // Act - var response = await _client.GetAsync($"/api/summary/{nonExistentNumber}"); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.NotFound); - } - - [Fact] - public async Task SaveReadingNotesMarkdown_WithValidMarkdown_ReturnsOkWithUrl() - { - // Arrange - var number = "456"; - var markdown = "# Test Reading Notes\n\nThis is test markdown content.\n\n## Summary\n\nTest summary here."; - var content = new StringContent(markdown, Encoding.UTF8, "text/plain"); - - // Act - var response = await _client.PostAsync($"/api/summary/{number}/markdown", content); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.OK); - - var resultUrl = await response.Content.ReadAsStringAsync(); - resultUrl.Should().NotBeNullOrEmpty(); - resultUrl.Should().Contain($"readingnotes-{number}.md"); - } - - [Fact] - public async Task SaveReadingNotesMarkdown_WithEmptyMarkdown_ReturnsBadRequest() - { - // Arrange - var number = "789"; - var content = new StringContent("", Encoding.UTF8, "text/plain"); - - // Act - var response = await _client.PostAsync($"/api/summary/{number}/markdown", content); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - } - - [Fact] - public async Task SaveSummary_ThenGetSummaries_IncludesNewSummary() - { - // Arrange - var testSummary = CreateTestSummary(); - - // Act - Save summary - var saveResponse = await _client.PostAsJsonAsync("/api/summary/summary", testSummary); - saveResponse.EnsureSuccessStatusCode(); - - // Act - Get all summaries - var getResponse = await _client.GetAsync("/api/summary/"); - - // Assert - getResponse.StatusCode.Should().Be(HttpStatusCode.OK); - - var summaries = await getResponse.Content.ReadFromJsonAsync>(); - summaries.Should().NotBeNull(); - summaries.Should().Contain(s => s.RowKey == testSummary.RowKey); - } - - [Fact] - public async Task SaveSummary_ExistingSummary_UpdatesSuccessfully() - { - // Arrange - var testSummary = CreateTestSummary(); - await _client.PostAsJsonAsync("/api/summary/summary", testSummary); - // Modify the summary - testSummary.Title = "Updated summary content"; - - // Act - var response = await _client.PostAsJsonAsync("/api/summary/summary", testSummary); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.Created); - - // Verify the summary was updated - var summaries = await GetAllSummaries(); - var updatedSummary = summaries.FirstOrDefault(s => s.RowKey == testSummary.RowKey); - updatedSummary.Should().NotBeNull(); - updatedSummary!.Title.Should().Be("Updated summary content"); - } - - // Helper methods - private async Task SeedTestSummaries() - { var summary1 = CreateTestSummary(); - summary1.RowKey = "summary-1"; - summary1.Id = "100"; - - var summary2 = CreateTestSummary(); - summary2.RowKey = "summary-2"; - summary2.Id = "101"; - - await _client.PostAsJsonAsync("/api/summary/summary", summary1); - await _client.PostAsJsonAsync("/api/summary/summary", summary2); - } - - private async Task SeedReadingNotes(ReadingNotes readingNotes) - { - // Save reading notes via the notes endpoint - await _client.PostAsJsonAsync("/api/notes/SaveReadingNotes", readingNotes); - } - - private async Task> GetAllSummaries() - { - var response = await _client.GetAsync("/api/summary/"); - response.EnsureSuccessStatusCode(); - var summaries = await response.Content.ReadFromJsonAsync>(); - return summaries ?? new List(); - } private static Summary CreateTestSummary() - { - return new Summary - { - PartitionKey = "summaries", - RowKey = Guid.NewGuid().ToString(), - Id = "123", - Title = "Test summary content for reading notes 123.", - FileName = "readingnotes-123.json" - }; } - - private static ReadingNotes CreateTestReadingNotes() - { - return new ReadingNotes("456") - { - Title = "Test Reading Notes for Summary", - Intro = "Test description for summary endpoint testing" - }; - } -} diff --git a/src/NoteBookmark.Api.Tests/NoteBookmark.Api.Tests.csproj b/src/NoteBookmark.Api.Tests/NoteBookmark.Api.Tests.csproj index 9b25b0a..a69d6ea 100644 --- a/src/NoteBookmark.Api.Tests/NoteBookmark.Api.Tests.csproj +++ b/src/NoteBookmark.Api.Tests/NoteBookmark.Api.Tests.csproj @@ -13,7 +13,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -25,7 +25,7 @@ - + diff --git a/src/NoteBookmark.Api/NoteBookmark.Api.csproj b/src/NoteBookmark.Api/NoteBookmark.Api.csproj index af0ee3f..be07b08 100644 --- a/src/NoteBookmark.Api/NoteBookmark.Api.csproj +++ b/src/NoteBookmark.Api/NoteBookmark.Api.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj b/src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj index 4412ddf..1f81dbe 100644 --- a/src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj +++ b/src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj @@ -1,5 +1,5 @@ - + Exe net9.0 @@ -9,9 +9,9 @@ 0784f0a9-b1e6-4e65-8d31-00f1369f6d75 - - - + + + diff --git a/src/NoteBookmark.BlazorApp/Components/Layout/NavMenu.razor b/src/NoteBookmark.BlazorApp/Components/Layout/NavMenu.razor index 3fa220f..fa00510 100644 --- a/src/NoteBookmark.BlazorApp/Components/Layout/NavMenu.razor +++ b/src/NoteBookmark.BlazorApp/Components/Layout/NavMenu.razor @@ -9,6 +9,7 @@ Generate Summary Summaries Posts + Search diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/Posts.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Posts.razor index 7ad04c7..e43e68f 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/Posts.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Posts.razor @@ -50,7 +50,7 @@ Sortable="true" SortBy="@defSort" IsDefaultSortColumn="true" Width="125px"/> - + diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor new file mode 100644 index 0000000..44debff --- /dev/null +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor @@ -0,0 +1,119 @@ +@page "/search" +@using NoteBookmark.AIServices +@using NoteBookmark.BlazorApp.Components.Shared +@using NoteBookmark.Domain +@using Microsoft.FluentUI.AspNetCore.Components +@inject PostNoteClient client +@* @inject IJSRuntime jsRuntime *@ +@inject IToastService toastService +@* @inject IDialogService DialogService *@ +@inject ResearchService aiService +@inject NavigationManager Navigation +@rendermode InteractiveServer + +Search + +

Search

+ + + + + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + @(isSearching ? "Searching..." : "Search") + +
+ +
+
+ +
+
+ @* + Read Only + UnRead Only + *@ + +
+ + + + +@code { + private List? suggestions; + private GridSort defSort = GridSort.ByDescending(c => c.PublicationDate); + private string newPostUrl = string.Empty; + private bool showRead = false; + private bool isSearching = false; + + private SearchCriterias _criterias = new SearchCriterias(); + + + protected override async Task OnInitializedAsync() + { + @* await LoadPosts(); *@ + } + + private async Task FetchSuggestions() + { + isSearching = true; + if (string.IsNullOrWhiteSpace(_criterias.SearchPrompt)) + { + 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); + suggestions = result.Suggestions ?? []; + StateHasChanged(); + } + catch(Exception ex) + { + toastService.ShowError($"Oops! Error: {ex.Message}"); + } + finally + { + isSearching = false; + } + } + + @* private async Task OpenUrlInNewWindow(string? url) + { + await jsRuntime.InvokeVoidAsync("open", url, "_blank"); + } *@ + + + + 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/Shared/SuggestionList.razor b/src/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor new file mode 100644 index 0000000..e07a60d --- /dev/null +++ b/src/NoteBookmark.BlazorApp/Components/Shared/SuggestionList.razor @@ -0,0 +1,109 @@ +@using NoteBookmark.Domain +@using Microsoft.FluentUI.AspNetCore.Components +@inject IToastService toastService +@inject IDialogService DialogService + +@inject PostNoteClient client + + +

Suggestions

+ + + + + + + + + + + + + + + +   Nothing to see here. Carry on! + + + + +@code { + + + [Parameter] + public List? Suggestions { get; set; } + + private PaginationState pagination = new PaginationState { ItemsPerPage = 20 }; + private string titleFilter = string.Empty; + + IQueryable? filteredUrlList => Suggestions? + .Where(x => x.Title!.Contains(titleFilter, StringComparison.CurrentCultureIgnoreCase)) + .AsQueryable(); + + + private GridSort defSort = GridSort.ByDescending(c => c.PublicationDate); + private string newPostUrl = string.Empty; + private bool showRead = false; + + + + private async Task AddSuggestion(string postURL) + { + if (postURL != null) + { + var result = await client.ExtractPostDetailsAndSave(postURL); + if (result != null) + { + Suggestions!.Remove(Suggestions.First(x => x.Url == postURL)); + StateHasChanged(); + toastService.ShowSuccess("Suggestion added as note successfully!"); + } + else + { + toastService.ShowError("Failed to add suggestion as note. Please try again."); + } + } + else + { + toastService.ShowError("Suggestion not found. Please try again."); + } + } + + private async Task DeleteSuggestion(string postURL) + { + var sug = Suggestions?.FirstOrDefault(x => x.Url == postURL); + if (sug != null) + { + Suggestions!.Remove(sug); + StateHasChanged(); + toastService.ShowSuccess("Suggestion deleted successfully!"); + } + else + { + toastService.ShowError("Failed to delete suggestion. Please try again."); + } + } + + + private void HandleTitleFilter(ChangeEventArgs args) + { + if (args.Value is string value) + { + titleFilter = value; + } + } + + private void HandleClearTitleFilter() + { + if (string.IsNullOrWhiteSpace(titleFilter)) + { + titleFilter = string.Empty; + } + } +} diff --git a/src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj b/src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj index 27fb224..be4bd25 100644 --- a/src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj +++ b/src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj @@ -1,20 +1,20 @@ - - - - net9.0 - enable - enable - - - - - - - - - - - - - - + + + + net9.0 + enable + enable + + + + + + + + + + + + + + diff --git a/src/NoteBookmark.BlazorApp/Program.cs b/src/NoteBookmark.BlazorApp/Program.cs index ee2df68..896b180 100644 --- a/src/NoteBookmark.BlazorApp/Program.cs +++ b/src/NoteBookmark.BlazorApp/Program.cs @@ -1,44 +1,69 @@ -using Microsoft.FluentUI.AspNetCore.Components; -using NoteBookmark.AIServices; -using NoteBookmark.BlazorApp; -using NoteBookmark.BlazorApp.Components; - -var builder = WebApplication.CreateBuilder(args); - -builder.AddServiceDefaults(); - -builder.Services.AddHttpClient(client => - { - client.BaseAddress = new Uri("https+http://api"); - }); - -builder.Services.AddHttpClient(client => -{ - client.Timeout = TimeSpan.FromSeconds(300); // Set to 5 minutes, adjust as needed -}); - -// Add services to the container. -builder.Services.AddRazorComponents() - .AddInteractiveServerComponents(); -builder.Services.AddFluentUIComponents(); - -var app = builder.Build(); -app.MapDefaultEndpoints(); - -// Configure the HTTP request pipeline. -if (!app.Environment.IsDevelopment()) -{ - app.UseExceptionHandler("/Error", createScopeForErrors: true); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); -} - -app.UseHttpsRedirection(); - -app.UseStaticFiles(); -app.UseAntiforgery(); - -app.MapRazorComponents() - .AddInteractiveServerRenderMode(); - -app.Run(); +using Microsoft.FluentUI.AspNetCore.Components; +using NoteBookmark.AIServices; +using NoteBookmark.BlazorApp; +using NoteBookmark.BlazorApp.Components; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +// 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); +// }); + +builder.Services.AddHttpClient(client => + { + client.BaseAddress = new Uri("https+http://api"); + }); + +builder.Services.AddHttpClient(client => +{ + client.Timeout = TimeSpan.FromMinutes(5); +}); + + +builder.Services.AddHttpClient(); + // .AddStandardResilienceHandler(); + + +// Add services to the container. +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); +builder.Services.AddFluentUIComponents(); + +var app = builder.Build(); +app.MapDefaultEndpoints(); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); + +app.UseStaticFiles(); +app.UseAntiforgery(); + +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +app.Run(); diff --git a/src/NoteBookmark.Domain/PostSuggestion.cs b/src/NoteBookmark.Domain/PostSuggestion.cs new file mode 100644 index 0000000..f6fe06a --- /dev/null +++ b/src/NoteBookmark.Domain/PostSuggestion.cs @@ -0,0 +1,55 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace NoteBookmark.Domain; + +public class PostSuggestion +{ + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; + + [JsonPropertyName("author")] + public string? Author { get; set; } + + [JsonPropertyName("summary")] + public string Summary { get; set; } = string.Empty; + + [JsonPropertyName("publication_date")] + [JsonConverter(typeof(DateOnlyJsonConverter))] + public string? PublicationDate { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } = string.Empty; +} + +public class DateOnlyJsonConverter : JsonConverter +{ + private const string DateFormat = "yyyy-MM-dd"; + + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + + var dateString = reader.GetString(); + if (string.IsNullOrEmpty(dateString)) + return null; + + if (DateTime.TryParse(dateString, out var date)) + { + return date.ToString(DateFormat); + } + return dateString; + } + + public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStringValue(value); + } +} diff --git a/src/NoteBookmark.Domain/PostSuggestions.cs b/src/NoteBookmark.Domain/PostSuggestions.cs new file mode 100644 index 0000000..4a71deb --- /dev/null +++ b/src/NoteBookmark.Domain/PostSuggestions.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace NoteBookmark.Domain; + +public class PostSuggestions +{ + [JsonPropertyName("suggestions")] + public List? Suggestions { get; set; } +} diff --git a/src/NoteBookmark.Domain/Suggestion.cs b/src/NoteBookmark.Domain/Suggestion.cs new file mode 100644 index 0000000..4115656 --- /dev/null +++ b/src/NoteBookmark.Domain/Suggestion.cs @@ -0,0 +1,29 @@ +using System; +using System.Runtime.Serialization; + +namespace NoteBookmark.Domain; + +public class Suggestion +{ + [DataMember(Name = "id")] + public string? Id { get; set; } + + public string? Title { get; set; } + + public string? Url { get; set; } + + public string? Overview { get; set; } + + public string? SuggestedDate { get; set; } + + + + public required string PartitionKey { get; set; } + + public required string RowKey { get; set; } + + public DateTimeOffset? Timestamp { get; set; } + + public Azure.ETag ETag { get; set; } + +} diff --git a/src/NoteBookmark.ServiceDefaults/Extensions.cs b/src/NoteBookmark.ServiceDefaults/Extensions.cs index 2a3f4e0..777e143 100644 --- a/src/NoteBookmark.ServiceDefaults/Extensions.cs +++ b/src/NoteBookmark.ServiceDefaults/Extensions.cs @@ -26,7 +26,13 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu builder.Services.ConfigureHttpClientDefaults(http => { // Turn on resilience by default - http.AddStandardResilienceHandler(); + http.AddStandardResilienceHandler(options => + { + TimeSpan timeSpan = TimeSpan.FromMinutes(5); + options.AttemptTimeout.Timeout = timeSpan; + options.CircuitBreaker.SamplingDuration = timeSpan * 2; + options.TotalRequestTimeout.Timeout = timeSpan * 3; + }); // Turn on service discovery by default http.AddServiceDiscovery(); diff --git a/src/NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj b/src/NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj index 145a4ab..233173d 100644 --- a/src/NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj +++ b/src/NoteBookmark.ServiceDefaults/NoteBookmark.ServiceDefaults.csproj @@ -11,12 +11,12 @@ - + - - - + + +