content management rewrite, groups, chunking, multidimensional collections

This commit is contained in:
Adam 2025-06-28 23:51:37 +02:00
parent 492fa9209b
commit de70b8c97f
163 changed files with 10248 additions and 78913 deletions

View File

@ -7,6 +7,13 @@
<AssemblyName>BLAIzor</AssemblyName> <AssemblyName>BLAIzor</AssemblyName>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Remove="SeemGen.Tests\**" />
<Content Remove="SeemGen.Tests\**" />
<EmbeddedResource Remove="SeemGen.Tests\**" />
<None Remove="SeemGen.Tests\**" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="Components\Pages\Home.razorOLD" /> <None Remove="Components\Pages\Home.razorOLD" />
</ItemGroup> </ItemGroup>
@ -30,9 +37,11 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="PdfPig" Version="0.1.10" /> <PackageReference Include="PdfPig" Version="0.1.10" />
<PackageReference Include="Qdrant.Client" Version="1.13.0" /> <PackageReference Include="Qdrant.Client" Version="1.13.0" />
<PackageReference Include="Radzen.Blazor" Version="6.4.0" /> <PackageReference Include="Radzen.Blazor" Version="7.1.1" />
<PackageReference Include="Sidio.Sitemap.Blazor" Version="1.1.2" /> <PackageReference Include="Sidio.Sitemap.Blazor" Version="1.1.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.3" /> <PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.3" />
<PackageReference Include="xunit.extensibility.core" Version="2.9.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -57,6 +66,9 @@
<Content Update="wwwroot\bootstrap\js\bootstrap.js"> <Content Update="wwwroot\bootstrap\js\bootstrap.js">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="wwwroot\admin.css">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\main.css"> <Content Update="wwwroot\main.css">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>

View File

@ -5,6 +5,8 @@ VisualStudioVersion = 17.10.35004.147
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BLAIzor", "BLAIzor.csproj", "{A7A021E4-C303-4DF0-B55B-CE3233137085}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BLAIzor", "BLAIzor.csproj", "{A7A021E4-C303-4DF0-B55B-CE3233137085}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeemGen.Tests", "SeemGen.Tests\SeemGen.Tests.csproj", "{9B3FF465-BD0D-F50F-F19B-E2D1338E7FE6}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -15,6 +17,10 @@ Global
{A7A021E4-C303-4DF0-B55B-CE3233137085}.Debug|Any CPU.Build.0 = Debug|Any CPU {A7A021E4-C303-4DF0-B55B-CE3233137085}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A7A021E4-C303-4DF0-B55B-CE3233137085}.Release|Any CPU.ActiveCfg = Release|Any CPU {A7A021E4-C303-4DF0-B55B-CE3233137085}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A7A021E4-C303-4DF0-B55B-CE3233137085}.Release|Any CPU.Build.0 = Release|Any CPU {A7A021E4-C303-4DF0-B55B-CE3233137085}.Release|Any CPU.Build.0 = Release|Any CPU
{9B3FF465-BD0D-F50F-F19B-E2D1338E7FE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9B3FF465-BD0D-F50F-F19B-E2D1338E7FE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9B3FF465-BD0D-F50F-F19B-E2D1338E7FE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9B3FF465-BD0D-F50F-F19B-E2D1338E7FE6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -12,6 +12,7 @@
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" /> <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" /> <link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="main.css" /> <link rel="stylesheet" href="main.css" />
<link rel="stylesheet" href="admin.css" />
<link rel="stylesheet" href="animate.css" /> <link rel="stylesheet" href="animate.css" />
<link rel="stylesheet" href="loader.css" /> <link rel="stylesheet" href="loader.css" />
<link rel="stylesheet" href="BLAIzor.styles.css" /> <link rel="stylesheet" href="BLAIzor.styles.css" />
@ -23,6 +24,7 @@
<script src="https://assets.calendly.com/assets/external/widget.js" type="text/javascript"></script> <script src="https://assets.calendly.com/assets/external/widget.js" type="text/javascript"></script>
<script type="text/javascript" src="scripts/background.js"> </script> <script type="text/javascript" src="scripts/background.js"> </script>
<script type="text/javascript" src="scripts/SeemGenCss.js"> </script> <script type="text/javascript" src="scripts/SeemGenCss.js"> </script>
@* <script> @* <script>
window.applyDynamicCss = (cssContent) => { window.applyDynamicCss = (cssContent) => {
let styleTag = document.getElementById('seemgen-style'); let styleTag = document.getElementById('seemgen-style');
@ -49,6 +51,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"
integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy" integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="_content/Radzen.Blazor/Radzen.Blazor.js?v=@(typeof(Radzen.Colors).Assembly.GetName().Version)"></script> <script type="text/javascript" src="scripts/finisher-header.es5.min.js"> </script>
<script src="_content/Radzen.Blazor/Radzen.Blazor.js?v=@(typeof(Radzen.Colors).Assembly.GetName().Version)"></script>
</body> </body>
</html> </html>

View File

@ -1,6 +1,6 @@
@using BLAIzor.Services @using BLAIzor.Services
@inherits LayoutComponentBase @inherits LayoutComponentBase
<RadzenComponents @rendermode="InteractiveServer" />
@Body @Body
<div id="blazor-error-ui"> <div id="blazor-error-ui">

View File

@ -8,7 +8,7 @@
@using BLAIzor.Components.Layout @using BLAIzor.Components.Layout
@attribute [Authorize] @attribute [Authorize]
@layout AdminLayout @layout AdminLayout
@inject ContentEditorService ContentEditorService @inject ContentEditorAIService ContentEditorAIService
<h3>Create a new from from pdf or word</h3> <h3>Create a new from from pdf or word</h3>
@ -68,7 +68,7 @@
private async Task<List<FormFieldGroup>> CallAiForFieldExtraction(string plainText) private async Task<List<FormFieldGroup>> CallAiForFieldExtraction(string plainText)
{ {
var jsonResult = await ContentEditorService.AnalyzeGroupedFormFieldsFromText(plainText); var jsonResult = await ContentEditorAIService.AnalyzeGroupedFormFieldsFromText(plainText);
Console.WriteLine($"📄 PDF form AI response: {jsonResult}"); Console.WriteLine($"📄 PDF form AI response: {jsonResult}");
try try

View File

@ -1,4 +1,4 @@
@page "/" @page "/"
@page "/menu/{topic?}" @page "/menu/{topic?}"
@inherits MainPageBase @inherits MainPageBase
@using BLAIzor.Models @using BLAIzor.Models
@ -21,44 +21,52 @@
<ErrorBoundary> <ErrorBoundary>
<ChildContent> <ChildContent>
<div class="page" style="z-index: 1"> <div class="page" style="z-index: 1">
<NavMenu MenuString="@Menu" OnMenuClicked=@MenuClick></NavMenu> <NewNavMenu Menu="@MenuItems" BrandName="@SelectedBrandName" OnMenuClicked=@MenuClick></NewNavMenu>
<main> <main>
<article class="content text-center" style="position: relative; z-index: 4;"> <article class="content text-center" style="position: relative; z-index: 4;">
<PageTitle>Home</PageTitle> <PageTitle>Home</PageTitle>
<VideoComponent SelectedBrandName="@SelectedBrandName" /> @{
@* <HeadContent> if(SiteInfo!= null)
{
if (!string.IsNullOrEmpty(SiteInfo.BackgroundVideo))
{
<VideoComponent site="SiteInfo" />
}
}
}
@* <HeadContent>
<style id="seemgen-style">@dynamicallyLoadedCss</style> <style id="seemgen-style">@dynamicallyLoadedCss</style>
</HeadContent> *@ </HeadContent> *@
<div id="maincontrol" > <div id="maincontrol">
@* <div class="hoverslide"> *@ @* <div class="hoverslide"> *@
<div class="displaysearch"> <div class="displaysearch">
<div class="searchBox"> <div class="searchBox">
<input @oninput="(e) => UserInput = e.Value.ToString()" <input @oninput="(e) => UserInput = e.Value.ToString()"
@onkeydown="@Enter" class="searchInput" type="text" name="" value="@UserInput" placeholder="Ask any question"> @onkeydown="@Enter" class="searchInput" type="text" name="" value="@UserInput" placeholder="Ask any question">
<button data-hint="ask anything" class="searchButton border-0" @onclick="SendUserQuery" href="#"> <button data-hint="ask anything" class="searchButton border-0" @onclick="SendUserQuery" href="#">
<i class="fa-solid fa-hexagon-nodes-bolt" style="font-size:20px"></i> <i class="fa-solid fa-hexagon-nodes-bolt" style="font-size:20px"></i>
</button> </button>
</div> </div>
@{ @{
@if(VoiceEnabled) @if (VoiceEnabled)
{ {
if(STTEnabled) if (STTEnabled)
{ {
<button id="recButton" class="btn btn-primary voicebutton" onclick="startRecording()"><i class="fa-solid fa-microphone"></i></button> <button id="recButton" class="btn btn-primary voicebutton" onclick="startRecording()"><i class="fa-solid fa-microphone"></i></button>
<button id="stopButton" class="btn btn-primary voicebutton" onclick="stopRecording()" hidden><i class="fa-solid fa-microphone-slash"></i></button> <button id="stopButton" class="btn btn-primary voicebutton" onclick="stopRecording()" hidden><i class="fa-solid fa-microphone-slash"></i></button>
} }
if(TTSEnabled) if (TTSEnabled)
{ {
if (!AiVoicePermitted) if (!AiVoicePermitted)
{ {
@ -89,10 +97,10 @@
@{ @{
if (!string.IsNullOrEmpty(HtmlContent.ToString())) if (!string.IsNullOrEmpty(HtmlContent.ToString()))
{ {
if (isEmailFormVisible) if (isEmailFormVisible)
{ {
<div class="conteiner-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="pt-5 @FirstColumnClass"> <div class="pt-5 @FirstColumnClass">
@((MarkupString)HtmlContent.ToString()) @((MarkupString)HtmlContent.ToString())
@ -101,7 +109,7 @@
<ContactFormComponent ContactFormModel="@ContactFormModel" DocumentEmailAddress="@DocumentEmailAddress" OnDataChanged="@ContentChangedInForm"></ContactFormComponent> <ContactFormComponent ContactFormModel="@ContactFormModel" DocumentEmailAddress="@DocumentEmailAddress" OnDataChanged="@ContentChangedInForm"></ContactFormComponent>
<button @onclick="CancelEmail" class="btn btn-primary">Cancel</button> <button @onclick="CancelEmail" class="btn btn-primary">Cancel</button>
</div> </div>
</div> </div>
</div> </div>
} }
@ -135,6 +143,7 @@
</article> </article>
</main> </main>
<FooterComponent MenuString="@Menu" OnMenuClicked=@MenuClick></FooterComponent>
</div> </div>
</ChildContent> </ChildContent>
<ErrorContent Context="ex"> <ErrorContent Context="ex">
@ -190,19 +199,6 @@
private string ChatGptResponse = string.Empty; private string ChatGptResponse = string.Empty;
private bool isRecording = false; private bool isRecording = false;
// private string FirstColumnClass = "";
// private bool isEmailFormVisible = false;
// private ContactFormModel ContactFormModel = new();
// // private string? SuccessMessage;
// // private string? ErrorMessage;
// private string? DocumentEmailAddress = "";
private string dynamicallyLoadedCss = string.Empty;
private string Menu;
private void AllowAIVoice() private void AllowAIVoice()
{ {
AiVoicePermitted = true; AiVoicePermitted = true;
@ -210,7 +206,7 @@
private void MuteAI() private void MuteAI()
{ {
AiVoicePermitted = false; AiVoicePermitted = false;
} }
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
@ -234,7 +230,7 @@
SelectedBrandName = "default"; SelectedBrandName = "default";
} }
Subdomain = HttpContextAccessor.HttpContext?.Items["Subdomain"]?.ToString(); Subdomain = HttpContextAccessor.HttpContext?.Items["Subdomain"]?.ToString();
SiteInfo = await _scopedContentService.GetSiteInfoByNameAsync(Subdomain); SiteInfo = await _contentEditorService.GetSiteInfoByNameAsync(Subdomain);
if (SiteInfo != null && SiteInfo.IsPublished) if (SiteInfo != null && SiteInfo.IsPublished)
{ {
@ -253,6 +249,8 @@
STTEnabled = false; STTEnabled = false;
} }
_scopedContentService.SelectedSiteId = SiteId; _scopedContentService.SelectedSiteId = SiteId;
ContentCollectionName = SiteInfo.VectorCollectionName ?? "default_content_collection";
Console.Write("------------------------"); Console.Write("------------------------");
// Load the CSS template for the selected brand from the database // Load the CSS template for the selected brand from the database
@ -273,16 +271,19 @@
Console.Write($"------------------------ {SiteInfo.MenuItems}, {SiteId}, {SiteInfo.TemplateId}, {SiteInfo.SiteName}"); Console.Write($"------------------------ {SiteInfo.MenuItems}, {SiteId}, {SiteInfo.TemplateId}, {SiteInfo.SiteName}");
Menu = await GetMenuList(SiteId); Menu = await GetMenuList(SiteId);
MenuItems = await GetMenuItems(SiteId);
if (string.IsNullOrEmpty(HtmlContent.ToString())) if (string.IsNullOrEmpty(HtmlContent.ToString()))
{ {
if(!string.IsNullOrWhiteSpace(topic)) if (!string.IsNullOrWhiteSpace(topic))
{ {
UserInput = topic; UserInput = topic;
await ChatGptService.ProcessContentRequest(SessionId, UserInput, SiteId, (int)SiteInfo.TemplateId!, TemplateCollectionName, Menu, true); await ChatGptService.ProcessContentRequest(SessionId, UserInput, SiteId, (int)SiteInfo.TemplateId!, ContentCollectionName, Menu, true);
} }
else else
{ {
await ChatGptService.GetChatGptWelcomeMessage(SessionId, SiteId, TemplateCollectionName, Menu); await ChatGptService.GetChatGptWelcomeMessage(SessionId, SiteId, TemplateCollectionName, Menu);
SiteModel = await ChatGptService.InitSite(SessionId, SiteId, TemplateCollectionName, Menu);
//await ChatGptService.ProcessContentRequest(SessionId, MenuItems.FirstOrDefault(), SiteId, (int)SiteInfo.TemplateId!, TemplateCollectionName, Menu, true);
} }
// HtmlContent = await ChatGptService.GetChatGptWelcomeMessage(); // HtmlContent = await ChatGptService.GetChatGptWelcomeMessage();
// UserInput = "Sumerize for me, what is this website about, and what can I do on this website?"; // UserInput = "Sumerize for me, what is this website about, and what can I do on this website?";
@ -340,7 +341,7 @@
VoiceEnabled = configuration?.GetSection("AiSettings")?.GetValue<bool>("VoiceActivated") ?? false; VoiceEnabled = configuration?.GetSection("AiSettings")?.GetValue<bool>("VoiceActivated") ?? false;
} }
private async void UpdateContent(string receivedSessionId, string content) private async void UpdateContent(string receivedSessionId, string content, MenuItem? menuItem)
{ {
if (receivedSessionId == SessionId) // Only accept messages meant for this tab if (receivedSessionId == SessionId) // Only accept messages meant for this tab
{ {

View File

@ -1,19 +1,17 @@
using BLAIzor.Models; using BLAIzor.Models;
using BLAIzor.Services; using BLAIzor.Services;
using Google.Api;
using Google.Cloud.Speech.V1; using Google.Cloud.Speech.V1;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using System.Collections; using Radzen;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using UglyToad.PdfPig.DocumentLayoutAnalysis;
namespace BLAIzor.Components.Pages namespace BLAIzor.Components.Pages
{ {
public class MainPageBase : ComponentBase public class MainPageBase : ComponentBase
{ {
public string SelectedBrandName = "default"; public string SelectedBrandName = "default";
[Inject] protected ScopedContentService _scopedContentService { get; set; } [Inject] protected ScopedContentService _scopedContentService { get; set; }
[Inject] protected ContentEditorService _contentEditorService { get; set; } [Inject] protected ContentEditorService _contentEditorService { get; set; }
@ -21,6 +19,7 @@ namespace BLAIzor.Components.Pages
[Inject] protected HttpClient Http { get; set; } [Inject] protected HttpClient Http { get; set; }
[Inject] protected IJSRuntime jsRuntime { get; set; } [Inject] protected IJSRuntime jsRuntime { get; set; }
[Inject] protected AIService ChatGptService { get; set; } [Inject] protected AIService ChatGptService { get; set; }
[Inject] NotificationService NotificationService { get; set; }
public static readonly Dictionary<string, MainPageBase> _instances = new(); public static readonly Dictionary<string, MainPageBase> _instances = new();
@ -34,6 +33,7 @@ namespace BLAIzor.Components.Pages
public string StatusContent = ""; public string StatusContent = "";
public string UserInput = string.Empty; public string UserInput = string.Empty;
public string TemplateCollectionName = "html_snippets"; public string TemplateCollectionName = "html_snippets";
public string ContentCollectionName = "";
public bool VoiceEnabled; public bool VoiceEnabled;
public bool TTSEnabled; public bool TTSEnabled;
@ -49,6 +49,13 @@ namespace BLAIzor.Components.Pages
// private string? ErrorMessage; // private string? ErrorMessage;
public string? DocumentEmailAddress = ""; public string? DocumentEmailAddress = "";
public string Menu;
public List<MenuItem> MenuItems = new();
public string dynamicallyLoadedCss = string.Empty;
public MenuItem currentMenuItem = new();
public WebsiteContentModel SiteModel = new();
public void DoSharedWork() public void DoSharedWork()
{ {
// Logic here // Logic here
@ -91,13 +98,13 @@ namespace BLAIzor.Components.Pages
public async Task<List<MenuItem>> GetMenuItems(int siteId) public async Task<List<MenuItem>> GetMenuItems(int siteId)
{ {
List<MenuItem> menuItems = (await _contentEditorService.GetMenuItemsBySiteIdAsync(siteId)).Where(m => m.ShowInMainMenu == true).OrderBy(m => m.SortOrder).ToList(); List<MenuItem> menuItems = (await _contentEditorService.GetMenuItemsBySiteIdWithChildrenAsync(siteId)).Where(m => m.ShowInMainMenu == true).OrderBy(m => m.SortOrder).ToList();
return menuItems; return menuItems;
} }
private string GetApiKey() => private string GetApiKey() =>
configuration?.GetSection("ElevenLabsAPI")?.GetValue<string>("ApiKey") ?? string.Empty; configuration?.GetSection("ElevenLabsAPI")?.GetValue<string>("ApiKey") ?? string.Empty;
public async Task ConvertTextToSpeech(string textContent) public async Task ConvertTextToSpeech(string textContent)
{ {
@ -211,14 +218,26 @@ namespace BLAIzor.Components.Pages
{ {
HtmlContent.Clear(); HtmlContent.Clear();
var menu = await GetMenuList(SiteId); var menu = await GetMenuList(SiteId);
var menuItem = (await GetMenuItems(SiteId)).Where(m => m.Name == input).FirstOrDefault(); var menuList = await GetMenuItems(SiteId);
var menuItem = CompareMenuItemNames(input, menuList);
if (menuItem == null) if (menuItem == null)
{ {
await ChatGptService.ProcessContentRequest(SessionId, input, SiteId, (int)SiteInfo.TemplateId!, TemplateCollectionName, menu, forceUnmodified); await ChatGptService.ProcessContentRequest(SessionId, input, SiteId, (int)SiteInfo.TemplateId!, ContentCollectionName, menu, forceUnmodified);
} }
else else
{ {
await ChatGptService.ProcessContentRequest(SessionId, menuItem, SiteId, (int)SiteInfo.TemplateId!, TemplateCollectionName, menu, forceUnmodified); currentMenuItem = menuItem;
if (!string.IsNullOrEmpty(menuItem.StoredHtml))
{
HtmlContent.Clear();
HtmlContent.Append(menuItem.StoredHtml);
}
else
{
await ChatGptService.ProcessContentRequest(SessionId, menuItem, SiteId, (int)SiteInfo.TemplateId!, ContentCollectionName, menu, forceUnmodified);
}
} }
UserInput = string.Empty; UserInput = string.Empty;
} }
@ -296,16 +315,40 @@ namespace BLAIzor.Components.Pages
Console.Write("openEmail with: " + emailAddress); Console.Write("openEmail with: " + emailAddress);
} }
public async Task SendMessage() public async Task SendMessage()
{ {
Console.WriteLine("Button clicked!"); Console.WriteLine("Button clicked!");
var menu = await GetMenuList(SiteId); var menu = await GetMenuList(SiteId);
HtmlContent.Clear(); HtmlContent.Clear();
await ChatGptService.ProcessUserIntent(SessionId, UserInput, SiteId, (int)SiteInfo.TemplateId!, TemplateCollectionName, menu); await ChatGptService.ProcessUserIntent(SessionId, UserInput, SiteId, (int)SiteInfo.TemplateId!, ContentCollectionName, menu);
UserInput = string.Empty; UserInput = string.Empty;
} }
public MenuItem CompareMenuItemNames(string input, List<MenuItem> menuList)
{
var parent = menuList.FirstOrDefault(ml => ml.Name == input);
if (parent != null)
{
return parent;
}
foreach (var item in menuList)
{
var child = item.Children.FirstOrDefault(ch => ch.Name == input);
if (child != null)
{
return child;
}
}
return null; // or throw, or return a default
}
public void ShowNotification(NotificationMessage message)
{
NotificationService.Notify(message);
}
} }
} }

View File

@ -0,0 +1,127 @@
@page "/site/{SiteInfoId:int}/content-groups"
@using BLAIzor.Components.Layout
@using BLAIzor.Models
@using BLAIzor.Services
@using Microsoft.AspNetCore.Components.Authorization
@layout AdminLayout
@inject ContentEditorService contentEditorService
@inject NavigationManager Navigation
@attribute [Authorize]
@if (isLoading)
{
<p>Loading content groups...</p>
}
else
{
@foreach (var group in groups)
{
if (selectedGroupId == group.Id)
{
<EditForm Model="@group" OnValidSubmit="() => SaveGroup(group)">
<div class="border rounded p-3 mb-3">
<label>
Name
<InputText class="form-control my-1" @bind-Value="group.Name" />
</label>
<label>
Type
<InputText class="form-control my-1" @bind-Value="group.Type" />
</label>
<div class="d-flex justify-content-between mt-2">
<button class="btn btn-success" type="submit">Save</button>
<button class="btn btn-danger" type="button" @onclick="() => DeleteGroup(group)">Delete</button>
<button class="btn btn-secondary" type="button" @onclick="() => DeselectGroup()">Cancel</button>
</div>
</div>
</EditForm>
}
else
{
<div class="border rounded p-3 mb-3 d-flex justify-content-between align-items-center">
<div>
<strong>@group.Name</strong><br />
<small class="text-muted">@group.Type</small>
</div>
<button class="btn btn-outline-primary" @onclick="() => SelectGroup(group.Id)">Edit</button>
</div>
}
}
<button class="btn btn-primary mt-3" @onclick="AddNewGroup">Add New Group</button>
}
@code {
[Parameter]
public int SiteInfoId { get; set; }
private List<ContentGroup> groups = new();
private bool isLoading = true;
private int? selectedGroupId = null;
private void SelectGroup(int groupId)
{
selectedGroupId = groupId;
}
private void DeselectGroup()
{
selectedGroupId = null;
}
protected override async Task OnInitializedAsync()
{
isLoading = true;
groups = (await contentEditorService.GetContentGroupsBySiteInfoIdAsync(SiteInfoId))
.Where(g => g.SiteInfoId == SiteInfoId)
.OrderByDescending(g => g.LastUpdated)
.ToList();
isLoading = false;
}
private void EditGroup(ContentGroup group)
{
Navigation.NavigateTo($"/content-group/{group.Id}/items");
}
private async Task SaveGroup(ContentGroup group)
{
group.LastUpdated = DateTime.UtcNow;
group.Version = group.Version + 1;
var result = await contentEditorService.UpdateContentGroupByIdAsync(group);
if(result != null) ;
}
private async Task DeleteGroup(ContentGroup group)
{
var result = await contentEditorService.DeleteContentGroupByIdAsync(group.Id); ;
groups.Remove(group);
}
private async Task AddNewGroup()
{
var newGroup = new ContentGroup
{
SiteInfoId = SiteInfoId,
Name = "New Group",
Slug = $"group-{DateTime.UtcNow.Ticks}",
Type = "manual",
VectorSize = 384,
EmbeddingModel = "default",
CreatedAt = DateTime.UtcNow,
LastUpdated = DateTime.UtcNow,
Version = 1
};
var result = await contentEditorService.CreateContentGroupAsync(newGroup);
if(result!=null)
{
groups.Insert(0, newGroup);
}
}
}

View File

@ -5,33 +5,35 @@
@using BLAIzor.Services @using BLAIzor.Services
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@layout AdminLayout @layout AdminLayout
@inject ScopedContentService SiteInfoService @inject ContentEditorService _contentEditorService
@inject AuthenticationStateProvider AuthenticationStateProvider @inject AuthenticationStateProvider AuthenticationStateProvider
@inject CustomAuthenticationStateProvider CustomAuthProvider @inject CustomAuthenticationStateProvider CustomAuthProvider
<h3>Site Information</h3>
<EditForm Model="@siteInfo" OnValidSubmit="SaveSiteInfo"> <EditForm Model="@siteInfo" OnValidSubmit="SaveSiteInfo">
<label> <label>
Brand name: Brand name
<InputText class="form-control my-3" id="siteName" @bind-Value="siteInfo.SiteName" /> <InputText class="form-control my-3" id="siteName" @bind-Value="siteInfo.SiteName" />
</label> </label>
<label> <label>
Site description: Site description
<InputText class="form-control my-3" id="siteDescription" @bind-Value="siteInfo.SiteDescription" /> <InputTextArea class="form-control my-3" style="height: 100px;" id="siteDescription" @bind-Value="siteInfo.SiteDescription" rows="3"/>
</label> </label>
<label> <label>
Logo url: Logo url
<InputText class="form-control my-3" id="brandLogoUrl" @bind-Value="siteInfo.BrandLogoUrl" /> <InputText class="form-control my-3" id="brandLogoUrl" @bind-Value="siteInfo.BrandLogoUrl" />
</label> </label>
<label> <label>
Default color Default color (Do not bother with this)
<InputText class="form-control my-3" id="defaultColor" @bind-Value="siteInfo.DefaultColor" /> <InputText class="form-control my-3" id="defaultColor" @bind-Value="siteInfo.DefaultColor" />
</label> </label>
<label> <label>
Entity The Entity behind this website (eg. "company", "professional sportsman", "worldwide brand", etc);
<InputText class="form-control my-3" id="defaultColor" @bind-Value="siteInfo.Entity" /> <InputText class="form-control my-3" id="defaultColor" @bind-Value="siteInfo.Entity" />
</label> </label>
<label>
Your agent Persona
<InputText class="form-control my-3" id="defaultColor" @bind-Value="siteInfo.Persona" />
</label>
<label> <label>
Your domain url Your domain url
<InputText class="form-control my-3" id="domainUrl" @bind-Value="siteInfo.DomainUrl" /> <InputText class="form-control my-3" id="domainUrl" @bind-Value="siteInfo.DomainUrl" />
@ -91,12 +93,12 @@ else
userId = CustomAuthProvider.GetUserId(); userId = CustomAuthProvider.GetUserId();
userName = CustomAuthProvider.GetUserName(); userName = CustomAuthProvider.GetUserName();
} }
siteInfo = await SiteInfoService.GetSiteInfoWithFormsByIdAsync(SiteId) ?? new SiteInfo(); siteInfo = await _contentEditorService.GetSiteInfoWithFormsByIdAsync(SiteId) ?? new SiteInfo();
} }
private async Task SaveSiteInfo() private async Task SaveSiteInfo()
{ {
await SiteInfoService.UpdateSiteInfoAsync(siteInfo); await _contentEditorService.UpdateSiteInfoAsync(siteInfo);
} }
} }

View File

@ -4,6 +4,8 @@
@using BLAIzor.Models.Editor @using BLAIzor.Models.Editor
@using BLAIzor.Services @using BLAIzor.Services
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@using SixLabors.ImageSharp
@using SixLabors.ImageSharp.Processing
@layout AdminLayout @layout AdminLayout
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IHttpContextAccessor HttpContextAccessor @inject IHttpContextAccessor HttpContextAccessor
@ -26,19 +28,28 @@ else
{ {
<div> <div>
<h4>Uploaded Images</h4> <h4>Uploaded Images</h4>
<div class="row">
<RadzenRow class="rz-text-align-center" Gap="1rem">
@foreach (var image in files.Images) @foreach (var image in files.Images)
{ {
<div class="col-xs-12 col-sm-6 col-md-3 col-lg-2">
<RadzenColumn Size="6" SizeXS="12" SizeSM="6" SizeMD="4" SizeLG="3" SizeXL="2" SizeXX="2" class="rz-color-on-info-lighter rz-p-5">
<div class="card"> <div class="card">
<div class="upload-image-container" @onclick="() => CopyToClipboard(image)"> <div class="upload-image-container" @onclick="() => CopyToClipboard(image.OriginalUrl)">
<img class="img-fluid square-thumbnail" src="@image" alt="Uploaded Image" /> <img class="img-fluid square-thumbnail" src="@image.ThumbnailUrl" alt="Uploaded Image" loading="lazy" />
</div> </div>
<button class="btn btn-danger" @onclick="@(() => DeleteFile(image, "Images"))">Delete</button> <button class="btn btn-danger" @onclick="@(() => DeleteFile(image.OriginalUrl, "Images"))">Delete</button>
</div> </div>
</div> </RadzenColumn>
} }
</div> </RadzenRow>
</div> </div>
<div> <div>
<h4>Uploaded Videos</h4> <h4>Uploaded Videos</h4>
@ -106,25 +117,81 @@ else
{ {
var basePath = Path.Combine("wwwroot", "uploads", userId); var basePath = Path.Combine("wwwroot", "uploads", userId);
files.Images = Directory.Exists(Path.Combine(basePath, "images")) // Load IMAGES
? Directory.GetFiles(Path.Combine(basePath, "images")) var imagesPath = Path.Combine(basePath, "images");
.Select(f => $"/uploads/{userId}/images/{Path.GetFileName(f)}") var thumbnailsPath = Path.Combine(imagesPath, "thumbnails");
.ToList()
: new List<string>();
files.Videos = Directory.Exists(Path.Combine(basePath, "videos")) if (!Directory.Exists(thumbnailsPath))
? Directory.GetFiles(Path.Combine(basePath, "videos")) {
Directory.CreateDirectory(thumbnailsPath);
}
if (Directory.Exists(imagesPath))
{
var imageFiles = Directory.GetFiles(imagesPath)
.Where(f => !Path.GetFileName(f).Equals("thumbnails", StringComparison.OrdinalIgnoreCase)) // skip folder
.Where(f => !Directory.Exists(f)) // skip directories
.Select(f =>
{
var fileName = Path.GetFileName(f);
var originalUrl = $"/uploads/{userId}/images/{fileName}";
var thumbnailPath = Path.Combine(thumbnailsPath, fileName);
var thumbnailUrl = $"/uploads/{userId}/images/thumbnails/{fileName}";
// Generate thumbnail if missing
if (!File.Exists(thumbnailPath))
{
try
{
using var image = Image.Load(f);
image.Mutate(x => x.Resize(new ResizeOptions
{
Mode = ResizeMode.Max,
Size = new Size(300, 300)
}));
image.Save(thumbnailPath); // Will auto-detect format
}
catch (Exception ex)
{
Console.WriteLine($"❌ Error generating thumbnail for {fileName}: {ex.Message}");
thumbnailUrl = string.Empty;
}
}
return new UploadedImage
{
OriginalUrl = originalUrl,
ThumbnailUrl = thumbnailUrl
};
})
.ToList();
files.Images = imageFiles;
}
else
{
files.Images = new List<UploadedImage>();
}
// Load VIDEOS
var videosPath = Path.Combine(basePath, "videos");
files.Videos = Directory.Exists(videosPath)
? Directory.GetFiles(videosPath)
.Select(f => $"/uploads/{userId}/videos/{Path.GetFileName(f)}") .Select(f => $"/uploads/{userId}/videos/{Path.GetFileName(f)}")
.ToList() .ToList()
: new List<string>(); : new List<string>();
files.Audio = Directory.Exists(Path.Combine(basePath, "audio")) // Load AUDIO
? Directory.GetFiles(Path.Combine(basePath, "audio")) var audioPath = Path.Combine(basePath, "audio");
files.Audio = Directory.Exists(audioPath)
? Directory.GetFiles(audioPath)
.Select(f => $"/uploads/{userId}/audio/{Path.GetFileName(f)}") .Select(f => $"/uploads/{userId}/audio/{Path.GetFileName(f)}")
.ToList() .ToList()
: new List<string>(); : new List<string>();
} }
private async Task DeleteFile(string filePath, string fileType) private async Task DeleteFile(string filePath, string fileType)
{ {
@ -152,27 +219,44 @@ else
var folder = GetFolderForFile(file.ContentType); var folder = GetFolderForFile(file.ContentType);
var folderPath = Path.Combine(uploadPath, folder); var folderPath = Path.Combine(uploadPath, folder);
// Create directories if they don't exist // Create target directory
if (!Directory.Exists(folderPath)) Directory.CreateDirectory(folderPath);
{
Directory.CreateDirectory(folderPath);
}
// Save file
var filePath = Path.Combine(folderPath, file.Name); var filePath = Path.Combine(folderPath, file.Name);
using (var stream = new FileStream(filePath, FileMode.Create)) await using (var stream = new FileStream(filePath, FileMode.Create))
{ {
await file.OpenReadStream(50 * 1024 * 1024).CopyToAsync(stream); await file.OpenReadStream(50 * 1024 * 1024).CopyToAsync(stream);
} }
// Add relative path to content
var relativePath = $"/uploads/{userId}/{folder}/{file.Name}"; var relativePath = $"/uploads/{userId}/{folder}/{file.Name}";
AppendFilePathToContent(file.ContentType, relativePath); AppendFilePathToContent(file.ContentType, relativePath);
// Generate thumbnail if it's an image
string? thumbnailRelativePath = null;
if (file.ContentType.StartsWith("image/"))
{
var thumbnailFolder = Path.Combine(folderPath, "thumbnails");
Directory.CreateDirectory(thumbnailFolder);
var thumbnailPath = Path.Combine(thumbnailFolder, file.Name);
using var image = await Image.LoadAsync(file.OpenReadStream());
image.Mutate(x => x.Resize(new ResizeOptions
{
Size = new Size(300, 0),
Mode = ResizeMode.Max
}));
await image.SaveAsync(thumbnailPath);
thumbnailRelativePath = $"/uploads/{userId}/{folder}/thumbnails/{file.Name}";
}
AppendFilePathToContent(file.ContentType, relativePath, thumbnailRelativePath);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.Write($"Error uploading files: {ex.Message}"); Console.WriteLine($"Error uploading files: {ex.Message}");
} }
finally finally
{ {
@ -191,11 +275,15 @@ else
}; };
} }
private void AppendFilePathToContent(string contentType, string relativePath) private void AppendFilePathToContent(string contentType, string relativePath, string? thumbnailPath = null)
{ {
if (contentType.StartsWith("image/")) if (contentType.StartsWith("image/"))
{ {
files.Images.Add(relativePath); files.Images.Add(new UploadedImage
{
OriginalUrl = relativePath,
ThumbnailUrl = thumbnailPath ?? string.Empty
});
} }
else if (contentType.StartsWith("video/")) else if (contentType.StartsWith("video/"))
{ {
@ -205,6 +293,7 @@ else
{ {
files.Audio.Add(relativePath); files.Audio.Add(relativePath);
} }
StateHasChanged(); StateHasChanged();
} }
} }

View File

@ -5,7 +5,7 @@
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@using System.Text @using System.Text
@using UglyToad.PdfPig @using UglyToad.PdfPig
@inject ContentEditorService ContentEditorService @inject ContentEditorAIService ContentEditorAIService
<h3>PDF Form Extractor</h3> <h3>PDF Form Extractor</h3>
@ -89,7 +89,7 @@
private async Task<List<FormFieldGroup>> CallAiForFieldExtraction(string plainText) private async Task<List<FormFieldGroup>> CallAiForFieldExtraction(string plainText)
{ {
var jsonResult = await ContentEditorService.AnalyzeGroupedFormFieldsFromText(plainText); var jsonResult = await ContentEditorAIService.AnalyzeGroupedFormFieldsFromText(plainText);
Console.WriteLine($"📄 PDF form AI response: {jsonResult}"); Console.WriteLine($"📄 PDF form AI response: {jsonResult}");
try try

View File

@ -9,6 +9,7 @@
@using System.Text @using System.Text
@using System.Text.Json @using System.Text.Json
@using System.Net @using System.Net
@using Radzen.Blazor.Rendering
@rendermode InteractiveServer @rendermode InteractiveServer
@inject ContentService _contentService @inject ContentService _contentService
@ -18,14 +19,84 @@
@inject DesignTemplateService DesignTemplateService @inject DesignTemplateService DesignTemplateService
@inject CssTemplateService CssTemplateService @inject CssTemplateService CssTemplateService
@inject CssInjectorService CssService @inject CssInjectorService CssService
@inject DialogService DialogService
<div class="page" style="z-index: 1"> <div class="page" style="z-index: 1">
<NavMenu MenuString="@Menu" OnMenuClicked=@MenuClick></NavMenu> <RadzenButton Click="@(args => ToggleSettings())" style="position: fixed;
z-index: 10005;
top: 100px;
left: 0px;
border-bottom-left-radius: 0px;
border-top-left-radius: 0px;
border-bottom-right-radius: 20px;
border-top-right-radius: 20px;">
<i class="fa-solid fa-wrench"></i>
</RadzenButton>
<RadzenStack class="editor-window animate__animated animate__slideInLeft" Orientation="Orientation.Vertical" style="" Visible=@displaySettingsPanel>
<RadzenStack Orientation="Orientation.Vertical" style="">
<div class="rz-p-2 rz-text-align-center">
<h3>Basic information</h3>
<p>@SiteInfo.SiteName</p>
<RadzenButton class="btn" Text="Edit basic info" Click="@EditSite" />
</div>
</RadzenStack>
<RadzenStack Orientation="Orientation.Vertical" style="">
<div class="rz-p-2 rz-text-align-center">
<h5>Manage content</h5>
<RadzenButton class="btn" Text="Manage content" Click="@OpenManageContentGroups" />
</div>
</RadzenStack>
<RadzenStack Orientation="Orientation.Vertical" style="">
<div class="rz-p-2 rz-text-align-center">
<h5>Manage design</h5>
<p>Template: @SiteInfo.TemplateId</p>
</div>
</RadzenStack>
<RadzenStack Orientation="Orientation.Vertical" Visible=@displayOptions style="">
<div class="rz-p-2 rz-text-align-center">
<h5>Page</h5>
<p>@currentMenuItem.Name</p>
</div>
<div class="rz-p-2 rz-text-align-center">
<p>Happy with this page?</p>
<RadzenButton class="btn" Text="@(isContentSaved ? "Update layout" : "Save layout")" Click="@(args => SaveCurrentLayout(currentMenuItem))" />
</div>
<div class="rz-p-2 rz-text-align-center">
<RadzenButton class="btn" @ref=button Text="Regenerate" Click="@(args => MenuClick(currentMenuItem.Name))" />
</div>
</RadzenStack>
<RadzenStack Orientation="Orientation.Vertical" style="">
<div class="rz-p-2 rz-text-align-center">
<h5>Media</h5>
<RadzenButton class="btn" Text="Open library" Click="@OpenManageUploads" />
</div>
</RadzenStack>
</RadzenStack>
<NewNavMenu Menu="@MenuItems" BrandName="@SelectedBrandName" OnMenuClicked=@MenuClick></NewNavMenu>
<main> <main>
<article class="content text-center" style="position: relative; z-index: 4;"> <article class="content text-center" style="position: relative; z-index: 4;">
<PageTitle>Home</PageTitle> <PageTitle>Home</PageTitle>
<VideoComponent SelectedBrandName="@SelectedBrandName" /> @{
if (SiteInfo != null)
{
if (SiteInfo.BackgroundVideo != null)
{
<VideoComponent site="SiteInfo" />
}
}
}
@* <HeadContent> @* <HeadContent>
@if (!string.IsNullOrEmpty(dynamicallyLoadedCss)) @if (!string.IsNullOrEmpty(dynamicallyLoadedCss))
@ -59,11 +130,12 @@
<button id="stopButton" class="btn btn-primary voicebutton" onclick="stopRecording()" hidden><i class="fa-solid fa-microphone-slash"></i></button> <button id="stopButton" class="btn btn-primary voicebutton" onclick="stopRecording()" hidden><i class="fa-solid fa-microphone-slash"></i></button>
} }
if(TTSEnabled) if (TTSEnabled)
{ {
if(!AiVoicePermitted) if (!AiVoicePermitted)
{ {
<button data-hint="listen" class="btn btn-primary voicebutton" @onclick="AllowAIVoice"><i class="fa-solid fa-volume-xmark"></i> <button data-hint="listen" class="btn btn-primary voicebutton" @onclick="AllowAIVoice">
<i class="fa-solid fa-volume-xmark"></i>
</button> </button>
} }
@ -134,6 +206,7 @@
<button class="btn btn-primary" @onclick="HomeClick"><i class="fa-solid fa-rotate"></i></button> <button class="btn btn-primary" @onclick="HomeClick"><i class="fa-solid fa-rotate"></i></button>
</article> </article>
</main> </main>
<FooterComponent MenuString="@Menu" OnMenuClicked=@MenuClick></FooterComponent>
</div> </div>
<script> <script>
@ -184,12 +257,36 @@
public string? topic { get; set; } public string? topic { get; set; }
private string ChatGptResponse = string.Empty; private string ChatGptResponse = string.Empty;
private bool isRecording = false; private bool isRecording = false;
private string dynamicallyLoadedCss = string.Empty; // private string dynamicallyLoadedCss = string.Empty;
private bool isContentSaved = false;
private string Menu; private bool displaySettingsPanel = false;
private bool displayOptions = false;
private bool forceRegenerate = false;
private RadzenButton button;
public string DialogKey { get; set; } = "DefaultDialog";
private Dictionary<string, EditSiteInfoDialogSettings> _dialogSettings = new();
private readonly Dictionary<string, bool> _hasInitialized = new();
private void ToggleSettings()
{
displaySettingsPanel = !displaySettingsPanel;
}
async Task SaveCurrentLayout(MenuItem menuItem)
{
//Save current layout called
menuItem.StoredHtml = HtmlContent.ToString();
Console.WriteLine($"Content length: {HtmlContent.Length}");
Console.WriteLine(menuItem.StoredHtml);
var result = await _contentEditorService.UpdateMenuItemAsync(menuItem);
Console.WriteLine($"menuitem updated: {result.Id}, {result.Name}");
}
private void AllowAIVoice() private void AllowAIVoice()
{ {
AiVoicePermitted = true; AiVoicePermitted = true;
@ -199,7 +296,6 @@
AiVoicePermitted = false; AiVoicePermitted = false;
} }
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)
@ -235,25 +331,6 @@
_navigationManager.Refresh(true); _navigationManager.Refresh(true);
} }
// private async Task HandleValidSubmit()
// {
// try
// {
// // Simulate sending an email
// await ((EmailService)_emailService).SendEmailAsync(ContactFormModel, DocumentEmailAddress);
// SuccessMessage = "Thank you for contacting us! Your message has been sent.";
// ErrorMessage = null;
// // Clear the form
// ContactFormModel = new();
// }
// catch (Exception ex)
// {
// ErrorMessage = "An error occurred while sending your message. Please try again later.";
// SuccessMessage = null;
// }
// }
private void CancelEmail() private void CancelEmail()
{ {
FirstColumnClass = ""; FirstColumnClass = "";
@ -266,26 +343,6 @@
myHome = this; // Set the static reference to the current instance myHome = this; // Set the static reference to the current instance
} }
// [JSInvokable("OpenEmailForm3")]
// public static async void OpenEmailForm3(string emailAddress)
// {
// if (myHome != null)
// {
// await myHome.DisplayEmailForm(emailAddress);
// }
// Console.Write("openEmail with: " + emailAddress);
// }
// public async Task DisplayEmailForm(string emailAddress)
// {
// FirstColumnClass = "col-12 col-md-6";
// isEmailFormVisible = true;
// DocumentEmailAddress = emailAddress;
// StateHasChanged();
// var result = await jsRuntime.InvokeAsync<object>("getDivContent", "currentContent");
// _scopedContentService.CurrentDOM = JsonSerializer.Serialize(result);
// // Console.Write($"{_scopedContentService.CurrentDOM}");
// }
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
@ -295,7 +352,7 @@
_scopedContentService.OnBrandNameChanged += HandleBrandNameChanged; _scopedContentService.OnBrandNameChanged += HandleBrandNameChanged;
SelectedBrandName = _scopedContentService.SelectedBrandName; SelectedBrandName = _scopedContentService.SelectedBrandName;
// Subdomain = HttpContextAccessor.HttpContext?.Items["Subdomain"]?.ToString(); // Subdomain = HttpContextAccessor.HttpContext?.Items["Subdomain"]?.ToString();
SiteInfo = await _scopedContentService.GetSiteInfoByIdAsync(SiteId); SiteInfo = await _contentEditorService.GetSiteInfoByIdAsync(SiteId);
if (SiteInfo != null) if (SiteInfo != null)
{ {
@ -304,7 +361,7 @@
_scopedContentService.SelectedBrandName = SiteInfo.SiteName.ToLower(); _scopedContentService.SelectedBrandName = SiteInfo.SiteName.ToLower();
TTSEnabled = SiteInfo.TTSActive; TTSEnabled = SiteInfo.TTSActive;
STTEnabled = SiteInfo.STTActive; STTEnabled = SiteInfo.STTActive;
Console.Write("Selected brand name:" + _scopedContentService.SelectedBrandName); Console.Write("Selected brand name:" + _scopedContentService.SelectedBrandName);
} }
else else
@ -313,9 +370,10 @@
_scopedContentService.SelectedBrandName = "default"; _scopedContentService.SelectedBrandName = "default";
TTSEnabled = false; TTSEnabled = false;
STTEnabled = false; STTEnabled = false;
} }
_scopedContentService.SelectedSiteId = SiteId; _scopedContentService.SelectedSiteId = SiteId;
ContentCollectionName = SiteInfo.VectorCollectionName;
Console.Write("------------------------"); Console.Write("------------------------");
// Load the CSS template for the selected brand from the database // Load the CSS template for the selected brand from the database
@ -340,34 +398,45 @@
AIService.OnTextContentAvailable += UpdateTextContentForVoice; AIService.OnTextContentAvailable += UpdateTextContentForVoice;
Menu = await GetMenuList(SiteId); Menu = await GetMenuList(SiteId);
MenuItems = await GetMenuItems(SiteId);
if (string.IsNullOrEmpty(HtmlContent.ToString())) if (string.IsNullOrEmpty(HtmlContent.ToString()))
{ {
if (!string.IsNullOrWhiteSpace(topic)) if (!string.IsNullOrWhiteSpace(topic))
{ {
UserInput = topic; UserInput = topic;
await ChatGptService.ProcessContentRequest(SessionId, UserInput, SiteId, (int)SiteInfo.TemplateId!, TemplateCollectionName, Menu, true); await ChatGptService.ProcessContentRequest(SessionId, UserInput, SiteId, (int)SiteInfo.TemplateId!, ContentCollectionName, Menu, true);
} }
else else
{ {
await ChatGptService.GetChatGptWelcomeMessage(SessionId, SiteId, TemplateCollectionName, Menu); await ChatGptService.GetChatGptWelcomeMessage(SessionId, SiteId, TemplateCollectionName, Menu);
SiteModel = await ChatGptService.InitSite(SessionId, SiteId, TemplateCollectionName, Menu);
//await ChatGptService.ProcessContentRequest(SessionId, MenuItems.FirstOrDefault(), SiteId, (int)SiteInfo.TemplateId!, TemplateCollectionName, Menu, true);
} }
} }
UserInput = string.Empty; UserInput = string.Empty;
_initVoicePending = true; _initVoicePending = true;
} }
private async void UpdateContent(string receivedSessionId, string content) private async void UpdateContent(string receivedSessionId, string content, MenuItem menuItem)
{ {
if (receivedSessionId == SessionId) // Only accept messages meant for this tab if (receivedSessionId == SessionId) // Only accept messages meant for this tab
{ {
HtmlContent.Clear(); HtmlContent.Clear();
HtmlContent.Append(content); HtmlContent.Append(content);
//TODO SAVE TO DB
if (menuItem != null)
{
currentMenuItem = menuItem;
displayOptions = true;
}
//InvokeAsync(StateHasChanged); // Ensures UI updates dynamically //InvokeAsync(StateHasChanged); // Ensures UI updates dynamically
await InvokeAsync(() => await InvokeAsync(() =>
{ {
StateHasChanged(); StateHasChanged();
}); });
var result = await jsRuntime.InvokeAsync<object>("getDivContent", "currentContent");
_scopedContentService.CurrentDOM = JsonSerializer.Serialize(result);
//_scopedContentService.CurrentDOM = await jsRuntime.InvokeAsync<string>("getDivContent", "currentContent"); //_scopedContentService.CurrentDOM = await jsRuntime.InvokeAsync<string>("getDivContent", "currentContent");
} }
} }
@ -420,34 +489,11 @@
{ {
HtmlContent.Clear(); HtmlContent.Clear();
var menu = await GetMenuList(SiteId); var menu = await GetMenuList(SiteId);
await ChatGptService.ProcessUserIntent(SessionId, UserInput, SiteId, (int)SiteInfo.TemplateId!, TemplateCollectionName, menu); await ChatGptService.ProcessUserIntent(SessionId, UserInput, SiteId, (int)SiteInfo.TemplateId!, ContentCollectionName, menu);
UserInput = string.Empty; UserInput = string.Empty;
} }
} }
// public async Task HandleVoiceCommand(string input)
// {
// // HtmlContent = string.Empty;
// UserInput = input;
// await InvokeAsync(StateHasChanged);
// await SendUserQuery();
// //UserInput = string.Empty;
// }
// private async Task SendUserQuery()
// {
// welcomeStage = false;
// if (!string.IsNullOrEmpty(UserInput))
// {
// HtmlContent.Clear();
// var menu = await GetMenuList(SiteId);
// Console.Write($"\n\n Input: {UserInput} \n\n");
// await ChatGptService.ProcessUserIntent(SessionId, UserInput, SiteId, (int)SiteInfo.TemplateId!, CollectionName, menu);
// UserInput = string.Empty;
// }
// }
public void Dispose() public void Dispose()
{ {
dynamicallyLoadedCss = ""; dynamicallyLoadedCss = "";
@ -461,4 +507,355 @@
{ {
await CssTemplateService.DeleteSessionCssFile(SessionId); await CssTemplateService.DeleteSessionCssFile(SessionId);
} }
public async Task EditSite()
{
string dialogKey = $"EditSiteInfoDialogSettings_{siteid}"; // can be anything unique
var settings = await LoadStateAsync(dialogKey)
?? new EditSiteInfoDialogSettings
{
Width = "500px",
Height = "512px",
Left = "10%",
Top = "10%"
};
await DialogService.OpenAsync<ManageSiteInfoPartial>($"Edit information of {SiteInfo.SiteName}",
new Dictionary<string, object>() {
{ "SiteId", siteid },
{ "OnSiteNameChanged", new Func<string, Task>(OnSiteNameChanged) },
},
new DialogOptions()
{
Resizable = true,
Draggable = true,
Resize = GetResizeHandler(dialogKey),
Drag = GetDragHandler(dialogKey),
Width = settings.Width,
Height = settings.Height,
Left = settings.Left,
Top = settings.Top,
CssClass = "draggable-popup-dialog",
WrapperCssClass = "draggable-popup-dialog-wrapper"
});
await SaveStateAsync(dialogKey, _editSiteinfoSettings);
}
private async Task OnSiteNameChanged(string newName)
{
Console.WriteLine("Sitename updated!!!!!!");
SelectedBrandName = newName;
}
public async Task EditContentItem(int Id)
{
string dialogKey = $"EditContentItemDialogSettings_{Id}"; // can be anything unique
var settings = await LoadStateAsync(dialogKey)
?? new EditSiteInfoDialogSettings
{
Width = "70vw",
Height = "80vh",
Left = "10%",
Top = "10%"
};
await DialogService.OpenAsync<EditContentItem>($"Edit content {Id}",
new Dictionary<string, object>() {
{ "ContentItemId", Id },
{ "OnContentUpdated", new Func<ContentItem, Task>(OnContentItemUpdated) },
{ "OnSaved", new Func<ContentItem, Task>(OnContentItemSaved) },
{ "OnCancelled", new Func<Task>(OnEditContentItemCancelClicked) },
},
new DialogOptions()
{
Resizable = true,
Draggable = true,
Resize = GetResizeHandler(dialogKey),
Drag = GetDragHandler(dialogKey),
Width = settings.Width,
Height = settings.Height,
Left = settings.Left,
Top = settings.Top,
CssClass = "draggable-popup-dialog",
WrapperCssClass = "draggable-popup-dialog-wrapper"
});
await SaveStateAsync(dialogKey, _editSiteinfoSettings);
}
private async Task OnContentItemUpdated(ContentItem contentGroup)
{
Console.WriteLine("ContentGroup updated!!!!!!");
}
private async Task OnContentItemSaved(ContentItem contentGroup)
{
Console.WriteLine("ContentGroup edit started!!!!!!");
}
private async Task OnEditContentItemCancelClicked()
{
Console.WriteLine("ContentItem Edit clicked");
}
public async Task OpenManageContentGroups()
{
string dialogKey = $"ManageContentGroupsDialogSettings_{siteid}"; // can be anything unique
var settings = await LoadStateAsync(dialogKey);
if (settings != null)
{
Console.WriteLine($"Settings: {settings.Top}, {settings.Left}, {settings.Height}, {settings.Width}");
}
else
{
settings = new EditSiteInfoDialogSettings
{
Width = "300px",
Height = "612px",
Left = "10%",
Top = "10%"
};
}
await DialogService.OpenAsync<ManageContentGroupsPartial>($"Manage content of {SiteInfo.SiteName}",
new Dictionary<string, object>() {
{ "SiteInfoId", siteid },
{ "OnManageContentItemClicked", new Func<string, int, Task>(OnManageContentItemClicked) }
},
new DialogOptions()
{
Resizable = true,
Draggable = true,
Resize = GetResizeHandler(dialogKey),
Drag = GetDragHandler(dialogKey),
Width = settings.Width,
Height = settings.Height,
Left = settings.Left,
Top = settings.Top,
CssClass = "draggable-popup-dialog",
WrapperCssClass = "draggable-popup-dialog-wrapper"
});
await SaveStateAsync(dialogKey, _editSiteinfoSettings);
}
private async Task OnContentGroupUpdated(ContentGroup contentGroup)
{
Console.WriteLine("ContentGroup updated!!!!!!");
}
private async Task OnContentGroupEditStarted(ContentGroup contentGroup)
{
Console.WriteLine("ContentGroup edit started!!!!!!");
}
private async Task OnManageContentItemClicked(string methodName, int Id)
{
Console.WriteLine("ContentItem Edit clicked");
await EditContentItem(Id);
}
public async Task OpenManageUploads()
{
string dialogKey = $"ManageUploadsDialogSettings_{siteid}"; // can be anything unique
var settings = await LoadStateAsync(dialogKey)
?? new EditSiteInfoDialogSettings
{
Width = "600px",
Height = "600px",
Left = "10%",
Top = "10%"
};
await DialogService.OpenAsync<ManageUploads>($"Media library",
null,
new DialogOptions()
{
Resizable = true,
Draggable = true,
Resize = GetResizeHandler(dialogKey),
Drag = GetDragHandler(dialogKey),
Width = settings.Width,
Height = settings.Height,
Left = settings.Left,
Top = settings.Top,
CssClass = "draggable-popup-dialog",
WrapperCssClass = "draggable-popup-dialog-wrapper"
});
await SaveStateAsync(dialogKey, _editSiteinfoSettings);
}
private Action<System.Drawing.Point> GetDragHandler(string key)
{
return (point) =>
{
if (!_hasInitialized.ContainsKey(key))
{
_hasInitialized[key] = true;
Console.WriteLine($"🚫 Skipping initial drag for {key}");
return;
}
var settings = GetSettingsForKey(key);
Console.WriteLine($"Got settings: {settings.Top}, {settings.Left}, {settings.Height}, {settings.Width}");
settings.Left = $"{point.X}px";
settings.Top = $"{point.Y}px";
Console.WriteLine($"NEW Settings: {settings.Top}, {settings.Left}, {settings.Height}, {settings.Width}");
_dialogSettings[key] = settings;
InvokeAsync(() => SaveStateAsync(key, settings));
};
}
private Action<System.Drawing.Size> GetResizeHandler(string key)
{
return (size) =>
{
if (!_hasInitialized.ContainsKey(key))
{
_hasInitialized[key] = true;
Console.WriteLine($"🚫 Skipping initial resize for {key}");
return;
}
var settings = GetSettingsForKey(key);
Console.WriteLine($"Got settings: {settings.Top}, {settings.Left}, {settings.Height}, {settings.Width}");
settings.Width = $"{size.Width}px";
settings.Height = $"{size.Height}px";
Console.WriteLine($"NEW Settings: {settings.Top}, {settings.Left}, {settings.Height}, {settings.Width}");
_dialogSettings[key] = settings;
InvokeAsync(() => SaveStateAsync(key, settings));
};
}
private EditSiteInfoDialogSettings GetSettingsForKey(string key)
{
Console.WriteLine($"Getting settings for {key}");
if (_dialogSettings.TryGetValue(key, out var value))
return value;
var newSettings = new EditSiteInfoDialogSettings();
_dialogSettings[key] = newSettings;
return newSettings;
}
// void OnDrag(System.Drawing.Point point)
// {
// jsRuntime.InvokeVoidAsync("eval", $"console.log('Dialog drag. Left:{point.X}, Top:{point.Y}')");
// if (EditSiteInfoSettings == null)
// {
// EditSiteInfoSettings = new EditSiteInfoDialogSettings();
// }
// EditSiteInfoSettings.Left = $"{point.X}px";
// EditSiteInfoSettings.Top = $"{point.Y}px";
// InvokeAsync(() => SaveStateAsync(DialogKey, EditSiteInfoSettings));
// }
// void OnResize(System.Drawing.Size size)
// {
// jsRuntime.InvokeVoidAsync("eval", $"console.log('Dialog resize. Width:{size.Width}, Height:{size.Height}')");
// if (EditSiteInfoSettings == null)
// {
// EditSiteInfoSettings = new EditSiteInfoDialogSettings();
// }
// EditSiteInfoSettings.Width = $"{size.Width}px";
// EditSiteInfoSettings.Height = $"{size.Height}px";
// InvokeAsync(() => SaveStateAsync(DialogKey, EditSiteInfoSettings));
// }
private EditSiteInfoDialogSettings _editSiteinfoSettings = new();
public EditSiteInfoDialogSettings EditSiteInfoSettings
{
get => _editSiteinfoSettings;
set
{
if (_editSiteinfoSettings != value)
{
_editSiteinfoSettings = value;
InvokeAsync(() => SaveStateAsync(DialogKey, _editSiteinfoSettings));
}
}
}
// private async Task LoadStateAsync()
// {
// await Task.CompletedTask;
// var result = await jsRuntime.InvokeAsync<string>("window.localStorage.getItem", "EditSiteInfoDialogSettings");
// if (!string.IsNullOrEmpty(result))
// {
// _editSiteinfoSettings = JsonSerializer.Deserialize<EditSiteInfoDialogSettings>(result);
// }
// }
// private async Task SaveStateAsync()
// {
// await Task.CompletedTask;
// await jsRuntime.InvokeVoidAsync("window.localStorage.setItem", "EditSiteInfoDialogSettings", JsonSerializer.Serialize<EditSiteInfoDialogSettings>(EditSiteInfoSettings));
// }
private async Task<EditSiteInfoDialogSettings?> LoadStateAsync(string dialogKey)
{
Console.WriteLine($"Loading setting for {dialogKey}");
var result = await jsRuntime.InvokeAsync<string>("window.localStorage.getItem", dialogKey);
if (!string.IsNullOrEmpty(result))
{
Console.WriteLine($"Found setting for {dialogKey}");
return JsonSerializer.Deserialize<EditSiteInfoDialogSettings>(result);
}
return null;
}
private async Task SaveStateAsync(string dialogKey, EditSiteInfoDialogSettings settings)
{
if (string.IsNullOrEmpty(settings?.Width) ||
string.IsNullOrEmpty(settings?.Height) ||
string.IsNullOrEmpty(settings?.Top) ||
string.IsNullOrEmpty(settings?.Left))
{
Console.WriteLine($"❌ Skipping save for {dialogKey} due to invalid values");
return; // Do NOT save garbage data
}
Console.WriteLine($"Saving setting for {dialogKey}");
var json = JsonSerializer.Serialize(settings);
Console.WriteLine($"Saving ettings: {settings.Top}, {settings.Left}, {settings.Height}, {settings.Width}, {json}");
await jsRuntime.InvokeVoidAsync("window.localStorage.setItem", dialogKey, json);
}
public class EditSiteInfoDialogSettings
{
public string Left { get; set; }
public string Top { get; set; }
public string Width { get; set; }
public string Height { get; set; }
}
} }

View File

@ -110,7 +110,7 @@ else
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
await base.OnParametersSetAsync(); await base.OnParametersSetAsync();
var siteInfo = await scopedContentService.GetSiteInfoByIdAsync(siteId); var siteInfo = await contentEditorService.GetSiteInfoByIdAsync(siteId);
// var menuItems1 = await contentEditorService.GetMenuItemsBySiteIdAsync(1); // var menuItems1 = await contentEditorService.GetMenuItemsBySiteIdAsync(1);
menuItems = new ObservableCollection<MenuItem>(await contentEditorService.GetMenuItemsBySiteIdAsync(siteId)); menuItems = new ObservableCollection<MenuItem>(await contentEditorService.GetMenuItemsBySiteIdAsync(siteId));
@ -129,17 +129,17 @@ else
foreach (var menuItem in menuItems) foreach (var menuItem in menuItems)
{ {
string content; List<WebPageContent> content;
MenuItemModel model = new MenuItemModel(""); MenuItemModel model = new MenuItemModel("");
//try to get content from qDrant //try to get content from qDrant
if (menuItem.QdrantPointId != null) if (menuItem.ContentGroupId != null && menuItem.ContentItemId != null)
{ {
content = await qDrantService.GetContentAsync(siteId, menuItem.SortOrder); var contentItem = await contentEditorService.GetContentItemByIdAsync((int)menuItem.ContentItemId);
var selectedPoint = JsonConvert.DeserializeObject<QDrantGetContentPointResult>(content)!;
if (selectedPoint != null) if (contentItem != null)
{ {
model.Content = selectedPoint.result.payload.content; model.Content = contentItem.Content;
Console.Write($"Found point: {selectedPoint.result.payload.content}");
} }
} }

View File

@ -6,7 +6,7 @@
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@layout AdminLayout @layout AdminLayout
@inject ScopedContentService SiteInfoService @inject ScopedContentService SiteInfoService
@inject ContentService ContentService @inject ContentEditorService _contentEditorService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider @inject AuthenticationStateProvider AuthenticationStateProvider
@inject CustomAuthenticationStateProvider CustomAuthProvider @inject CustomAuthenticationStateProvider CustomAuthProvider
@ -63,7 +63,7 @@
{ {
<RadzenPanel AllowCollapse="true" class="rz-my-5 rz-mx-auto" Style="width: 100%" <RadzenPanel AllowCollapse="true" class="rz-my-5 rz-mx-auto" Style="width: 100%"
Expand=@(() => Change("Panel expanded")) Collapse=@(() => Change("Panel collapsed"))> Expand=@(() => Change("Panel expanded")) Collapse=@(() => Change("Panel collapsed"))>
<HeaderTemplate> <HeaderTemplate>
<RadzenText TextStyle="TextStyle.H6" class="rz-display-flex rz-align-items-center rz-m-0"> <RadzenText TextStyle="TextStyle.H6" class="rz-display-flex rz-align-items-center rz-m-0">
<RadzenIcon Icon="account_box" class="rz-me-1" /><b>Sites created</b> <RadzenIcon Icon="account_box" class="rz-me-1" /><b>Sites created</b>
@ -72,7 +72,7 @@
<ChildContent> <ChildContent>
<RadzenCard class="rz-mt-4"> <RadzenCard class="rz-mt-4">
<RadzenDataList PageSize="3" WrapItems="true" AllowPaging="true" <RadzenDataList PageSize="3" WrapItems="true" AllowPaging="true"
Data="@siteInfoList" TItem="SiteInfo"> Data="@siteInfoList" TItem="SiteInfo">
<Template Context="site"> <Template Context="site">
<RadzenCard Style="width: 250px; background-color: darkgrey"> <RadzenCard Style="width: 250px; background-color: darkgrey">
<RadzenRow JustifyContent="@JustifyContent.SpaceBetween"> <RadzenRow JustifyContent="@JustifyContent.SpaceBetween">
@ -95,9 +95,9 @@
</RadzenStack> </RadzenStack>
</RadzenStack> </RadzenStack>
<RadzenStack Orientation="@Orientation.Horizontal" Gap="10px" Reverse="false" JustifyContent="@JustifyContent.Center" AlignItems="@AlignItems.Center" Wrap="@FlexWrap.Wrap" Style="height: fit-content"> <RadzenStack Orientation="@Orientation.Horizontal" Gap="10px" Reverse="false" JustifyContent="@JustifyContent.Center" AlignItems="@AlignItems.Center" Wrap="@FlexWrap.Wrap" Style="height: fit-content">
<InputText @bind-Value="collectionName" class="form-control" style="width: 100%;" placeholder="Site name" />
<a style="font-size: 14px;" href="/site-info/@site.Id" class="btn btn-secondary">Edit</a> <a style="font-size: 14px;" @onclick="()=>Migrate(site.Id, collectionName)" class="btn btn-secondary">Migrate</a>
<a style="font-size: 14px;" href="/generate-content/@site.Id" class="btn btn-secondary">Manage content</a> @* <a style="font-size: 14px;" href="/generate-content/@site.Id" class="btn btn-secondary">Manage content</a> *@
<a style="font-size: 14px;" @onclick="()=>Preview(site)" class="btn btn-secondary"> Preview</a> <a style="font-size: 14px;" @onclick="()=>Preview(site)" class="btn btn-secondary"> Preview</a>
</RadzenStack> </RadzenStack>
@ -157,6 +157,8 @@
private string? userName; private string? userName;
private AuthenticationState? authState; private AuthenticationState? authState;
int position = 1; int position = 1;
//TEMPORARY
private string collectionName = "seemgen-collection";
void Change(string text) void Change(string text)
{ {
@ -170,6 +172,12 @@
NavigationManager.NavigateTo(site.DefaultUrl, true); NavigationManager.NavigateTo(site.DefaultUrl, true);
} }
private async Task Migrate(int siteId, string collectionName)
{
var result = await _contentEditorService.MigrateQdrantToContentItemsAsync(siteId, collectionName);
Console.WriteLine($"Migration result: {result}");
}
private async Task Preview(SiteInfo site) private async Task Preview(SiteInfo site)
{ {
string myUrl = $"/preview/{site.Id}"; string myUrl = $"/preview/{site.Id}";
@ -192,7 +200,7 @@
userId = CustomAuthProvider.GetUserId(); userId = CustomAuthProvider.GetUserId();
userName = CustomAuthProvider.GetUserName(); userName = CustomAuthProvider.GetUserName();
} }
siteInfoList = await SiteInfoService.GetUserSitesAsync(userId!); siteInfoList = await _contentEditorService.GetUserSitesAsync(userId!);
} }
private async Task HandleValidSubmit() private async Task HandleValidSubmit()
@ -200,8 +208,9 @@
newSite.UserId = userId; newSite.UserId = userId;
newSite.TemplateId = 1; newSite.TemplateId = 1;
newSite.DefaultUrl = await GenerateSubdomainAsync(newSite.SiteName); newSite.DefaultUrl = await GenerateSubdomainAsync(newSite.SiteName);
var result = await SiteInfoService.AddSiteInfoAsync(newSite); newSite.VectorCollectionName = _contentEditorService.GetGeneratedVectorCollectionName(newSite);
siteInfoList = await SiteInfoService.GetUserSitesAsync(userId!); var result = await _contentEditorService.AddSiteInfoAsync(newSite);
siteInfoList = await _contentEditorService.GetUserSitesAsync(userId!);
newSite = new(); // Reset the form newSite = new(); // Reset the form
} }
public async Task<string> GenerateSubdomainAsync(string siteName) public async Task<string> GenerateSubdomainAsync(string siteName)

View File

@ -0,0 +1,148 @@
@using BLAIzor.Models
@using BLAIzor.Services
@inject ContentEditorService contentEditorService
@inject DialogService dialogService
@inject NotificationService notificationService
<a style="width:100%" @onclick="ToggleVisibility">
<div class="mb-2 p-3 reference-button bg-panel">
<div class="text-content">
<strong>Content count: @contentItems.Count</strong><br />
<small class="text-muted">See all</small>
</div>
<div class="icon-buttons">
<div class="icon-circle"><i class="fa-solid fa-arrow-down text-white"></i></div>
</div>
</div>
</a>
@if (isVisible)
{
<ul class="list-group content-item-list mb-3">
<li class="list-group-item">
<button class="pointer bg-transparent border-0" style="width:100%; text-align: left; " @onclick="CreateNewItem">
<div class="px-3 py-2 reference-button bg-panel-gradient-highlight pointer">
<div class="text-content">
<strong>Add new</strong>
<br />
<small class="text-muted">Add a new content to this group</small>
</div>
<div class="icon-buttons">
<div class="icon-circle"><i class="fa-solid fa-plus text-white"></i></div>
</div>
</div>
</button>
</li>
@* <li><button class="btn btn-sm btn-primary ms-2" @onclick="CreateNewItem">Add New Item</button></li> *@
@foreach (var item in contentItems)
{
<li class="list-group-item">
<div class="p-2 reference-button">
<div class="text-content">
<strong>@item.Title</strong>
<br />
<small class="text-muted">@item.Language - @item.Tags</small>
</div>
<div class="icon-buttons">
<button class="btn btn-sm btn-outline-primary" @onclick="() => EditItem(item.Id)">Edit</button>
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteItem(item.Id)">Delete</button>
@* <div class="icon-circle">V</div> *@
</div>
</div>
</li>
}
</ul>
}
@code {
[Parameter] public int ContentGroupId { get; set; }
[Parameter] public Func<string, int, Task> OnManageContentItemClicked { get; set; }
private List<ContentItem> contentItems = new();
private bool isVisible = false;
protected override async Task OnInitializedAsync()
{
contentItems = await contentEditorService.GetContentItemsByGroupIdAsync(ContentGroupId);
}
private void ToggleVisibility()
{
isVisible = !isVisible;
}
private async Task CreateNewItem()
{
var newItem = new ContentItem
{
ContentGroupId = ContentGroupId,
Title = "New Item",
Description = "",
Content = "",
Language = "en",
Tags = "",
IsPublished = false,
CreatedAt = DateTime.UtcNow,
LastUpdated = DateTime.UtcNow,
Version = 1
};
var result = await contentEditorService.CreateContentItemAsync(newItem);
if (result != null)
{
contentItems.Insert(0, result);
}
}
private async Task EditItem(int itemId)
{
if (OnManageContentItemClicked != null)
await OnManageContentItemClicked.Invoke("EditContentItem", itemId);
}
private async Task DeleteItem(int itemId)
{
var confirmationResult = await dialogService.Confirm("Are you sure?", "Delete content", new ConfirmOptions() { OkButtonText = "Delete", CancelButtonText = "Oops, no" });
if (confirmationResult == true)
{
await ReallyDeleteItem(itemId);
}
else
{
//do nothing?
}
}
private async Task ReallyDeleteItem(int itemId)
{
var result = await contentEditorService.DeleteContentItemByIdAsync(itemId);
if (result)
{
var toRemove = contentItems.FirstOrDefault(c => c.Id == itemId);
if (toRemove != null)
{
contentItems.Remove(toRemove);
}
var message = NotificationHelper.CreateNotificationMessage("Group deleted", 2, "Success", "ContentGroup deleted");
ShowNotification(message);
StateHasChanged();
}
}
public void ShowNotification(NotificationMessage message)
{
notificationService.Notify(message);
}
}

View File

@ -0,0 +1,23 @@
@page "/dialogcard/{OrderID}"
@using Microsoft.EntityFrameworkCore
@inject Radzen.DialogService dialogService
<RadzenStack Gap="1rem" Orientation="Orientation.Vertical" JustifyContent="JustifyContent.SpaceBetween" Style="height: 100%;">
<RadzenStack>
</RadzenStack>
</RadzenStack>
@code {
[Parameter] public int OrderID { get; set; }
[Parameter] public bool ShowClose { get; set; } = true;
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
}
}

View File

@ -0,0 +1,120 @@
@using BLAIzor.Models
@if (!editCurrentGroup)
{
@* <div class="reference-button">
<div class="text-content">
<h4>Reference</h4>
<p>Create songs inspired by a<br>reference track</p>
</div>
<div class="icon-buttons">
<div class="icon-circle">+</div>
<div class="icon-circle">★</div>
</div>
</div> *@
<div class="bg-panel rounded p-3 mb-3">
<div class="row">
<div class="col-8">
<div @key="Group.Id">
<strong>
@Group.Name
</strong>
<small class="text-muted">
Group type: @Group.Type
</small>
<small class="text-muted">
Group slug: @Group.Slug
</small>
</div>
</div>
<div class="col-4">
<div class="d-flex flex-row-reverse">
@* <RadzenButton Text="Edit" Click="() => ToggleGroupEdit()"></RadzenButton>
<RadzenButton ButtonStyle=@ButtonStyle.Warning Text="Edit" Click="() => OnForceReChunkClicked(Group)"></RadzenButton> *@
<button class="btn" type="button" @onclick="() => ToggleGroupEdit()">Edit</button>
<button class="btn btn-warning" type="button" @onclick="() => OnForceReChunkClicked(Group)">ReChunk</button>
<button class="btn btn-danger" type="button" @onclick="() => DeleteGroup(Group)">Delete</button>
</div>
</div>
</div>
<ContentItemList ContentGroupId="@Group.Id" OnManageContentItemClicked="ContentItemManagedCallback"></ContentItemList>
</div>
}
else
{
<EditForm Model="@Group" OnValidSubmit="() => SaveGroup(Group)">
<div class="rounded bg-panel p-3 mb-3">
<label>
Name
<InputText class="form-control my-1" @bind-Value="Group.Name" />
</label>
<label>
Type
<InputText class="form-control my-1" @bind-Value="Group.Type" />
</label>
<div class="d-flex flex-row-reverse">
<button class="btn btn-success" type="submit">Save</button>
<button class="btn btn-secondary" type="button" @onclick="() => ToggleGroupEdit()">Cancel</button>
</div>
</div>
</EditForm>
}
@code {
[Parameter] public ContentGroup Group { get; set; }
// [Parameter] public Action<ContentGroup> OnContentGroupUpdated { get; set; }
[Parameter] public Func<ContentGroup, Task> OnContentGroupSaveClicked { get; set; }
[Parameter] public Func<ContentGroup, Task> OnContentGroupDeleteClicked { get; set; }
[Parameter] public Func<ContentGroup, Task> OnForceReChunkClicked { get; set; }
// [Parameter] public Func<Task> OnContentGroupDeselectClicked { get; set; }
[Parameter] public Func<string, int, Task> OnManageContentItemClicked { get; set; }
private bool editCurrentGroup = false;
private async Task ForceReChunk(int contenGroupId)
{
}
private void ToggleGroupEdit()
{
Console.WriteLine($"dklsajdlaéjdksléajdéal djkéa sdjklaésj dkléajdklaéjdkléa");
editCurrentGroup = !editCurrentGroup;
StateHasChanged();
}
private async Task SaveGroup(ContentGroup group)
{
if (OnContentGroupSaveClicked != null)
await OnContentGroupSaveClicked.Invoke(group);
}
private async Task DeleteGroup(ContentGroup group)
{
if (OnContentGroupDeleteClicked != null)
await OnContentGroupDeleteClicked.Invoke(group);
}
private async Task ContentItemManagedCallback(string method, int itemId)
{
if (OnManageContentItemClicked != null)
await OnManageContentItemClicked.Invoke(method, itemId);
}
private async Task ForceContentReChunkCallback(ContentGroup group)
{
if (OnForceReChunkClicked != null)
await OnForceReChunkClicked.Invoke(group);
}
}

View File

@ -0,0 +1,92 @@
@using BLAIzor.Models
@using BLAIzor.Services
@inject ContentEditorService contentEditorService
@if (isLoading)
{
<p>Loading content item...</p>
}
else if (contentItem == null)
{
<p class="text-danger">Content item not found.</p>
}
else
{
<EditForm Model="@contentItem" OnValidSubmit="HandleSave">
<div class="rounded p-3 bg-panel">
<label>
Title
<InputText class="form-control my-2" @bind-Value="contentItem.Title" />
</label>
<label>
Description
<InputTextArea class="form-control my-2" style="height: 60px;" @bind-Value="contentItem.Description" Rows="3" />
</label>
<label>
Content
<InputTextArea class="form-control my-2" style="height: 200px;" @bind-Value="contentItem.Content" Rows="10" />
</label>
<label>
Language
<InputText class="form-control my-2" @bind-Value="contentItem.Language" />
</label>
<label>
Tags (comma-separated)
<InputText class="form-control my-2" @bind-Value="contentItem.Tags" />
</label>
<div class="form-check my-2">
<InputCheckbox class="form-check-input" id="isPublished" @bind-Value="contentItem.IsPublished" />
<label class="form-check-label" for="isPublished">Published</label>
</div>
<div class="d-flex flex-row-reverse">
<button class="btn btn-primary" type="submit">Save</button>
<button class="btn btn-secondary" type="button" @onclick="CancelEdit">Cancel</button>
</div>
</div>
</EditForm>
}
@code {
[Parameter] public int ContentItemId { get; set; }
[Parameter] public Func<ContentItem, Task> OnContentUpdated { get; set; }
[Parameter] public Func<ContentItem, Task> OnSaved { get; set; }
[Parameter] public Func<Task> OnCancelled { get; set; }
private ContentItem? contentItem;
private bool isLoading = true;
protected override async Task OnInitializedAsync()
{
isLoading = true;
contentItem = await contentEditorService.GetContentItemByIdAsync(ContentItemId);
isLoading = false;
}
private async Task HandleSave()
{
contentItem!.LastUpdated = DateTime.UtcNow;
await contentEditorService.SaveAndSyncContentItemAsync(contentItem, contentItem.ContentGroup.SiteInfo.VectorCollectionName, false);
if (OnSaved != null)
await OnSaved.Invoke(contentItem);
}
private async Task CancelEdit()
{
if (OnCancelled != null)
await OnCancelled.Invoke();
}
private async Task OnUpdated()
{
if (OnContentUpdated != null)
await OnContentUpdated.Invoke(contentItem);
}
}

View File

@ -0,0 +1,28 @@
using BLAIzor.Models;
using Microsoft.AspNetCore.Components;
namespace BLAIzor.Components.Partials
{
public class EditorUIComponentBase<T> : ComponentBase
{
[Parameter] public Func<T, Task> OnContentUpdated { get; set; }
[Parameter] public Func<T, Task> OnContentEditStarted { get; set; }
protected async Task NotifyContentUpdatedAsync(T updatedObject)
{
if (OnContentUpdated != null)
{
await OnContentUpdated.Invoke(updatedObject);
}
}
protected async Task NotifyContentEditStartedAsync(T updatedObject)
{
if (OnContentEditStarted != null)
{
await OnContentEditStarted.Invoke(updatedObject);
}
}
}
}

View File

@ -0,0 +1,128 @@
@using BLAIzor.Models
@using BLAIzor.Services
@inject ScopedContentService _scopedContentService;
@inject NavigationManager _navigationManager;
@inject ContentEditorService _contentEditorService
@inject IHttpContextAccessor HttpContextAccessor
@inject IJSRuntime JS
<div class="container-fluid footer" style="position: relative; z-index: 10000; margin-top: 100px;">
<div class="container">
<footer class="footer-inner pt-5">
<div class="row">
@{
var brandFileName = _scopedContentService.SelectedDocument;
var brandName = _scopedContentService.SelectedBrandName;
if (MenuList != null)
{
int columnCount = 3;
int itemsPerColumn = (int)Math.Ceiling(MenuList.Length / (double)columnCount);
var columns = new List<List<string>>();
for (int i = 0; i < columnCount; i++)
{
columns.Add(MenuList.Skip(i * itemsPerColumn).Take(itemsPerColumn).ToList());
}
@foreach (var column in columns)
{
<div class="col-6 col-md-2 mb-3">
<h5>@brandName</h5>
<ul class="nav flex-column">
@foreach (var item in column)
{
<li class="nav-item mb-2" style="text-align: start;"><a class="nav-link p-0 text-body-secondary" @onclick="()=>MenuClickedAsync(item)">@item</a></li>
}
</ul>
</div>
}
}
}
<div class="col-md-5 offset-md-1 mb-3">
<form>
<h5>Subscribe to our newsletter</h5> <p>Monthly digest of what's new and exciting from us.</p>
<div class="d-flex flex-column flex-sm-row w-100 gap-2">
<label for="newsletter1" class="visually-hidden">Email address</label>
<input id="newsletter1" type="email" class="form-control" placeholder="Email address">
<button class="btn btn-primary" type="button">Subscribe</button>
</div>
</form>
</div>
</div>
<div class="d-flex flex-column flex-sm-row justify-content-between pt-4 mt-4 border-top">
<p>© 2025 <NavLink @onclick="HomeClick">@brandName</NavLink>, Inc. All rights reserved.</p>
<ul class="list-unstyled d-flex">
<li class="ms-3"><a href="#" aria-label="Instagram"><i class="fa-brands fa-square-instagram"></i></a></li>
<li class="ms-3"><a href="#" aria-label="Facebook"><i class="fa-brands fa-square-facebook"></i></a></li>
</ul>
</div>
</footer>
</div>
</div>
@code {
public int SiteId;
[Parameter]
public string? MenuString { get; set; }
public string[] MenuList;
public string videoUrl = "";
public string SelectedLanguage;
private int RenderCounter = 0;
[Parameter] public Action<string>? OnMenuClicked { get; set; }
public async Task MenuClickedAsync(string menuName)
{
OnMenuClicked?.Invoke(menuName);
await JS.InvokeVoidAsync("collapseNavbar");
}
public void HomeClick()
{
_navigationManager.Refresh(true);
}
protected override async Task OnInitializedAsync()
{
var lang = await JS.InvokeAsync<string>("getUserLanguage");
// Normalize and match to one of your supported languages
if (lang.StartsWith("hu", StringComparison.OrdinalIgnoreCase))
SelectedLanguage = "Hungarian";
else if (lang.StartsWith("de", StringComparison.OrdinalIgnoreCase))
SelectedLanguage = "German";
else
SelectedLanguage = "English";
_scopedContentService.SelectedLanguage = SelectedLanguage;
}
protected override async Task OnParametersSetAsync()
{
if (!string.IsNullOrEmpty(MenuString))
{
MenuList = MenuString.Split(",");
}
await base.OnParametersSetAsync();
}
public void OnLanguageSelected(ChangeEventArgs e)
{
SelectedLanguage = e.Value?.ToString();
if (!string.IsNullOrEmpty(SelectedLanguage))
{
Console.WriteLine($"Language selected: {SelectedLanguage}");
_scopedContentService.SelectedLanguage = SelectedLanguage;
}
}
}

View File

@ -4,6 +4,7 @@
@using Newtonsoft.Json @using Newtonsoft.Json
@using System.Collections.ObjectModel @using System.Collections.ObjectModel
@inject ContentEditorService ContentEditorService @inject ContentEditorService ContentEditorService
@inject ContentEditorAIService ContentEditorAIService
@inject HtmlSnippetProcessor HtmlSnippetProcessor @inject HtmlSnippetProcessor HtmlSnippetProcessor
@inject QDrantService QDrantService @inject QDrantService QDrantService
@inject IJSRuntime JSRuntime @inject IJSRuntime JSRuntime
@ -153,18 +154,23 @@ else if (!string.IsNullOrEmpty(ErrorMessage))
foreach (var menuItem in menuItems) foreach (var menuItem in menuItems)
{ {
string content; List<WebPageContent> content;
MenuItemModel model = new MenuItemModel(""); MenuItemModel model = new MenuItemModel("");
//try to get content from qDrant //try to get content from qDrant
if (menuItem.QdrantPointId != null) if (menuItem.ContentGroupId != null && menuItem.ContentItemId != null) ////FIXXXXXXX
{ {
content = await QDrantService.GetContentAsync(SiteId, menuItem.PointId); ContentItem contentItem = await ContentEditorService.GetContentItemByIdAsync((int)menuItem.ContentItemId);
var selectedPoint = JsonConvert.DeserializeObject<QDrantGetContentPointResult>(content)!;
if (selectedPoint != null) /* content = await QDrantService.GetPointFromQdrantAsyncByPointId(SiteId, contentItem.Chunks); */ //FIXXXXXXX WONT WORKNOW
//var selectedPoint = JsonConvert.DeserializeObject<QDrantGetContentPointResult>(content)!;
if (contentItem != null)
{ {
model.Content = selectedPoint.result.payload.content; foreach (var vector in contentItem.Chunks)
model.ContentDescription = selectedPoint.result.payload.description; {
Console.Write($"Found point: {selectedPoint.result.payload.content}"); //get vectors to compare
}
model.Content = contentItem.Content;
model.ContentDescription = contentItem.Description;
} }
} }
@ -201,14 +207,14 @@ else if (!string.IsNullOrEmpty(ErrorMessage))
//get website subject //get website subject
var prompt1 = $"Analyze the following text and make an assumption about the characteristics of the text type (website, document, book, etc). Do not attach any explanation, make your answer like `A medical clinic of mammography` or `A webdesign and software development company` or `A book about XY` :\n\n{document}"; var prompt1 = $"Analyze the following text and make an assumption about the characteristics of the text type (website, document, book, etc). Do not attach any explanation, make your answer like `A medical clinic of mammography` or `A webdesign and software development company` or `A book about XY` :\n\n{document}";
var response1 = await ContentEditorService.GetGeneratedContentAsync(SessionId, prompt1); var response1 = await ContentEditorAIService.GetGeneratedContentAsync(SessionId, prompt1);
Console.Write(response1); Console.Write(response1);
subject = response1; subject = response1;
if (!MenuItemsSaved) if (!MenuItemsSaved)
{ {
var prompt2 = $"Analyze the following text and based on the sections suggest a list of menu items for a website. Do not attach any explanation. Text:\n\n`{document}`."; var prompt2 = $"Analyze the following text and based on the sections suggest a list of menu items for a website. Do not attach any explanation. Text:\n\n`{document}`.";
var response2 = await ContentEditorService.GetMenuSuggestionsAsync(SessionId, prompt2); var response2 = await ContentEditorAIService.GetMenuSuggestionsAsync(SessionId, prompt2);
ExtractedMenuItems = response2.Select(name => new MenuItemModel(name)).ToList(); ExtractedMenuItems = response2.Select(name => new MenuItemModel(name)).ToList();
} }
// Send the content to ChatGPT // Send the content to ChatGPT
@ -255,7 +261,7 @@ else if (!string.IsNullOrEmpty(ErrorMessage))
private async Task SaveMenuItems(bool updateVectorDatabase) private async Task SaveMenuItems(bool updateVectorDatabase)
{ {
//bool valami = updateVectorDatabase; //bool valami = updateVectorDatabase;
var result = await ContentEditorService.ProcessMenuItems(SiteId, hasCollection, ExtractedMenuItems, subject, MenuItemsSaved, updateVectorDatabase); var result = await ContentEditorAIService.ProcessMenuItems(SiteId, hasCollection, ExtractedMenuItems, subject, MenuItemsSaved, updateVectorDatabase);
if (result == "OK") if (result == "OK")
{ {
MenuItemsSaved = true; MenuItemsSaved = true;

View File

@ -4,6 +4,7 @@
@using BLAIzor.Components.Partials @using BLAIzor.Components.Partials
@using System.Collections.ObjectModel @using System.Collections.ObjectModel
@inject ContentEditorService ContentEditorService @inject ContentEditorService ContentEditorService
@inject ContentEditorAIService ContentEditorAIService
@inject HtmlSnippetProcessor HtmlSnippetProcessor @inject HtmlSnippetProcessor HtmlSnippetProcessor
@inject QDrantService QDrantService @inject QDrantService QDrantService
@inject IJSRuntime JSRuntime @inject IJSRuntime JSRuntime
@ -122,24 +123,27 @@ else if (!string.IsNullOrEmpty(errorMessage))
foreach (var menuItem in menuItems) foreach (var menuItem in menuItems)
{ {
string content; List<WebPageContent> content;
MenuItemModel model = new MenuItemModel(""); MenuItemModel model = new MenuItemModel("");
//try to get content from qDrant //try to get content from qDrant
if (menuItem.QdrantPointId != null) if (menuItem.ContentGroupId != null && menuItem.ContentItemId != null) ////FIXXXXXXX
{ {
content = await QDrantService.GetContentAsync(SiteId, menuItem.PointId); ContentItem contentItem = await ContentEditorService.GetContentItemByIdAsync((int)menuItem.ContentItemId);
// content = await QDrantService.GetContentAsync(SiteId, menuItem.QdrantPointId); TODO
var selectedPoint = JsonConvert.DeserializeObject<QDrantGetContentPointResult>(content)!; /* content = await QDrantService.GetPointFromQdrantAsyncByPointId(SiteId, contentItem.Chunks); */
if (selectedPoint != null) //var selectedPoint = JsonConvert.DeserializeObject<QDrantGetContentPointResult>(content)!;
if (contentItem != null)
{ {
model.Content = selectedPoint.result.payload.content; foreach (var vector in contentItem.Chunks)
model.ContentDescription = selectedPoint.result.payload.description; {
Console.Write($"Found point: {selectedPoint.result.payload.content}"); //get vectors to compare
}
model.Content = contentItem.Content;
model.ContentDescription = contentItem.Description;
} }
} }
model.MenuItem = menuItem; model.MenuItem = menuItem;
extractedMenuItems.Add(model); extractedMenuItems.Add(model);
// UpdateMenuItem(model); // UpdateMenuItem(model);
} }
@ -162,7 +166,7 @@ else if (!string.IsNullOrEmpty(errorMessage))
try try
{ {
var prompt = $"Suggest a list of menu items for a website about: {subject}. Please do not attach any explanation."; var prompt = $"Suggest a list of menu items for a website about: {subject}. Please do not attach any explanation.";
var response = await ContentEditorService.GetMenuSuggestionsAsync(SessionId, prompt); var response = await ContentEditorAIService.GetMenuSuggestionsAsync(SessionId, prompt);
extractedMenuItems = response.Select(name => new MenuItemModel(name)).ToList(); extractedMenuItems = response.Select(name => new MenuItemModel(name)).ToList();
} }
catch (Exception ex) catch (Exception ex)
@ -208,7 +212,7 @@ else if (!string.IsNullOrEmpty(errorMessage))
private async Task SaveMenuItems(bool updateVectorDatabase) private async Task SaveMenuItems(bool updateVectorDatabase)
{ {
var result = await ContentEditorService.ProcessMenuItems(SiteId, hasCollection, extractedMenuItems, subject, MenuItemsSaved, updateVectorDatabase); var result = await ContentEditorAIService.ProcessMenuItems(SiteId, hasCollection, extractedMenuItems, subject, MenuItemsSaved, updateVectorDatabase);
if (result == "OK") if (result == "OK")
{ {
MenuItemsSaved = true; MenuItemsSaved = true;

View File

@ -0,0 +1,208 @@
@using BLAIzor.Models
@using BLAIzor.Services
@using Microsoft.AspNetCore.Components.Authorization
@inherits EditorUIComponentBase<ContentGroup>
@inject ContentEditorService contentEditorService
@inject NavigationManager navigation
@inject NotificationService notificationService
@inject DialogService dialogService
@attribute [Authorize]
@if (isLoading)
{
<p>Loading content groups...</p>
}
else
{
@foreach (var group in groups)
{
if (selectedGroupId == group.Id)
{
<EditContentGroup Group="group" OnContentGroupSaveClicked="SaveGroup" OnContentGroupDeleteClicked="DeleteGroup" OnManageContentItemClicked="ContentItemManagedCallback" OnForceReChunkClicked="Rechunk"></EditContentGroup>
}
else
{
<div class="rounded bg-panel p-3 mb-3 d-flex justify-content-between align-items-center">
<a style="width:100%" @onclick="() => SelectGroup(group.Id)">
<div style="width:100%; cursor:pointer;">
<strong>@group.Name</strong><br />
<small class="text-muted">@group.Type</small>
</div>
</a>
</div>
}
}
<button class="pointer bg-transparent border-0 p-0" style="width:100%; text-align: left;" @onclick="AddNewGroup">
<div class="mb-2 p-3 reference-button bg-panel-gradient-highlight pointer">
<div class="text-content">
<strong>Add new</strong>
<br />
<small class="text-muted">Add a new content group like "Blog", News, "Manuals" etc...</small>
</div>
<div class="icon-buttons">
<div class="icon-circle"><i class="fa-solid fa-plus text-white"></i></div>
</div>
</div>
</button>
}
@code {
[Parameter]
public int SiteInfoId { get; set; }
[Parameter] public Func<string, int, Task> OnManageContentItemClicked { get; set; }
private List<ContentGroup> groups = new();
private bool isLoading = true;
private int? selectedGroupId = null;
private async Task SelectGroup(int groupId)
{
selectedGroupId = groupId;
var group = groups.FirstOrDefault(g => g.Id == groupId);
if (group != null)
{
await NotifyContentEditStartedAsync(group);
}
}
private async Task DeselectGroup()
{
selectedGroupId = null;
StateHasChanged();
}
protected override async Task OnInitializedAsync()
{
isLoading = true;
groups = (await contentEditorService.GetContentGroupsBySiteInfoIdAsync(SiteInfoId))
.Where(g => g.SiteInfoId == SiteInfoId)
.OrderByDescending(g => g.LastUpdated)
.ToList();
isLoading = false;
}
private void EditGroup(ContentGroup group)
{
navigation.NavigateTo($"/content-group/{group.Id}/items");
}
private async Task Rechunk(ContentGroup group)
{
var confirmationResult = await dialogService.Confirm("Are you sure?", "Rechunk group", new ConfirmOptions() { OkButtonText = "Rechunk", CancelButtonText = "Oops, no" });
if (confirmationResult == true)
{
await ReallyRechunk(group);
}
else
{
//do nothing?
}
}
private async Task ReallyRechunk(ContentGroup group)
{
group.LastUpdated = DateTime.UtcNow;
var siteInfo = await contentEditorService.GetSiteInfoByIdAsync(group.SiteInfoId);
var result = await contentEditorService.ForceRechunkContentGroupAsync(group.Id, siteInfo.VectorCollectionName);
if (result != null)
{
await NotifyContentUpdatedAsync(group); // This triggers the base class logic
await DeselectGroup();
}
}
private async Task SaveGroup(ContentGroup group)
{
group.LastUpdated = DateTime.UtcNow;
group.Version = group.Version + 1;
var result = await contentEditorService.UpdateContentGroupByIdAsync(group);
if (result != null)
{
await NotifyContentUpdatedAsync(group); // This triggers the base class logic
await DeselectGroup();
}
}
private async Task DeleteGroup(ContentGroup group)
{
var confirmationResult = await dialogService.Confirm("Are you sure?", "Delete group", new ConfirmOptions() { OkButtonText = "Delete", CancelButtonText = "Oops, no" });
if (confirmationResult == true)
{
await ReallyDeleteGroup(group);
}
else
{
//do nothing?
}
}
private async Task ReallyDeleteGroup(ContentGroup group)
{
var result = await contentEditorService.DeleteContentGroupByIdAsync(group.Id); ;
groups.Remove(group);
var message = NotificationHelper.CreateNotificationMessage("Group deleted", 2, "Success", "ContentGroup deleted");
ShowNotification(message);
StateHasChanged();
}
private async Task AddNewGroup()
{
var newGroup = new ContentGroup
{
SiteInfoId = SiteInfoId,
Name = "New Group",
Slug = $"group-{DateTime.UtcNow.Ticks}",
Type = "manual",
VectorSize = 384,
EmbeddingModel = "default",
CreatedAt = DateTime.UtcNow,
LastUpdated = DateTime.UtcNow,
Version = 1
};
var result = await contentEditorService.CreateContentGroupAsync(newGroup);
if (result != null)
{
var message = NotificationHelper.CreateNotificationMessage("Group created", 2, "Success", "ContentGroup created");
ShowNotification(message);
groups.Insert(0, newGroup);
selectedGroupId = newGroup.Id;
}
}
private async Task ContentItemManagedCallback(string method, int itemId)
{
if (OnManageContentItemClicked != null)
await OnManageContentItemClicked.Invoke(method, itemId);
}
public void ShowNotification(NotificationMessage message)
{
notificationService.Notify(message);
}
RenderFragment GetMessage()
{
return __builder =>
{
<text>
Are <b>you</b> sure?
</text>
};
}
}

View File

@ -0,0 +1,152 @@

@attribute [Authorize]
@using BLAIzor.Components.Layout
@using BLAIzor.Models
@using BLAIzor.Services
@using Microsoft.AspNetCore.Components.Authorization
@layout AdminLayout
@inject ContentEditorService _contentEditorService
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject CustomAuthenticationStateProvider CustomAuthProvider
@inject NotificationService notificationService
@inject DialogService dialogService
<EditForm OnValidSubmit="SaveSiteInfo" EditContext="@EditContext">
<label>
Brand name
<InputText class="form-control my-3" id="siteName" @bind-Value="siteInfo.SiteName" />
</label>
<label>
Site description
<InputTextArea class="form-control my-3" style="height: 100px;" id="siteDescription" @bind-Value="siteInfo.SiteDescription" rows="3"/>
</label>
<label>
Logo url
<InputText class="form-control my-3" id="brandLogoUrl" @bind-Value="siteInfo.BrandLogoUrl" />
</label>
<label>
Default color (Do not bother with this)
<InputText class="form-control my-3" id="defaultColor" @bind-Value="siteInfo.DefaultColor" />
</label>
<label>
The Entity behind this website (eg. "company", "professional sportsman", "worldwide brand", etc);
<InputText class="form-control my-3" id="defaultColor" @bind-Value="siteInfo.Entity" />
</label>
<label>
Your agent Persona
<InputText class="form-control my-3" id="defaultColor" @bind-Value="siteInfo.Persona" />
</label>
<label>
Your domain url
<InputText class="form-control my-3" id="domainUrl" @bind-Value="siteInfo.DomainUrl" />
</label>
<label>
Chosen template
<InputNumber class="form-control my-3" id="templateId" @bind-Value="siteInfo.TemplateId" />
</label>
<label>
Published
<InputCheckbox class="my-3" id="ispublished" @bind-Value="siteInfo.IsPublished" />
</label>
<button class="btn btn-primary" type="submit">Save</button>
</EditForm>
<button class="btn btn-warning" type="button" @onclick="() => Rechunk(siteInfo)">ReChunk Site</button>
<hr />
<h4 class="mt-4">Forms linked to this site</h4>
@if (siteInfo.FormDefinitions?.Count > 0)
{
<ul class="list-group mb-3">
@foreach (var form in siteInfo.FormDefinitions)
{
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>
<strong>@form.Title</strong><br />
<small class="text-muted">@form.CreatedAt.ToString("yyyy-MM-dd HH:mm")</small>
</span>
<a class="btn btn-sm btn-outline-secondary" href="/form-editor/@form.Id">Edit</a>
</li>
}
</ul>
}
else
{
<p class="text-muted">No forms added yet.</p>
}
<a class="btn btn-success" href="/createform/@SiteId">
<i class="fas fa-plus-circle"></i> Add New Form
</a>
@code {
[Parameter]
public int SiteId { get; set; }
[Parameter] public Func<ContentGroup, Task> OnSiteInfoSaveClicked { get; set; }
[Parameter] public Func<string, Task> OnSiteNameChanged { get; set; }
[Parameter] public Func<string, int, Task> OnCancelItemClicked { get; set; }
private EditContext EditContext;
private SiteInfo siteInfo = new();
private string? userId;
private string? userName;
private AuthenticationState? authState;
protected override Task OnInitializedAsync()
{
EditContext = new EditContext(siteInfo);
EditContext.OnFieldChanged += EditContext_OnFieldChanged;
return base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync()
{
authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (authState.User.Identity?.IsAuthenticated == true)
{
userId = CustomAuthProvider.GetUserId();
userName = CustomAuthProvider.GetUserName();
}
siteInfo = await _contentEditorService.GetSiteInfoWithFormsByIdAsync(SiteId) ?? new SiteInfo();
}
private async Task SaveSiteInfo()
{
await _contentEditorService.UpdateSiteInfoAsync(siteInfo);
}
private async void EditContext_OnFieldChanged(object sender, FieldChangedEventArgs e)
{
if (e.FieldIdentifier.FieldName == "SiteName")
{
//invoke SiteNameChanged
if (OnSiteNameChanged != null)
await OnSiteNameChanged.Invoke(siteInfo.SiteName);
}
}
private async Task Rechunk(SiteInfo site)
{
var confirmationResult = await dialogService.Confirm("Are you sure?", "Rechunk site", new ConfirmOptions() { OkButtonText = "Rechunk", CancelButtonText = "Oops, no" });
if (confirmationResult == true)
{
await ReallyRechunk(site);
}
else
{
//do nothing?
}
}
private async Task ReallyRechunk(SiteInfo site)
{
await _contentEditorService.ForceRecreateQdrantCollectionAsync(site.Id);
}
}

View File

@ -2,6 +2,7 @@
@using BLAIzor.Services @using BLAIzor.Services
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@inject ContentEditorService ContentEditorService @inject ContentEditorService ContentEditorService
@inject ContentEditorAIService ContentEditorAIService
@inject AuthenticationStateProvider AuthenticationStateProvider @inject AuthenticationStateProvider AuthenticationStateProvider
@inject CustomAuthenticationStateProvider CustomAuthProvider @inject CustomAuthenticationStateProvider CustomAuthProvider
@ -72,7 +73,7 @@
{ {
Console.Write($"Subject: {Subject}"); Console.Write($"Subject: {Subject}");
var prompt = $"Generate website content for a page titled '{MenuItem.MenuItem.Name}' of a '{Subject}' website. Please do not attach any explanation."; var prompt = $"Generate website content for a page titled '{MenuItem.MenuItem.Name}' of a '{Subject}' website. Please do not attach any explanation.";
GeneratedContent = await ContentEditorService.GetGeneratedContentAsync(SessionId, prompt); GeneratedContent = await ContentEditorAIService.GetGeneratedContentAsync(SessionId, prompt);
MenuItem.Content = GeneratedContent; MenuItem.Content = GeneratedContent;
await OnContentUpdated.InvokeAsync(MenuItem); await OnContentUpdated.InvokeAsync(MenuItem);
} }
@ -93,7 +94,7 @@
{ {
Console.Write($"Subject: {Subject}"); Console.Write($"Subject: {Subject}");
var prompt = $"This is the provided text: {WordFile}. Look for the relevant content for a page titled '{MenuItem.MenuItem.Name}' of a '{Subject}' website. Please do not attach any explanation."; var prompt = $"This is the provided text: {WordFile}. Look for the relevant content for a page titled '{MenuItem.MenuItem.Name}' of a '{Subject}' website. Please do not attach any explanation.";
GeneratedContent = await ContentEditorService.GetGeneratedContentAsync(SessionId, prompt); GeneratedContent = await ContentEditorAIService.GetGeneratedContentAsync(SessionId, prompt);
MenuItem.Content = GeneratedContent; MenuItem.Content = GeneratedContent;
await OnContentUpdated.InvokeAsync(MenuItem); await OnContentUpdated.InvokeAsync(MenuItem);
} }

View File

@ -9,7 +9,7 @@
@* <nav class="navbar navbar-expand-lg bg-body-tertiary" style="z-index: 5"> *@ @* <nav class="navbar navbar-expand-lg bg-body-tertiary" style="z-index: 5"> *@
<nav class="navbar fixed-top" style="z-index: 5"> <nav class="navbar fixed-top" style="z-index: 10005">
@* <div class="container-fluid"> *@ @* <div class="container-fluid"> *@
<div style="--bs-gutter-x: 1.5rem; --bs-gutter-y: 0; width: 100%; padding-right: calc(var(--bs-gutter-x) * .5); padding-left: calc(var(--bs-gutter-x) * .5); margin-right: auto; margin-left: auto;"> <div style="--bs-gutter-x: 1.5rem; --bs-gutter-y: 0; width: 100%; padding-right: calc(var(--bs-gutter-x) * .5); padding-left: calc(var(--bs-gutter-x) * .5); margin-right: auto; margin-left: auto;">
@{ @{

View File

@ -0,0 +1,150 @@
@using BLAIzor.Models
@using BLAIzor.Services
@inject ScopedContentService _scopedContentService;
@inject NavigationManager _navigationManager;
@inject ContentEditorService _contentEditorService
@inject IHttpContextAccessor HttpContextAccessor
@inject IJSRuntime JS
<nav class="navbar fixed-top navbar-expand-lg" style="z-index: 10005">
<div class="container-fluid">
<NavLink class="navbar-brand" @onclick="HomeClick">@BrandName</NavLink>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<i class="fa-solid fa-bars"></i>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
@{
if (Menu != null)
{
// foreach (var menuItem in Menu)
// {
// <li class="nav-item">
// <a class="nav-link" aria-current="page" @onclick="()=>MenuClickedAsync(menuItem.Name)">@menuItem.Name</a>
// </li>
// }
foreach (var menuItem in Menu.Where(m => m.ParentId == null).OrderBy(m => m.SortOrder))
{
if (menuItem.Children != null && menuItem.Children.Any())
{
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" role="button"
data-bs-toggle="dropdown" aria-expanded="false">
@menuItem.Name
</a>
<ul class="dropdown-menu">
@foreach (var child in menuItem.Children.OrderBy(c => c.SortOrder))
{
<li>
<a class="dropdown-item" @onclick="() => MenuClickedAsync(child.Name)">
@child.Name
</a>
</li>
}
</ul>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link" @onclick="() => MenuClickedAsync(menuItem.Name)">
@menuItem.Name
</a>
</li>
}
}
}
}
@* <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Dropdown
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</li> *@
</ul>
<select @onchange="OnLanguageSelected" class="form-select" style="width:100px">
<option value="">Lang</option>
@foreach (var Language in Languages)
{
<option value="@Language">@Language</option>
}
</select>
</div>
</div>
</nav>
@code {
public int SiteId;
[Parameter]
public List<MenuItem> Menu { get; set; }
[Parameter] public string? BrandName { get; set; } = "BLAIzor";
//public string[] MenuList;
public string videoUrl = "";
public string SelectedLanguage;
private int RenderCounter = 0;
//public static event Action<string>? OnMenuClicked;
[Parameter] public Action<string>? OnMenuClicked { get; set; }
public List<string> Languages = new List<string>
{
"Hungarian", "English", "German"
};
public async Task MenuClickedAsync(string menuName)
{
OnMenuClicked?.Invoke(menuName);
await JS.InvokeVoidAsync("collapseNavbar");
}
public void HomeClick()
{
_navigationManager.Refresh(true);
}
protected override async Task OnInitializedAsync()
{
var lang = await JS.InvokeAsync<string>("getUserLanguage");
// Normalize and match to one of your supported languages
if (lang.StartsWith("hu", StringComparison.OrdinalIgnoreCase))
SelectedLanguage = "Hungarian";
else if (lang.StartsWith("de", StringComparison.OrdinalIgnoreCase))
SelectedLanguage = "German";
else
SelectedLanguage = "English";
_scopedContentService.SelectedLanguage = SelectedLanguage;
}
protected override async Task OnParametersSetAsync()
{
if (Menu != null)
{
//
}
await base.OnParametersSetAsync();
}
public void OnLanguageSelected(ChangeEventArgs e)
{
SelectedLanguage = e.Value?.ToString();
if (!string.IsNullOrEmpty(SelectedLanguage))
{
Console.WriteLine($"Language selected: {SelectedLanguage}");
_scopedContentService.SelectedLanguage = SelectedLanguage;
}
}
}

View File

@ -1,4 +1,5 @@
<style> @using BLAIzor.Models
<style>
.parent-element-to-video { .parent-element-to-video {
overflow: hidden; overflow: hidden;
width: 120vw; width: 120vw;
@ -16,8 +17,8 @@
</style> </style>
<div class="parent-element-to-video"> <div class="parent-element-to-video">
<video style="opacity:0.3; z-index: -1" autoplay muted loop id="myVideo" @key="SelectedBrandName"> <video style="opacity:0.3; z-index: -1" autoplay muted loop id="myVideo" @key="site.BackgroundVideo">
<source src="@("/video/" + SelectedBrandName + ".mp4")" type="video/mp4"> <source src="@(site.BackgroundVideo)" type="video/mp4">
</video> </video>
</div> </div>
@ -34,5 +35,5 @@
@code { @code {
[Parameter] [Parameter]
public string SelectedBrandName { get; set; } = "default"; public SiteInfo site { get; set; } = new SiteInfo();
} }

View File

@ -15,10 +15,14 @@ namespace BLAIzor.Data
public DbSet<SiteInfo> SiteInfos { get; set; } public DbSet<SiteInfo> SiteInfos { get; set; }
public DbSet<MenuItem> MenuItems { get; set; } // Add MenuItem DbSet public DbSet<MenuItem> MenuItems { get; set; } // Add MenuItem DbSet
public DbSet<DesignTemplate> DesignTemplates { get; set; } public DbSet<DesignTemplate> DesignTemplates { get; set; }
public DbSet<CssTemplate> CssTemplates { get; set; } public DbSet<CssTemplate> CssTemplates { get; set; }
public DbSet<ContentGroup> ContentGroups { get; set; }
public DbSet<ContentItem> ContentItems { get; set; }
public DbSet<ContentChunk> ContentChunks { get; set; }
public DbSet<FormDefinition> FormDefinitions { get; set; } public DbSet<FormDefinition> FormDefinitions { get; set; }
public DbSet<AppLog> Logs { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
@ -90,6 +94,36 @@ namespace BLAIzor.Data
.HasForeignKey(m => m.SiteInfoId) .HasForeignKey(m => m.SiteInfoId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MenuItem>()
.HasOne(m => m.ContentGroup)
.WithMany()
.HasForeignKey(m => m.ContentGroupId)
.OnDelete(DeleteBehavior.NoAction);
modelBuilder.Entity<MenuItem>()
.HasOne(m => m.ContentItem)
.WithMany()
.HasForeignKey(m => m.ContentItemId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<MenuItem>()
.HasOne(m => m.Parent)
.WithMany(p => p.Children)
.HasForeignKey(m => m.ParentId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<ContentItem>()
.HasOne(ci => ci.ContentGroup)
.WithMany(cg => cg.Items)
.HasForeignKey(ci => ci.ContentGroupId)
.OnDelete(DeleteBehavior.Cascade); // When a ContentGroup is deleted, delete its ContentItems
modelBuilder.Entity<ContentChunk>()
.HasOne(cc => cc.ContentItem)
.WithMany(ci => ci.Chunks)
.HasForeignKey(cc => cc.ContentItemId)
.OnDelete(DeleteBehavior.Cascade); // When a ContentItem is deleted, delete its Chunks
modelBuilder.Entity<DesignTemplate>() modelBuilder.Entity<DesignTemplate>()
.HasOne(s => s.User) .HasOne(s => s.User)
.WithMany() .WithMany()
@ -103,10 +137,10 @@ namespace BLAIzor.Data
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<FormDefinition>() modelBuilder.Entity<FormDefinition>()
.HasOne(fd => fd.SiteInfo) .HasOne(fd => fd.SiteInfo)
.WithMany(s => s.FormDefinitions) .WithMany(s => s.FormDefinitions)
.HasForeignKey(fd => fd.SiteInfoId) .HasForeignKey(fd => fd.SiteInfoId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<FormDefinition>() modelBuilder.Entity<FormDefinition>()
.HasIndex(fd => new { fd.SiteInfoId, fd.Slug }) .HasIndex(fd => new { fd.SiteInfoId, fd.Slug })

130
Helpers/ChunkingHelper.cs Normal file
View File

@ -0,0 +1,130 @@
using BLAIzor.Models;
using System.Text;
namespace BLAIzor.Helpers
{
public static class ChunkingHelper
{
private static readonly string[] SectionDelimiters =
{
"\n##", "\n###", "\nSection:", "\nTopic:", "\nTitle:", "\n---", "[[CHUNK_BREAK]]", "### Logical Break ###"
};
public static List<string> SplitStructuredText(string content, int maxChunkCharLength = 3000)
{
if (string.IsNullOrWhiteSpace(content))
return new List<string>();
// Normalize newlines
content = content.Replace("\r\n", "\n").Trim();
// Step 1: Split by known logical boundaries
var logicalParts = SplitByDelimiters(content, SectionDelimiters);
// Step 2: Recombine smaller logical parts into full-size chunks
var chunks = new List<string>();
var currentChunk = new StringBuilder();
foreach (var part in logicalParts)
{
if (currentChunk.Length + part.Length + 2 <= maxChunkCharLength)
{
currentChunk.AppendLine(part);
currentChunk.AppendLine();
}
else
{
if (currentChunk.Length > 0)
{
chunks.Add(currentChunk.ToString().Trim());
currentChunk.Clear();
}
if (part.Length > maxChunkCharLength)
{
chunks.AddRange(SplitLongParagraph(part, maxChunkCharLength));
}
else
{
currentChunk.AppendLine(part);
currentChunk.AppendLine();
}
}
}
if (currentChunk.Length > 0)
chunks.Add(currentChunk.ToString().Trim());
return chunks;
}
private static List<string> SplitByDelimiters(string content, string[] delimiters)
{
var parts = new List<string>();
var remaining = content;
foreach (var delimiter in delimiters)
{
remaining = remaining.Replace(delimiter, "\n[[SECTION_BREAK]]");
}
return remaining.Split(new[] { "[[SECTION_BREAK]]" }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.Where(s => !string.IsNullOrWhiteSpace(s))
.ToList();
}
private static List<string> SplitLongParagraph(string text, int maxLength)
{
var chunks = new List<string>();
int index = 0;
while (index < text.Length)
{
int len = Math.Min(maxLength, text.Length - index);
chunks.Add(text.Substring(index, len));
index += len;
}
return chunks;
}
/// <summary>
/// Checks whether the Qdrant-based chunks match the original ContentItem text.
/// </summary>
/// <param name="item">The ContentItem to check.</param>
/// <param name="chunks">The SQL chunks that reference Qdrant IDs.</param>
/// <param name="vectorData">All related WebPageContent objects pulled from Qdrant.</param>
public static bool IsChunkingConsistent(
ContentItem item,
List<ContentChunk> chunks,
List<WebPageContent> vectorData)
{
if (item == null || string.IsNullOrWhiteSpace(item.Content) || chunks == null || vectorData == null)
return false;
var orderedChunks = chunks.OrderBy(c => c.ChunkIndex).ToList();
var combinedText = string.Join("",
orderedChunks.Select(chunk =>
vectorData.FirstOrDefault(v => v.UId == chunk.QdrantPointId)?.Content?.Trim() ?? ""
));
var original = NormalizeText(item.Content);
var reassembled = NormalizeText(combinedText);
return original == reassembled;
}
private static string NormalizeText(string text)
{
return text.Replace("\r", "")
.Replace("\n", "")
.Replace(" ", "")
.Trim()
.ToLowerInvariant();
}
}
}

50
Helpers/Notification.cs Normal file
View File

@ -0,0 +1,50 @@
using System;
using System.Text.RegularExpressions;
using System.Text;
using System.Collections.Generic;
using Radzen;
public static class NotificationHelper
{
// Special character replacement map
public static NotificationMessage CreateNotificationMessage(string messageText, int severity, string summary, string detail, int duration = 4000, bool showProgress = true, Action<Radzen.NotificationMessage> callBack = null)
{
NotificationSeverity messageSeverity;
switch (severity)
{
case 0:
messageSeverity = NotificationSeverity.Error;
break;
case 1:
messageSeverity = NotificationSeverity.Info;
break;
case 2:
messageSeverity = NotificationSeverity.Success;
break;
case 3:
messageSeverity = NotificationSeverity.Warning;
break;
default:
messageSeverity = NotificationSeverity.Info;
break;
}
var message = new NotificationMessage
{
//error 0, info 1, success 2, warning 3
Severity = messageSeverity,
Summary = summary,
Detail = detail,
Duration = duration,
ShowProgress = showProgress,
Click = callBack,
CloseOnClick = true,
Payload = DateTime.Now
};
return message;
}
}

View File

@ -150,6 +150,10 @@ public static class TextHelper
{ {
aiResponse = aiResponse.Substring(4); aiResponse = aiResponse.Substring(4);
} }
if (aiResponse.StartsWith("html"))
{
aiResponse = aiResponse.Substring(4);
}
aiResponse = aiResponse.Substring(0, aiResponse.Length - 3); aiResponse = aiResponse.Substring(0, aiResponse.Length - 3);
} }

View File

@ -0,0 +1,48 @@
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace BLAIzor.Helpers
{
public static class VectorHashHelper
{
/// <summary>
/// Computes a SHA-256 hash for a float vector and returns it as a Base64 string.
/// </summary>
public static string ComputeVectorHash(float[] vector)
{
if (vector == null || vector.Length == 0)
throw new ArgumentException("Vector cannot be null or empty.");
// Convert float array to byte array
byte[] byteArray = new byte[vector.Length * sizeof(float)];
Buffer.BlockCopy(vector, 0, byteArray, 0, byteArray.Length);
using (var sha = SHA256.Create())
{
byte[] hashBytes = sha.ComputeHash(byteArray);
return Convert.ToBase64String(hashBytes);
}
}
/// <summary>
/// Compares two float vectors by computing and comparing their SHA-256 hashes.
/// </summary>
public static bool AreVectorsEqual(float[] vector1, float[] vector2)
{
if (vector1 == null || vector2 == null)
return false;
if (vector1.Length != vector2.Length)
return false;
string hash1 = ComputeVectorHash(vector1);
string hash2 = ComputeVectorHash(vector2);
return hash1 == hash2;
}
}
}

View File

@ -0,0 +1,841 @@
// <auto-generated />
using System;
using BLAIzor.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace BLAIzor.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250617221435_CollectionAdministrationUpdate")]
partial class CollectionAdministrationUpdate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("BLAIzor.Models.ContentChunk", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("ChunkIndex")
.HasColumnType("int");
b.Property<int>("ContentItemId")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("QdrantPointId")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("VectorHash")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("ContentItemId");
b.ToTable("ContentChunks");
});
modelBuilder.Entity("BLAIzor.Models.ContentGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("EmbeddingModel")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime2");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("SiteInfoId")
.HasColumnType("int");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("VectorSize")
.HasColumnType("int");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("SiteInfoId");
b.ToTable("ContentGroups");
});
modelBuilder.Entity("BLAIzor.Models.ContentItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Content")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("ContentGroupId")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<string>("Language")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime2");
b.Property<string>("Tags")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ContentGroupId");
b.ToTable("ContentItems");
});
modelBuilder.Entity("BLAIzor.Models.CssTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("CssContent")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("DesignTemplateId")
.HasColumnType("int");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("DesignTemplateId")
.IsUnique();
b.ToTable("CssTemplates");
});
modelBuilder.Entity("BLAIzor.Models.DesignTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsDeprecated")
.HasColumnType("bit");
b.Property<bool>("IsPrivate")
.HasColumnType("bit");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<string>("QDrandCollectionName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Tags")
.HasColumnType("nvarchar(max)");
b.Property<string>("TemplateName")
.HasColumnType("nvarchar(max)");
b.Property<string>("TemplatePhotoUrl")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("DesignTemplates");
b.HasData(
new
{
Id = 1,
CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "The default template",
IsDeprecated = false,
IsPrivate = false,
IsPublished = false,
QDrandCollectionName = "html_snippets",
Status = "Draft",
Tags = "system",
TemplateName = "Default Site",
TemplatePhotoUrl = "/images/default-logo.png",
UserId = "0988758e-e16c-4c2c-8c1e-efa3ac5f0274",
Version = 1
});
});
modelBuilder.Entity("BLAIzor.Models.FormDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<string>("JsonDefinition")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("SiteInfoId")
.HasColumnType("int");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("SiteInfoId", "Slug")
.IsUnique();
b.ToTable("FormDefinitions");
});
modelBuilder.Entity("BLAIzor.Models.MenuItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int?>("ContentGroupId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int?>("ParentId")
.HasColumnType("int");
b.Property<bool>("ShowInMainMenu")
.HasColumnType("bit");
b.Property<int>("SiteInfoId")
.HasColumnType("int");
b.Property<string>("Slug")
.HasColumnType("nvarchar(max)");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<string>("StoredHtml")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("ContentGroupId");
b.HasIndex("ParentId");
b.HasIndex("SiteInfoId");
b.ToTable("MenuItems");
});
modelBuilder.Entity("BLAIzor.Models.SiteInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("BackgroundVideo")
.HasColumnType("nvarchar(max)");
b.Property<string>("BrandLogoUrl")
.HasColumnType("nvarchar(max)");
b.Property<string>("DefaultColor")
.HasColumnType("nvarchar(max)");
b.Property<string>("DefaultLanguage")
.HasColumnType("nvarchar(max)");
b.Property<string>("DefaultUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("DomainUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("EmbeddingService")
.HasColumnType("nvarchar(max)");
b.Property<string>("Entity")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<string>("Persona")
.HasColumnType("nvarchar(max)");
b.Property<bool>("STTActive")
.HasColumnType("bit");
b.Property<string>("SiteDescription")
.HasColumnType("nvarchar(max)");
b.Property<string>("SiteName")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TTSActive")
.HasColumnType("bit");
b.Property<int?>("TemplateId")
.HasColumnType("int");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<string>("VoiceId")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("TemplateId");
b.HasIndex("UserId");
b.ToTable("SiteInfos");
b.HasData(
new
{
Id = 1,
BrandLogoUrl = "/images/default-logo.png",
DefaultColor = "#FFFFFF",
DefaultUrl = "https://ai.poppixel.cloud",
DomainUrl = "poppixel.cloud",
IsPublished = false,
STTActive = false,
SiteName = "Default Site",
TTSActive = false,
TemplateId = 1,
UserId = "0988758e-e16c-4c2c-8c1e-efa3ac5f0274"
});
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("AspNetUsers", (string)null);
b.HasData(
new
{
Id = "0988758e-e16c-4c2c-8c1e-efa3ac5f0274",
AccessFailedCount = 0,
ConcurrencyStamp = "a2836246-0303-4370-b283-e53a9a3f2813",
Email = "adam.g@aycode.com",
EmailConfirmed = true,
LockoutEnabled = false,
NormalizedEmail = "ADAM.G@AYCODE.COM",
NormalizedUserName = "ADAM.G@AYCODE.COM",
PasswordHash = "AQAAAAIAAYagAAAAEChxKCu+ReGvcZFR/6kPASbpnQdMp1MJuepeRyR4bfHTkUk8SfNAqmckGXvuw+GaGA==",
PhoneNumberConfirmed = false,
SecurityStamp = "7ecf121a-b0e7-4e30-a1f1-299eeaf0a9cc",
TwoFactorEnabled = false,
UserName = "adam.g@aycode.com"
});
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ProviderKey")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("RoleId")
.HasColumnType("nvarchar(450)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Name")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("BLAIzor.Models.ContentChunk", b =>
{
b.HasOne("BLAIzor.Models.ContentItem", "ContentItem")
.WithMany("Chunks")
.HasForeignKey("ContentItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ContentItem");
});
modelBuilder.Entity("BLAIzor.Models.ContentGroup", b =>
{
b.HasOne("BLAIzor.Models.SiteInfo", "SiteInfo")
.WithMany()
.HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SiteInfo");
});
modelBuilder.Entity("BLAIzor.Models.ContentItem", b =>
{
b.HasOne("BLAIzor.Models.ContentGroup", "ContentGroup")
.WithMany("Items")
.HasForeignKey("ContentGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ContentGroup");
});
modelBuilder.Entity("BLAIzor.Models.CssTemplate", b =>
{
b.HasOne("BLAIzor.Models.DesignTemplate", "DesignTemplate")
.WithOne("CssTemplate")
.HasForeignKey("BLAIzor.Models.CssTemplate", "DesignTemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DesignTemplate");
});
modelBuilder.Entity("BLAIzor.Models.DesignTemplate", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("BLAIzor.Models.FormDefinition", b =>
{
b.HasOne("BLAIzor.Models.SiteInfo", "SiteInfo")
.WithMany("FormDefinitions")
.HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SiteInfo");
});
modelBuilder.Entity("BLAIzor.Models.MenuItem", b =>
{
b.HasOne("BLAIzor.Models.ContentGroup", "ContentGroup")
.WithMany()
.HasForeignKey("ContentGroupId")
.OnDelete(DeleteBehavior.NoAction);
b.HasOne("BLAIzor.Models.MenuItem", "Parent")
.WithMany("Children")
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("BLAIzor.Models.SiteInfo", "SiteInfo")
.WithMany("MenuItems")
.HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ContentGroup");
b.Navigation("Parent");
b.Navigation("SiteInfo");
});
modelBuilder.Entity("BLAIzor.Models.SiteInfo", b =>
{
b.HasOne("BLAIzor.Models.DesignTemplate", "Template")
.WithMany("Sites")
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Template");
b.Navigation("User");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("BLAIzor.Models.ContentGroup", b =>
{
b.Navigation("Items");
});
modelBuilder.Entity("BLAIzor.Models.ContentItem", b =>
{
b.Navigation("Chunks");
});
modelBuilder.Entity("BLAIzor.Models.DesignTemplate", b =>
{
b.Navigation("CssTemplate")
.IsRequired();
b.Navigation("Sites");
});
modelBuilder.Entity("BLAIzor.Models.MenuItem", b =>
{
b.Navigation("Children");
});
modelBuilder.Entity("BLAIzor.Models.SiteInfo", b =>
{
b.Navigation("FormDefinitions");
b.Navigation("MenuItems");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,214 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BLAIzor.Migrations
{
/// <inheritdoc />
public partial class CollectionAdministrationUpdate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PointId",
table: "MenuItems");
migrationBuilder.DropColumn(
name: "QdrantPointId",
table: "MenuItems");
migrationBuilder.AddColumn<int>(
name: "ContentGroupId",
table: "MenuItems",
type: "int",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "ParentId",
table: "MenuItems",
type: "int",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Slug",
table: "MenuItems",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.CreateTable(
name: "ContentGroups",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
SiteInfoId = table.Column<int>(type: "int", nullable: false),
Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
Slug = table.Column<string>(type: "nvarchar(max)", nullable: false),
Type = table.Column<string>(type: "nvarchar(max)", nullable: false),
VectorSize = table.Column<int>(type: "int", nullable: false),
EmbeddingModel = table.Column<string>(type: "nvarchar(max)", nullable: false),
Version = table.Column<int>(type: "int", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
LastUpdated = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ContentGroups", x => x.Id);
table.ForeignKey(
name: "FK_ContentGroups_SiteInfos_SiteInfoId",
column: x => x.SiteInfoId,
principalTable: "SiteInfos",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ContentItems",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
ContentGroupId = table.Column<int>(type: "int", nullable: false),
Title = table.Column<string>(type: "nvarchar(max)", nullable: false),
Description = table.Column<string>(type: "nvarchar(max)", nullable: false),
Content = table.Column<string>(type: "nvarchar(max)", nullable: false),
Language = table.Column<string>(type: "nvarchar(max)", nullable: false),
Tags = table.Column<string>(type: "nvarchar(max)", nullable: false),
IsPublished = table.Column<bool>(type: "bit", nullable: false),
Version = table.Column<int>(type: "int", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
LastUpdated = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ContentItems", x => x.Id);
table.ForeignKey(
name: "FK_ContentItems_ContentGroups_ContentGroupId",
column: x => x.ContentGroupId,
principalTable: "ContentGroups",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ContentChunks",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
ContentItemId = table.Column<int>(type: "int", nullable: false),
QdrantPointId = table.Column<string>(type: "nvarchar(max)", nullable: false),
ChunkIndex = table.Column<int>(type: "int", nullable: false),
VectorHash = table.Column<string>(type: "nvarchar(max)", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ContentChunks", x => x.Id);
table.ForeignKey(
name: "FK_ContentChunks_ContentItems_ContentItemId",
column: x => x.ContentItemId,
principalTable: "ContentItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_MenuItems_ContentGroupId",
table: "MenuItems",
column: "ContentGroupId");
migrationBuilder.CreateIndex(
name: "IX_MenuItems_ParentId",
table: "MenuItems",
column: "ParentId");
migrationBuilder.CreateIndex(
name: "IX_ContentChunks_ContentItemId",
table: "ContentChunks",
column: "ContentItemId");
migrationBuilder.CreateIndex(
name: "IX_ContentGroups_SiteInfoId",
table: "ContentGroups",
column: "SiteInfoId");
migrationBuilder.CreateIndex(
name: "IX_ContentItems_ContentGroupId",
table: "ContentItems",
column: "ContentGroupId");
migrationBuilder.AddForeignKey(
name: "FK_MenuItems_ContentGroups_ContentGroupId",
table: "MenuItems",
column: "ContentGroupId",
principalTable: "ContentGroups",
principalColumn: "Id",
onDelete: ReferentialAction.NoAction);
migrationBuilder.AddForeignKey(
name: "FK_MenuItems_MenuItems_ParentId",
table: "MenuItems",
column: "ParentId",
principalTable: "MenuItems",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_MenuItems_ContentGroups_ContentGroupId",
table: "MenuItems");
migrationBuilder.DropForeignKey(
name: "FK_MenuItems_MenuItems_ParentId",
table: "MenuItems");
migrationBuilder.DropTable(
name: "ContentChunks");
migrationBuilder.DropTable(
name: "ContentItems");
migrationBuilder.DropTable(
name: "ContentGroups");
migrationBuilder.DropIndex(
name: "IX_MenuItems_ContentGroupId",
table: "MenuItems");
migrationBuilder.DropIndex(
name: "IX_MenuItems_ParentId",
table: "MenuItems");
migrationBuilder.DropColumn(
name: "ContentGroupId",
table: "MenuItems");
migrationBuilder.DropColumn(
name: "ParentId",
table: "MenuItems");
migrationBuilder.DropColumn(
name: "Slug",
table: "MenuItems");
migrationBuilder.AddColumn<int>(
name: "PointId",
table: "MenuItems",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<Guid>(
name: "QdrantPointId",
table: "MenuItems",
type: "uniqueidentifier",
nullable: true);
}
}
}

View File

@ -0,0 +1,841 @@
// <auto-generated />
using System;
using BLAIzor.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace BLAIzor.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250617224307_CollectionAdministrationUpdateFIX")]
partial class CollectionAdministrationUpdateFIX
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("BLAIzor.Models.ContentChunk", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("ChunkIndex")
.HasColumnType("int");
b.Property<int>("ContentItemId")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("QdrantPointId")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("VectorHash")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("ContentItemId");
b.ToTable("ContentChunks");
});
modelBuilder.Entity("BLAIzor.Models.ContentGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("EmbeddingModel")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime2");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("SiteInfoId")
.HasColumnType("int");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("VectorSize")
.HasColumnType("int");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("SiteInfoId");
b.ToTable("ContentGroups");
});
modelBuilder.Entity("BLAIzor.Models.ContentItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Content")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("ContentGroupId")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<string>("Language")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime2");
b.Property<string>("Tags")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ContentGroupId");
b.ToTable("ContentItems");
});
modelBuilder.Entity("BLAIzor.Models.CssTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("CssContent")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("DesignTemplateId")
.HasColumnType("int");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("DesignTemplateId")
.IsUnique();
b.ToTable("CssTemplates");
});
modelBuilder.Entity("BLAIzor.Models.DesignTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsDeprecated")
.HasColumnType("bit");
b.Property<bool>("IsPrivate")
.HasColumnType("bit");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<string>("QDrandCollectionName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Tags")
.HasColumnType("nvarchar(max)");
b.Property<string>("TemplateName")
.HasColumnType("nvarchar(max)");
b.Property<string>("TemplatePhotoUrl")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("DesignTemplates");
b.HasData(
new
{
Id = 1,
CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "The default template",
IsDeprecated = false,
IsPrivate = false,
IsPublished = false,
QDrandCollectionName = "html_snippets",
Status = "Draft",
Tags = "system",
TemplateName = "Default Site",
TemplatePhotoUrl = "/images/default-logo.png",
UserId = "0988758e-e16c-4c2c-8c1e-efa3ac5f0274",
Version = 1
});
});
modelBuilder.Entity("BLAIzor.Models.FormDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<string>("JsonDefinition")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("SiteInfoId")
.HasColumnType("int");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("SiteInfoId", "Slug")
.IsUnique();
b.ToTable("FormDefinitions");
});
modelBuilder.Entity("BLAIzor.Models.MenuItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int?>("ContentGroupId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int?>("ParentId")
.HasColumnType("int");
b.Property<bool>("ShowInMainMenu")
.HasColumnType("bit");
b.Property<int>("SiteInfoId")
.HasColumnType("int");
b.Property<string>("Slug")
.HasColumnType("nvarchar(max)");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<string>("StoredHtml")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("ContentGroupId");
b.HasIndex("ParentId");
b.HasIndex("SiteInfoId");
b.ToTable("MenuItems");
});
modelBuilder.Entity("BLAIzor.Models.SiteInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("BackgroundVideo")
.HasColumnType("nvarchar(max)");
b.Property<string>("BrandLogoUrl")
.HasColumnType("nvarchar(max)");
b.Property<string>("DefaultColor")
.HasColumnType("nvarchar(max)");
b.Property<string>("DefaultLanguage")
.HasColumnType("nvarchar(max)");
b.Property<string>("DefaultUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("DomainUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("EmbeddingService")
.HasColumnType("nvarchar(max)");
b.Property<string>("Entity")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<string>("Persona")
.HasColumnType("nvarchar(max)");
b.Property<bool>("STTActive")
.HasColumnType("bit");
b.Property<string>("SiteDescription")
.HasColumnType("nvarchar(max)");
b.Property<string>("SiteName")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TTSActive")
.HasColumnType("bit");
b.Property<int?>("TemplateId")
.HasColumnType("int");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<string>("VoiceId")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("TemplateId");
b.HasIndex("UserId");
b.ToTable("SiteInfos");
b.HasData(
new
{
Id = 1,
BrandLogoUrl = "/images/default-logo.png",
DefaultColor = "#FFFFFF",
DefaultUrl = "https://ai.poppixel.cloud",
DomainUrl = "poppixel.cloud",
IsPublished = false,
STTActive = false,
SiteName = "Default Site",
TTSActive = false,
TemplateId = 1,
UserId = "0988758e-e16c-4c2c-8c1e-efa3ac5f0274"
});
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("AspNetUsers", (string)null);
b.HasData(
new
{
Id = "0988758e-e16c-4c2c-8c1e-efa3ac5f0274",
AccessFailedCount = 0,
ConcurrencyStamp = "a2836246-0303-4370-b283-e53a9a3f2813",
Email = "adam.g@aycode.com",
EmailConfirmed = true,
LockoutEnabled = false,
NormalizedEmail = "ADAM.G@AYCODE.COM",
NormalizedUserName = "ADAM.G@AYCODE.COM",
PasswordHash = "AQAAAAIAAYagAAAAEChxKCu+ReGvcZFR/6kPASbpnQdMp1MJuepeRyR4bfHTkUk8SfNAqmckGXvuw+GaGA==",
PhoneNumberConfirmed = false,
SecurityStamp = "7ecf121a-b0e7-4e30-a1f1-299eeaf0a9cc",
TwoFactorEnabled = false,
UserName = "adam.g@aycode.com"
});
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ProviderKey")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("RoleId")
.HasColumnType("nvarchar(450)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Name")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("BLAIzor.Models.ContentChunk", b =>
{
b.HasOne("BLAIzor.Models.ContentItem", "ContentItem")
.WithMany("Chunks")
.HasForeignKey("ContentItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ContentItem");
});
modelBuilder.Entity("BLAIzor.Models.ContentGroup", b =>
{
b.HasOne("BLAIzor.Models.SiteInfo", "SiteInfo")
.WithMany()
.HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SiteInfo");
});
modelBuilder.Entity("BLAIzor.Models.ContentItem", b =>
{
b.HasOne("BLAIzor.Models.ContentGroup", "ContentGroup")
.WithMany("Items")
.HasForeignKey("ContentGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ContentGroup");
});
modelBuilder.Entity("BLAIzor.Models.CssTemplate", b =>
{
b.HasOne("BLAIzor.Models.DesignTemplate", "DesignTemplate")
.WithOne("CssTemplate")
.HasForeignKey("BLAIzor.Models.CssTemplate", "DesignTemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DesignTemplate");
});
modelBuilder.Entity("BLAIzor.Models.DesignTemplate", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("BLAIzor.Models.FormDefinition", b =>
{
b.HasOne("BLAIzor.Models.SiteInfo", "SiteInfo")
.WithMany("FormDefinitions")
.HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SiteInfo");
});
modelBuilder.Entity("BLAIzor.Models.MenuItem", b =>
{
b.HasOne("BLAIzor.Models.ContentGroup", "ContentGroup")
.WithMany()
.HasForeignKey("ContentGroupId")
.OnDelete(DeleteBehavior.NoAction);
b.HasOne("BLAIzor.Models.MenuItem", "Parent")
.WithMany("Children")
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("BLAIzor.Models.SiteInfo", "SiteInfo")
.WithMany("MenuItems")
.HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ContentGroup");
b.Navigation("Parent");
b.Navigation("SiteInfo");
});
modelBuilder.Entity("BLAIzor.Models.SiteInfo", b =>
{
b.HasOne("BLAIzor.Models.DesignTemplate", "Template")
.WithMany("Sites")
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Template");
b.Navigation("User");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("BLAIzor.Models.ContentGroup", b =>
{
b.Navigation("Items");
});
modelBuilder.Entity("BLAIzor.Models.ContentItem", b =>
{
b.Navigation("Chunks");
});
modelBuilder.Entity("BLAIzor.Models.DesignTemplate", b =>
{
b.Navigation("CssTemplate")
.IsRequired();
b.Navigation("Sites");
});
modelBuilder.Entity("BLAIzor.Models.MenuItem", b =>
{
b.Navigation("Children");
});
modelBuilder.Entity("BLAIzor.Models.SiteInfo", b =>
{
b.Navigation("FormDefinitions");
b.Navigation("MenuItems");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,41 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BLAIzor.Migrations
{
/// <inheritdoc />
public partial class CollectionAdministrationUpdateFIX : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_MenuItems_ContentGroups_ContentGroupId",
table: "MenuItems");
migrationBuilder.AddForeignKey(
name: "FK_MenuItems_ContentGroups_ContentGroupId",
table: "MenuItems",
column: "ContentGroupId",
principalTable: "ContentGroups",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_MenuItems_ContentGroups_ContentGroupId",
table: "MenuItems");
migrationBuilder.AddForeignKey(
name: "FK_MenuItems_ContentGroups_ContentGroupId",
table: "MenuItems",
column: "ContentGroupId",
principalTable: "ContentGroups",
principalColumn: "Id",
onDelete: ReferentialAction.NoAction);
}
}
}

View File

@ -0,0 +1,855 @@
// <auto-generated />
using System;
using BLAIzor.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace BLAIzor.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250618105221_CollectionAdministrationUpdate2")]
partial class CollectionAdministrationUpdate2
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("BLAIzor.Models.ContentChunk", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("ChunkIndex")
.HasColumnType("int");
b.Property<int>("ContentItemId")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("QdrantPointId")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("VectorHash")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("ContentItemId");
b.ToTable("ContentChunks");
});
modelBuilder.Entity("BLAIzor.Models.ContentGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("EmbeddingModel")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime2");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("SiteInfoId")
.HasColumnType("int");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("VectorSize")
.HasColumnType("int");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("SiteInfoId");
b.ToTable("ContentGroups");
});
modelBuilder.Entity("BLAIzor.Models.ContentItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Content")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("ContentGroupId")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<string>("Language")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime2");
b.Property<string>("Tags")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ContentGroupId");
b.ToTable("ContentItems");
});
modelBuilder.Entity("BLAIzor.Models.CssTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("CssContent")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("DesignTemplateId")
.HasColumnType("int");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("DesignTemplateId")
.IsUnique();
b.ToTable("CssTemplates");
});
modelBuilder.Entity("BLAIzor.Models.DesignTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsDeprecated")
.HasColumnType("bit");
b.Property<bool>("IsPrivate")
.HasColumnType("bit");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<string>("QDrandCollectionName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Tags")
.HasColumnType("nvarchar(max)");
b.Property<string>("TemplateName")
.HasColumnType("nvarchar(max)");
b.Property<string>("TemplatePhotoUrl")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("DesignTemplates");
b.HasData(
new
{
Id = 1,
CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "The default template",
IsDeprecated = false,
IsPrivate = false,
IsPublished = false,
QDrandCollectionName = "html_snippets",
Status = "Draft",
Tags = "system",
TemplateName = "Default Site",
TemplatePhotoUrl = "/images/default-logo.png",
UserId = "0988758e-e16c-4c2c-8c1e-efa3ac5f0274",
Version = 1
});
});
modelBuilder.Entity("BLAIzor.Models.FormDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<string>("JsonDefinition")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("SiteInfoId")
.HasColumnType("int");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("SiteInfoId", "Slug")
.IsUnique();
b.ToTable("FormDefinitions");
});
modelBuilder.Entity("BLAIzor.Models.MenuItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int?>("ContentGroupId")
.HasColumnType("int");
b.Property<int?>("ContentItemId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int?>("ParentId")
.HasColumnType("int");
b.Property<bool>("ShowInMainMenu")
.HasColumnType("bit");
b.Property<int>("SiteInfoId")
.HasColumnType("int");
b.Property<string>("Slug")
.HasColumnType("nvarchar(max)");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<string>("StoredHtml")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("ContentGroupId");
b.HasIndex("ContentItemId");
b.HasIndex("ParentId");
b.HasIndex("SiteInfoId");
b.ToTable("MenuItems");
});
modelBuilder.Entity("BLAIzor.Models.SiteInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("BackgroundVideo")
.HasColumnType("nvarchar(max)");
b.Property<string>("BrandLogoUrl")
.HasColumnType("nvarchar(max)");
b.Property<string>("DefaultColor")
.HasColumnType("nvarchar(max)");
b.Property<string>("DefaultLanguage")
.HasColumnType("nvarchar(max)");
b.Property<string>("DefaultUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("DomainUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("EmbeddingService")
.HasColumnType("nvarchar(max)");
b.Property<string>("Entity")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<string>("Persona")
.HasColumnType("nvarchar(max)");
b.Property<bool>("STTActive")
.HasColumnType("bit");
b.Property<string>("SiteDescription")
.HasColumnType("nvarchar(max)");
b.Property<string>("SiteName")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TTSActive")
.HasColumnType("bit");
b.Property<int?>("TemplateId")
.HasColumnType("int");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<string>("VoiceId")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("TemplateId");
b.HasIndex("UserId");
b.ToTable("SiteInfos");
b.HasData(
new
{
Id = 1,
BrandLogoUrl = "/images/default-logo.png",
DefaultColor = "#FFFFFF",
DefaultUrl = "https://ai.poppixel.cloud",
DomainUrl = "poppixel.cloud",
IsPublished = false,
STTActive = false,
SiteName = "Default Site",
TTSActive = false,
TemplateId = 1,
UserId = "0988758e-e16c-4c2c-8c1e-efa3ac5f0274"
});
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("AspNetUsers", (string)null);
b.HasData(
new
{
Id = "0988758e-e16c-4c2c-8c1e-efa3ac5f0274",
AccessFailedCount = 0,
ConcurrencyStamp = "a2836246-0303-4370-b283-e53a9a3f2813",
Email = "adam.g@aycode.com",
EmailConfirmed = true,
LockoutEnabled = false,
NormalizedEmail = "ADAM.G@AYCODE.COM",
NormalizedUserName = "ADAM.G@AYCODE.COM",
PasswordHash = "AQAAAAIAAYagAAAAEChxKCu+ReGvcZFR/6kPASbpnQdMp1MJuepeRyR4bfHTkUk8SfNAqmckGXvuw+GaGA==",
PhoneNumberConfirmed = false,
SecurityStamp = "7ecf121a-b0e7-4e30-a1f1-299eeaf0a9cc",
TwoFactorEnabled = false,
UserName = "adam.g@aycode.com"
});
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ProviderKey")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("RoleId")
.HasColumnType("nvarchar(450)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Name")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("BLAIzor.Models.ContentChunk", b =>
{
b.HasOne("BLAIzor.Models.ContentItem", "ContentItem")
.WithMany("Chunks")
.HasForeignKey("ContentItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ContentItem");
});
modelBuilder.Entity("BLAIzor.Models.ContentGroup", b =>
{
b.HasOne("BLAIzor.Models.SiteInfo", "SiteInfo")
.WithMany("ContentGroups")
.HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SiteInfo");
});
modelBuilder.Entity("BLAIzor.Models.ContentItem", b =>
{
b.HasOne("BLAIzor.Models.ContentGroup", "ContentGroup")
.WithMany("Items")
.HasForeignKey("ContentGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ContentGroup");
});
modelBuilder.Entity("BLAIzor.Models.CssTemplate", b =>
{
b.HasOne("BLAIzor.Models.DesignTemplate", "DesignTemplate")
.WithOne("CssTemplate")
.HasForeignKey("BLAIzor.Models.CssTemplate", "DesignTemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DesignTemplate");
});
modelBuilder.Entity("BLAIzor.Models.DesignTemplate", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("BLAIzor.Models.FormDefinition", b =>
{
b.HasOne("BLAIzor.Models.SiteInfo", "SiteInfo")
.WithMany("FormDefinitions")
.HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SiteInfo");
});
modelBuilder.Entity("BLAIzor.Models.MenuItem", b =>
{
b.HasOne("BLAIzor.Models.ContentGroup", "ContentGroup")
.WithMany()
.HasForeignKey("ContentGroupId")
.OnDelete(DeleteBehavior.NoAction);
b.HasOne("BLAIzor.Models.ContentItem", "ContentItem")
.WithMany()
.HasForeignKey("ContentItemId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("BLAIzor.Models.MenuItem", "Parent")
.WithMany("Children")
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("BLAIzor.Models.SiteInfo", "SiteInfo")
.WithMany("MenuItems")
.HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ContentGroup");
b.Navigation("ContentItem");
b.Navigation("Parent");
b.Navigation("SiteInfo");
});
modelBuilder.Entity("BLAIzor.Models.SiteInfo", b =>
{
b.HasOne("BLAIzor.Models.DesignTemplate", "Template")
.WithMany("Sites")
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Template");
b.Navigation("User");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("BLAIzor.Models.ContentGroup", b =>
{
b.Navigation("Items");
});
modelBuilder.Entity("BLAIzor.Models.ContentItem", b =>
{
b.Navigation("Chunks");
});
modelBuilder.Entity("BLAIzor.Models.DesignTemplate", b =>
{
b.Navigation("CssTemplate")
.IsRequired();
b.Navigation("Sites");
});
modelBuilder.Entity("BLAIzor.Models.MenuItem", b =>
{
b.Navigation("Children");
});
modelBuilder.Entity("BLAIzor.Models.SiteInfo", b =>
{
b.Navigation("ContentGroups");
b.Navigation("FormDefinitions");
b.Navigation("MenuItems");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,49 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BLAIzor.Migrations
{
/// <inheritdoc />
public partial class CollectionAdministrationUpdate2 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "ContentItemId",
table: "MenuItems",
type: "int",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_MenuItems_ContentItemId",
table: "MenuItems",
column: "ContentItemId");
migrationBuilder.AddForeignKey(
name: "FK_MenuItems_ContentItems_ContentItemId",
table: "MenuItems",
column: "ContentItemId",
principalTable: "ContentItems",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_MenuItems_ContentItems_ContentItemId",
table: "MenuItems");
migrationBuilder.DropIndex(
name: "IX_MenuItems_ContentItemId",
table: "MenuItems");
migrationBuilder.DropColumn(
name: "ContentItemId",
table: "MenuItems");
}
}
}

View File

@ -0,0 +1,858 @@
// <auto-generated />
using System;
using BLAIzor.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace BLAIzor.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250625185236_StoredCollectionName")]
partial class StoredCollectionName
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("BLAIzor.Models.ContentChunk", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("ChunkIndex")
.HasColumnType("int");
b.Property<int>("ContentItemId")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("QdrantPointId")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("VectorHash")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("ContentItemId");
b.ToTable("ContentChunks");
});
modelBuilder.Entity("BLAIzor.Models.ContentGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("EmbeddingModel")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime2");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("SiteInfoId")
.HasColumnType("int");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("VectorSize")
.HasColumnType("int");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("SiteInfoId");
b.ToTable("ContentGroups");
});
modelBuilder.Entity("BLAIzor.Models.ContentItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Content")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("ContentGroupId")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<string>("Language")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime2");
b.Property<string>("Tags")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ContentGroupId");
b.ToTable("ContentItems");
});
modelBuilder.Entity("BLAIzor.Models.CssTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("CssContent")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("DesignTemplateId")
.HasColumnType("int");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("DesignTemplateId")
.IsUnique();
b.ToTable("CssTemplates");
});
modelBuilder.Entity("BLAIzor.Models.DesignTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsDeprecated")
.HasColumnType("bit");
b.Property<bool>("IsPrivate")
.HasColumnType("bit");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<string>("QDrandCollectionName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Tags")
.HasColumnType("nvarchar(max)");
b.Property<string>("TemplateName")
.HasColumnType("nvarchar(max)");
b.Property<string>("TemplatePhotoUrl")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("DesignTemplates");
b.HasData(
new
{
Id = 1,
CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "The default template",
IsDeprecated = false,
IsPrivate = false,
IsPublished = false,
QDrandCollectionName = "html_snippets",
Status = "Draft",
Tags = "system",
TemplateName = "Default Site",
TemplatePhotoUrl = "/images/default-logo.png",
UserId = "0988758e-e16c-4c2c-8c1e-efa3ac5f0274",
Version = 1
});
});
modelBuilder.Entity("BLAIzor.Models.FormDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<string>("JsonDefinition")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("SiteInfoId")
.HasColumnType("int");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("SiteInfoId", "Slug")
.IsUnique();
b.ToTable("FormDefinitions");
});
modelBuilder.Entity("BLAIzor.Models.MenuItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int?>("ContentGroupId")
.HasColumnType("int");
b.Property<int?>("ContentItemId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int?>("ParentId")
.HasColumnType("int");
b.Property<bool>("ShowInMainMenu")
.HasColumnType("bit");
b.Property<int>("SiteInfoId")
.HasColumnType("int");
b.Property<string>("Slug")
.HasColumnType("nvarchar(max)");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<string>("StoredHtml")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("ContentGroupId");
b.HasIndex("ContentItemId");
b.HasIndex("ParentId");
b.HasIndex("SiteInfoId");
b.ToTable("MenuItems");
});
modelBuilder.Entity("BLAIzor.Models.SiteInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("BackgroundVideo")
.HasColumnType("nvarchar(max)");
b.Property<string>("BrandLogoUrl")
.HasColumnType("nvarchar(max)");
b.Property<string>("DefaultColor")
.HasColumnType("nvarchar(max)");
b.Property<string>("DefaultLanguage")
.HasColumnType("nvarchar(max)");
b.Property<string>("DefaultUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("DomainUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("EmbeddingService")
.HasColumnType("nvarchar(max)");
b.Property<string>("Entity")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<string>("Persona")
.HasColumnType("nvarchar(max)");
b.Property<bool>("STTActive")
.HasColumnType("bit");
b.Property<string>("SiteDescription")
.HasColumnType("nvarchar(max)");
b.Property<string>("SiteName")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TTSActive")
.HasColumnType("bit");
b.Property<int?>("TemplateId")
.HasColumnType("int");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<string>("VectorCollectionName")
.HasColumnType("nvarchar(max)");
b.Property<string>("VoiceId")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("TemplateId");
b.HasIndex("UserId");
b.ToTable("SiteInfos");
b.HasData(
new
{
Id = 1,
BrandLogoUrl = "/images/default-logo.png",
DefaultColor = "#FFFFFF",
DefaultUrl = "https://ai.poppixel.cloud",
DomainUrl = "poppixel.cloud",
IsPublished = false,
STTActive = false,
SiteName = "Default Site",
TTSActive = false,
TemplateId = 1,
UserId = "0988758e-e16c-4c2c-8c1e-efa3ac5f0274"
});
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("AspNetUsers", (string)null);
b.HasData(
new
{
Id = "0988758e-e16c-4c2c-8c1e-efa3ac5f0274",
AccessFailedCount = 0,
ConcurrencyStamp = "a2836246-0303-4370-b283-e53a9a3f2813",
Email = "adam.g@aycode.com",
EmailConfirmed = true,
LockoutEnabled = false,
NormalizedEmail = "ADAM.G@AYCODE.COM",
NormalizedUserName = "ADAM.G@AYCODE.COM",
PasswordHash = "AQAAAAIAAYagAAAAEChxKCu+ReGvcZFR/6kPASbpnQdMp1MJuepeRyR4bfHTkUk8SfNAqmckGXvuw+GaGA==",
PhoneNumberConfirmed = false,
SecurityStamp = "7ecf121a-b0e7-4e30-a1f1-299eeaf0a9cc",
TwoFactorEnabled = false,
UserName = "adam.g@aycode.com"
});
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ProviderKey")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("RoleId")
.HasColumnType("nvarchar(450)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Name")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("BLAIzor.Models.ContentChunk", b =>
{
b.HasOne("BLAIzor.Models.ContentItem", "ContentItem")
.WithMany("Chunks")
.HasForeignKey("ContentItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ContentItem");
});
modelBuilder.Entity("BLAIzor.Models.ContentGroup", b =>
{
b.HasOne("BLAIzor.Models.SiteInfo", "SiteInfo")
.WithMany("ContentGroups")
.HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SiteInfo");
});
modelBuilder.Entity("BLAIzor.Models.ContentItem", b =>
{
b.HasOne("BLAIzor.Models.ContentGroup", "ContentGroup")
.WithMany("Items")
.HasForeignKey("ContentGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ContentGroup");
});
modelBuilder.Entity("BLAIzor.Models.CssTemplate", b =>
{
b.HasOne("BLAIzor.Models.DesignTemplate", "DesignTemplate")
.WithOne("CssTemplate")
.HasForeignKey("BLAIzor.Models.CssTemplate", "DesignTemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DesignTemplate");
});
modelBuilder.Entity("BLAIzor.Models.DesignTemplate", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("BLAIzor.Models.FormDefinition", b =>
{
b.HasOne("BLAIzor.Models.SiteInfo", "SiteInfo")
.WithMany("FormDefinitions")
.HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SiteInfo");
});
modelBuilder.Entity("BLAIzor.Models.MenuItem", b =>
{
b.HasOne("BLAIzor.Models.ContentGroup", "ContentGroup")
.WithMany()
.HasForeignKey("ContentGroupId")
.OnDelete(DeleteBehavior.NoAction);
b.HasOne("BLAIzor.Models.ContentItem", "ContentItem")
.WithMany()
.HasForeignKey("ContentItemId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("BLAIzor.Models.MenuItem", "Parent")
.WithMany("Children")
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("BLAIzor.Models.SiteInfo", "SiteInfo")
.WithMany("MenuItems")
.HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ContentGroup");
b.Navigation("ContentItem");
b.Navigation("Parent");
b.Navigation("SiteInfo");
});
modelBuilder.Entity("BLAIzor.Models.SiteInfo", b =>
{
b.HasOne("BLAIzor.Models.DesignTemplate", "Template")
.WithMany("Sites")
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Template");
b.Navigation("User");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("BLAIzor.Models.ContentGroup", b =>
{
b.Navigation("Items");
});
modelBuilder.Entity("BLAIzor.Models.ContentItem", b =>
{
b.Navigation("Chunks");
});
modelBuilder.Entity("BLAIzor.Models.DesignTemplate", b =>
{
b.Navigation("CssTemplate")
.IsRequired();
b.Navigation("Sites");
});
modelBuilder.Entity("BLAIzor.Models.MenuItem", b =>
{
b.Navigation("Children");
});
modelBuilder.Entity("BLAIzor.Models.SiteInfo", b =>
{
b.Navigation("ContentGroups");
b.Navigation("FormDefinitions");
b.Navigation("MenuItems");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BLAIzor.Migrations
{
/// <inheritdoc />
public partial class StoredCollectionName : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "VectorCollectionName",
table: "SiteInfos",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.UpdateData(
table: "SiteInfos",
keyColumn: "Id",
keyValue: 1,
column: "VectorCollectionName",
value: null);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "VectorCollectionName",
table: "SiteInfos");
}
}
}

View File

@ -0,0 +1,885 @@
// <auto-generated />
using System;
using BLAIzor.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace BLAIzor.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250628191216_logger")]
partial class logger
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("BLAIzor.Models.AppLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Details")
.HasColumnType("nvarchar(max)");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Severity")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime2");
b.HasKey("Id");
b.ToTable("Logs");
});
modelBuilder.Entity("BLAIzor.Models.ContentChunk", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("ChunkIndex")
.HasColumnType("int");
b.Property<int>("ContentItemId")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("QdrantPointId")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("VectorHash")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("ContentItemId");
b.ToTable("ContentChunks");
});
modelBuilder.Entity("BLAIzor.Models.ContentGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("EmbeddingModel")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime2");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("SiteInfoId")
.HasColumnType("int");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("VectorSize")
.HasColumnType("int");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("SiteInfoId");
b.ToTable("ContentGroups");
});
modelBuilder.Entity("BLAIzor.Models.ContentItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Content")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("ContentGroupId")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<string>("Language")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime2");
b.Property<string>("Tags")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ContentGroupId");
b.ToTable("ContentItems");
});
modelBuilder.Entity("BLAIzor.Models.CssTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("CssContent")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("DesignTemplateId")
.HasColumnType("int");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("DesignTemplateId")
.IsUnique();
b.ToTable("CssTemplates");
});
modelBuilder.Entity("BLAIzor.Models.DesignTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsDeprecated")
.HasColumnType("bit");
b.Property<bool>("IsPrivate")
.HasColumnType("bit");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<string>("QDrandCollectionName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Tags")
.HasColumnType("nvarchar(max)");
b.Property<string>("TemplateName")
.HasColumnType("nvarchar(max)");
b.Property<string>("TemplatePhotoUrl")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("DesignTemplates");
b.HasData(
new
{
Id = 1,
CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "The default template",
IsDeprecated = false,
IsPrivate = false,
IsPublished = false,
QDrandCollectionName = "html_snippets",
Status = "Draft",
Tags = "system",
TemplateName = "Default Site",
TemplatePhotoUrl = "/images/default-logo.png",
UserId = "0988758e-e16c-4c2c-8c1e-efa3ac5f0274",
Version = 1
});
});
modelBuilder.Entity("BLAIzor.Models.FormDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<string>("JsonDefinition")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("SiteInfoId")
.HasColumnType("int");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("SiteInfoId", "Slug")
.IsUnique();
b.ToTable("FormDefinitions");
});
modelBuilder.Entity("BLAIzor.Models.MenuItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int?>("ContentGroupId")
.HasColumnType("int");
b.Property<int?>("ContentItemId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int?>("ParentId")
.HasColumnType("int");
b.Property<bool>("ShowInMainMenu")
.HasColumnType("bit");
b.Property<int>("SiteInfoId")
.HasColumnType("int");
b.Property<string>("Slug")
.HasColumnType("nvarchar(max)");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<string>("StoredHtml")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("ContentGroupId");
b.HasIndex("ContentItemId");
b.HasIndex("ParentId");
b.HasIndex("SiteInfoId");
b.ToTable("MenuItems");
});
modelBuilder.Entity("BLAIzor.Models.SiteInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("BackgroundVideo")
.HasColumnType("nvarchar(max)");
b.Property<string>("BrandLogoUrl")
.HasColumnType("nvarchar(max)");
b.Property<string>("DefaultColor")
.HasColumnType("nvarchar(max)");
b.Property<string>("DefaultLanguage")
.HasColumnType("nvarchar(max)");
b.Property<string>("DefaultUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("DomainUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("EmbeddingService")
.HasColumnType("nvarchar(max)");
b.Property<string>("Entity")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<string>("Persona")
.HasColumnType("nvarchar(max)");
b.Property<bool>("STTActive")
.HasColumnType("bit");
b.Property<string>("SiteDescription")
.HasColumnType("nvarchar(max)");
b.Property<string>("SiteName")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TTSActive")
.HasColumnType("bit");
b.Property<int?>("TemplateId")
.HasColumnType("int");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<string>("VectorCollectionName")
.HasColumnType("nvarchar(max)");
b.Property<string>("VoiceId")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("TemplateId");
b.HasIndex("UserId");
b.ToTable("SiteInfos");
b.HasData(
new
{
Id = 1,
BrandLogoUrl = "/images/default-logo.png",
DefaultColor = "#FFFFFF",
DefaultUrl = "https://ai.poppixel.cloud",
DomainUrl = "poppixel.cloud",
IsPublished = false,
STTActive = false,
SiteName = "Default Site",
TTSActive = false,
TemplateId = 1,
UserId = "0988758e-e16c-4c2c-8c1e-efa3ac5f0274"
});
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("AspNetUsers", (string)null);
b.HasData(
new
{
Id = "0988758e-e16c-4c2c-8c1e-efa3ac5f0274",
AccessFailedCount = 0,
ConcurrencyStamp = "a2836246-0303-4370-b283-e53a9a3f2813",
Email = "adam.g@aycode.com",
EmailConfirmed = true,
LockoutEnabled = false,
NormalizedEmail = "ADAM.G@AYCODE.COM",
NormalizedUserName = "ADAM.G@AYCODE.COM",
PasswordHash = "AQAAAAIAAYagAAAAEChxKCu+ReGvcZFR/6kPASbpnQdMp1MJuepeRyR4bfHTkUk8SfNAqmckGXvuw+GaGA==",
PhoneNumberConfirmed = false,
SecurityStamp = "7ecf121a-b0e7-4e30-a1f1-299eeaf0a9cc",
TwoFactorEnabled = false,
UserName = "adam.g@aycode.com"
});
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ProviderKey")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("RoleId")
.HasColumnType("nvarchar(450)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Name")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("BLAIzor.Models.ContentChunk", b =>
{
b.HasOne("BLAIzor.Models.ContentItem", "ContentItem")
.WithMany("Chunks")
.HasForeignKey("ContentItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ContentItem");
});
modelBuilder.Entity("BLAIzor.Models.ContentGroup", b =>
{
b.HasOne("BLAIzor.Models.SiteInfo", "SiteInfo")
.WithMany("ContentGroups")
.HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SiteInfo");
});
modelBuilder.Entity("BLAIzor.Models.ContentItem", b =>
{
b.HasOne("BLAIzor.Models.ContentGroup", "ContentGroup")
.WithMany("Items")
.HasForeignKey("ContentGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ContentGroup");
});
modelBuilder.Entity("BLAIzor.Models.CssTemplate", b =>
{
b.HasOne("BLAIzor.Models.DesignTemplate", "DesignTemplate")
.WithOne("CssTemplate")
.HasForeignKey("BLAIzor.Models.CssTemplate", "DesignTemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DesignTemplate");
});
modelBuilder.Entity("BLAIzor.Models.DesignTemplate", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("BLAIzor.Models.FormDefinition", b =>
{
b.HasOne("BLAIzor.Models.SiteInfo", "SiteInfo")
.WithMany("FormDefinitions")
.HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SiteInfo");
});
modelBuilder.Entity("BLAIzor.Models.MenuItem", b =>
{
b.HasOne("BLAIzor.Models.ContentGroup", "ContentGroup")
.WithMany()
.HasForeignKey("ContentGroupId")
.OnDelete(DeleteBehavior.NoAction);
b.HasOne("BLAIzor.Models.ContentItem", "ContentItem")
.WithMany()
.HasForeignKey("ContentItemId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("BLAIzor.Models.MenuItem", "Parent")
.WithMany("Children")
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("BLAIzor.Models.SiteInfo", "SiteInfo")
.WithMany("MenuItems")
.HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ContentGroup");
b.Navigation("ContentItem");
b.Navigation("Parent");
b.Navigation("SiteInfo");
});
modelBuilder.Entity("BLAIzor.Models.SiteInfo", b =>
{
b.HasOne("BLAIzor.Models.DesignTemplate", "Template")
.WithMany("Sites")
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Template");
b.Navigation("User");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("BLAIzor.Models.ContentGroup", b =>
{
b.Navigation("Items");
});
modelBuilder.Entity("BLAIzor.Models.ContentItem", b =>
{
b.Navigation("Chunks");
});
modelBuilder.Entity("BLAIzor.Models.DesignTemplate", b =>
{
b.Navigation("CssTemplate")
.IsRequired();
b.Navigation("Sites");
});
modelBuilder.Entity("BLAIzor.Models.MenuItem", b =>
{
b.Navigation("Children");
});
modelBuilder.Entity("BLAIzor.Models.SiteInfo", b =>
{
b.Navigation("ContentGroups");
b.Navigation("FormDefinitions");
b.Navigation("MenuItems");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,38 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BLAIzor.Migrations
{
/// <inheritdoc />
public partial class logger : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Logs",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Timestamp = table.Column<DateTime>(type: "datetime2", nullable: false),
Severity = table.Column<string>(type: "nvarchar(max)", nullable: false),
Message = table.Column<string>(type: "nvarchar(max)", nullable: false),
Details = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Logs", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Logs");
}
}
}

View File

@ -22,6 +22,160 @@ namespace BLAIzor.Migrations
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("BLAIzor.Models.AppLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Details")
.HasColumnType("nvarchar(max)");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Severity")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime2");
b.HasKey("Id");
b.ToTable("Logs");
});
modelBuilder.Entity("BLAIzor.Models.ContentChunk", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("ChunkIndex")
.HasColumnType("int");
b.Property<int>("ContentItemId")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("QdrantPointId")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("VectorHash")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("ContentItemId");
b.ToTable("ContentChunks");
});
modelBuilder.Entity("BLAIzor.Models.ContentGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("EmbeddingModel")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime2");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("SiteInfoId")
.HasColumnType("int");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("VectorSize")
.HasColumnType("int");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("SiteInfoId");
b.ToTable("ContentGroups");
});
modelBuilder.Entity("BLAIzor.Models.ContentItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Content")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("ContentGroupId")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<string>("Language")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime2");
b.Property<string>("Tags")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ContentGroupId");
b.ToTable("ContentItems");
});
modelBuilder.Entity("BLAIzor.Models.CssTemplate", b => modelBuilder.Entity("BLAIzor.Models.CssTemplate", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -173,22 +327,28 @@ namespace BLAIzor.Migrations
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int?>("ContentGroupId")
.HasColumnType("int");
b.Property<int?>("ContentItemId")
.HasColumnType("int");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<int>("PointId") b.Property<int?>("ParentId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<Guid?>("QdrantPointId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("ShowInMainMenu") b.Property<bool>("ShowInMainMenu")
.HasColumnType("bit"); .HasColumnType("bit");
b.Property<int>("SiteInfoId") b.Property<int>("SiteInfoId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("Slug")
.HasColumnType("nvarchar(max)");
b.Property<int>("SortOrder") b.Property<int>("SortOrder")
.HasColumnType("int"); .HasColumnType("int");
@ -197,6 +357,12 @@ namespace BLAIzor.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ContentGroupId");
b.HasIndex("ContentItemId");
b.HasIndex("ParentId");
b.HasIndex("SiteInfoId"); b.HasIndex("SiteInfoId");
b.ToTable("MenuItems"); b.ToTable("MenuItems");
@ -261,6 +427,9 @@ namespace BLAIzor.Migrations
.IsRequired() .IsRequired()
.HasColumnType("nvarchar(450)"); .HasColumnType("nvarchar(450)");
b.Property<string>("VectorCollectionName")
.HasColumnType("nvarchar(max)");
b.Property<string>("VoiceId") b.Property<string>("VoiceId")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
@ -509,6 +678,39 @@ namespace BLAIzor.Migrations
b.ToTable("AspNetUserTokens", (string)null); b.ToTable("AspNetUserTokens", (string)null);
}); });
modelBuilder.Entity("BLAIzor.Models.ContentChunk", b =>
{
b.HasOne("BLAIzor.Models.ContentItem", "ContentItem")
.WithMany("Chunks")
.HasForeignKey("ContentItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ContentItem");
});
modelBuilder.Entity("BLAIzor.Models.ContentGroup", b =>
{
b.HasOne("BLAIzor.Models.SiteInfo", "SiteInfo")
.WithMany("ContentGroups")
.HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SiteInfo");
});
modelBuilder.Entity("BLAIzor.Models.ContentItem", b =>
{
b.HasOne("BLAIzor.Models.ContentGroup", "ContentGroup")
.WithMany("Items")
.HasForeignKey("ContentGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ContentGroup");
});
modelBuilder.Entity("BLAIzor.Models.CssTemplate", b => modelBuilder.Entity("BLAIzor.Models.CssTemplate", b =>
{ {
b.HasOne("BLAIzor.Models.DesignTemplate", "DesignTemplate") b.HasOne("BLAIzor.Models.DesignTemplate", "DesignTemplate")
@ -544,12 +746,33 @@ namespace BLAIzor.Migrations
modelBuilder.Entity("BLAIzor.Models.MenuItem", b => modelBuilder.Entity("BLAIzor.Models.MenuItem", b =>
{ {
b.HasOne("BLAIzor.Models.ContentGroup", "ContentGroup")
.WithMany()
.HasForeignKey("ContentGroupId")
.OnDelete(DeleteBehavior.NoAction);
b.HasOne("BLAIzor.Models.ContentItem", "ContentItem")
.WithMany()
.HasForeignKey("ContentItemId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("BLAIzor.Models.MenuItem", "Parent")
.WithMany("Children")
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("BLAIzor.Models.SiteInfo", "SiteInfo") b.HasOne("BLAIzor.Models.SiteInfo", "SiteInfo")
.WithMany("MenuItems") .WithMany("MenuItems")
.HasForeignKey("SiteInfoId") .HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("ContentGroup");
b.Navigation("ContentItem");
b.Navigation("Parent");
b.Navigation("SiteInfo"); b.Navigation("SiteInfo");
}); });
@ -622,6 +845,16 @@ namespace BLAIzor.Migrations
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("BLAIzor.Models.ContentGroup", b =>
{
b.Navigation("Items");
});
modelBuilder.Entity("BLAIzor.Models.ContentItem", b =>
{
b.Navigation("Chunks");
});
modelBuilder.Entity("BLAIzor.Models.DesignTemplate", b => modelBuilder.Entity("BLAIzor.Models.DesignTemplate", b =>
{ {
b.Navigation("CssTemplate") b.Navigation("CssTemplate")
@ -630,8 +863,15 @@ namespace BLAIzor.Migrations
b.Navigation("Sites"); b.Navigation("Sites");
}); });
modelBuilder.Entity("BLAIzor.Models.MenuItem", b =>
{
b.Navigation("Children");
});
modelBuilder.Entity("BLAIzor.Models.SiteInfo", b => modelBuilder.Entity("BLAIzor.Models.SiteInfo", b =>
{ {
b.Navigation("ContentGroups");
b.Navigation("FormDefinitions"); b.Navigation("FormDefinitions");
b.Navigation("MenuItems"); b.Navigation("MenuItems");

424
Models/AiPrompts.cs Normal file
View File

@ -0,0 +1,424 @@
using BLAIzor.Services;
using System.Text;
namespace BLAIzor.Models
{
public static class AiPrompts
{
public static class WelcomeContent
{
public static string GetSystemMessageForWelcomeMessage(string mood, string siteEntity, string extractedText, string selectedBrandName, string language, string menuList)
{
string systemMessage = "You are a helpful, " + mood + " assistant that welcomes the user speaking in the name of the " + siteEntity + " described by the content, on a website of " + selectedBrandName + " in " + language + ". Use the following content: `" +
extractedText + "` " +
//"and generate a short" +Mood+ "but kind marketing-oriented welcome message and introduction of the brand for the user, constructed as simple Bootstrap HTML codeblock with a <h1> tagged title and a paragraph." +
"and generate a" + mood + " marketing-oriented welcome message and a summary of the content and introduction " +
"of the brand for the user in the name of " + siteEntity + " , aiming to explain clearly, what does the company/person offer, constructed " +
"as simple Bootstrap HTML <div clss=\"container\"> codeblock with a <h1> tagged title and a paragraph." +
"If there is any logo, or not logo but main brand image in the document use that url, " +
"and add that in a new container, as a bootstrap responsive ('img-fluid py-3') image, with the maximum height of 30vh." +
"If there is anything marked important in the text, make sure to add that in your answer." +
"If there are links to be displayed, make sure to display them as clickable links." +
"After the welcome message, always add in a new div with container class: 'This website has a live, " +
"AI interface: if you have any questions, you can simply ask either by typing in the message " +
"box, or by clicking the microphone icon on the top of the page. '" +
"Here is a list of topics " + menuList +
", make a new bootstrap clearfix and after that make a clickable bootstrap " +
"styled (btn btn-primary) button from each of the determined topics, " +
"that calls the javascript function 'callAI({the name of the topic})' on click. " +
"Do not include anything else than the html title and text elements, no css, no scripts, no head or other tags." +
"Do not mark your answer with ```html or any other mark.";
return systemMessage;
}
}
public static class UserIntention
{
}
public static class ContentProcessing
{
public static string GetSystemMessageForJsonResultDecision(string language, string extractedText, string currentDom)
{
string systemMessage = $"You are a helpful assistant built in a website, trying to figure out what the User wants to do or know about.\r\n" +
"Your job is to classify the user's request into one of the following categories:\r\n" +
"1. **Ask about or search infromation in the websites content** (Return a 'Text result')\r\n" +
"2. **Analyze the currently displayed HTML content** (Return an 'Examination result')\r\n" +
"3. **Initiate an action** (Return a 'Method result')\r\n" +
"If none of the above applies, return an 'Error result'.\r\n\r\n" +
"**Response format:**\r\n" +
"Strictly respond in " + language + " as a JSON object, using one of the following formats:\r\n" +
"1. **chatGPTMethodResult** (for initiating actions):\r\n" +
" - `type`: \"methodresult\"\r\n" +
" - `text`: A short explanation of what the user wants to do.\r\n" +
" - `methodToCall`: One of: [openContactForm, openCalendar, openApplicationForm]\r\n" +
" - `parameter`: [email address for openContactForm, calendlyUserName for openCalendar, empty string for openApplicationForm]\r\n\r\n" +
"2. **chatGPTTextResult** (for general website content searches):\r\n" +
" - `type`: \"textresult\"\r\n" +
" - `text`: The users unmodified query.\r\n\r\n" +
"3. **chatGPTExaminationResult** (for analyzing the currently displayed page only):\r\n" +
" - `type`: \"examinationresult\"\r\n" +
" - `text`: The users unmodified query.\r\n\r\n" +
"4. **chatGPTErrorResult** (for errors):\r\n" +
" - `type`: \"errorresult\"\r\n" +
" - `text`: A description of the issue encountered.\r\n\r\n" +
"**Decision Rules:**\r\n" +
"- If the user is **searching for website content** beyond what is currently displayed (e.g., 'Find information about our services'), return a `textresult`.\r\n" +
"- If the user is **asking about the currently visible content** (e.g., 'What is shown on the page?'), return an `examinationresult`.\r\n" +
"- If the user wants to **perform an action**, return a `methodresult`.\r\n" +
"- If the required parameter is missing, return an `errorresult`.\r\n\r\n" +
"**Examples:**\r\n" +
"- User asks: 'Show me information about pricing' → `textresult`\r\n" +
"- User asks: 'What is displayed right now?' → `examinationresult`\r\n" +
"- User asks: 'Open the contact form' → `methodresult`\r\n" +
"- User asks: 'Contact support' but no email is found → `errorresult`\r\n\r\n" +
"**Context:**\r\n" +
"- Base responses on this initial document: {" + extractedText + "}\r\n" +
"- Current displayed HTML: {" + currentDom + "}\r\n" +
"**IMPORTANT:**\r\n" +
"- If the request is about general content, **DO NOT use 'examinationresult'**.\r\n" +
"- If the request is about the currently displayed page, **DO NOT use 'textresult'**.\r\n" +
"- Do NOT format the response with markdown, code blocks, or `json` tags, do not add any title, or explanation besides the plain json object";
return systemMessage;
}
}
public static class HtmlGeneration
{
public const string BootstrapCard = "Generate a Bootstrap 5 card layout using the following content:";
public const string SectionLayout = "Create a responsive HTML5 layout section for this text:";
}
public static class LayoutPlanning
{
public static string GetLayoutPlanningSystemPrompt(List<HtmlSnippet> htmlToUse, Dictionary<string, string>? photos, string[]? topics)
{
var sb = new StringBuilder();
// 📌 STRICT FORMAT INSTRUCTION
sb.AppendLine("You are a helpful assistant whose ONLY task is to break down THE PROVIDED CONTENT into a structured JSON object for Bootstrap 5 pages, using \n" +
"**layoutplan**:\r\n" +
"- `title`: \"The title of the page\"\r\n" +
"- `blocks`: \"an array of layoutblocks\"\r\n" +
".");
if ((htmlToUse != null))
{
sb.AppendLine("\n### You have these html snippets provided:");
foreach (var snippet in htmlToUse)
{
//no need to paste the html code
//sb.AppendLine($"Snippet id: {snippet.Id}, Snippet name: {snippet.Name}, Snippet description: {snippet.Description}, Snippet type: {snippet.Type}, Snippet template code: {snippet.Html}");
sb.AppendLine($"Snippet id: {snippet.Id}, Snippet name: {snippet.Name}, Snippet description: {snippet.Description}, Snippet type: {snippet.Type}");
}
sb.AppendLine("- Use them ONLY if relevant.");
}
if (photos != null && photos.Any())
{
sb.AppendLine("\n### Photos to be used in the blocks:");
sb.AppendLine(string.Join(", ", photos.Select(kv => $"{kv.Key}: {kv.Value}")));
}
if (topics != null && topics.Any())
{
sb.AppendLine("\n### Topics:");
sb.AppendLine(string.Join(", ", topics));
}
sb.AppendLine("### ⛔️ ABSOLUTELY DO NOT:");
sb.AppendLine("- ❌ Return HTML structure trees like `<section><div>`...");
sb.AppendLine("- ❌ Include keys like `src`, `alt`, `url`, `items`, or nested object lists.");
sb.AppendLine("- ❌ Invent block types not on the list.");
sb.AppendLine("- ❌ Return any explanation, markdown, prose, comments, or fallback formats.");
sb.AppendLine("- ❌ Use any of these block types: `image`, `quote`, `list`, `layout`, `header`, `footer`, `sidebar`, etc.");
sb.AppendLine("- ❌ Remove messages for AI marked with []");
sb.AppendLine("\n### ✅ REQUIRED OUTPUT FORMAT:");
sb.AppendLine("{");
sb.AppendLine(" \"title\": \"string\",");
sb.AppendLine(" \"blocks\": [");
sb.AppendLine(" { \"type\": \"string\", \"rawcontent\": \"string (text)\", \"preferredsnippetid\" : an integer id of matching html snippet if one found., \"order\": an incremented integer to set the blocks in order }");
sb.AppendLine(" ]");
sb.AppendLine("}");
sb.AppendLine("\n### ✅ Allowed `type` values (only use these):");
sb.AppendLine("- hero");
sb.AppendLine("- text");
sb.AppendLine("- text-image");
sb.AppendLine("- features");
sb.AppendLine("- cta");
sb.AppendLine("- video");
sb.AppendLine("- icon-list");
sb.AppendLine("- event-list");
sb.AppendLine("- audio-player");
sb.AppendLine("- topic-buttons");
sb.AppendLine("\n### ❗ GOOD EXAMPLE:");
sb.AppendLine("{");
sb.AppendLine(" \"title\": \"Welcome to Our Product\",");
sb.AppendLine(" \"blocks\": [");
sb.AppendLine(" { \"type\": \"hero\", \"rawcontent\": \"Example text\", \"preferredsnippetid\": 1, \"order\": 0 },");
sb.AppendLine(" { \"type\": \"features\", \"rawcontent\": \"Example features\", \"order\": 1 },");
sb.AppendLine(" { \"type\": \"cta\", \"rawcontent\": \"call to action content\", \"order\": 2 }");
sb.AppendLine(" ]");
sb.AppendLine("}");
sb.AppendLine("\n### ⛔ BAD EXAMPLES (DO NOT DO THIS):");
sb.AppendLine("- { \"type\": \"image\", \"src\": \"...\", \"alt\": \"...\" }");
sb.AppendLine("- { \"type\": \"list\", \"items\": [\"...\", \"...\"] }");
sb.AppendLine("- { \"layout\": { \"header\": ..., \"footer\": ... } }");
sb.AppendLine("\n### FINAL TASK:");
sb.AppendLine("Based on the following content, generate a JSON object following the exact format above. Output only the JSON. No markdown. No other marks. No explanation. ONLY the pure JSON.");
return sb.ToString();
}
public static string GetLayoutPlanningUserPrompt(string interMediateResult, string pageTitle, Dictionary<string, string>? photos)
{
var userMessage = "Based on the following content:\n\n*** " + interMediateResult + " ***\n\n" +
"and the available photos:";
if (photos != null && photos.Any())
{
userMessage += "\n### Photo urls to be used WITHOUT ANY MODIFICATION in the blocks:";
userMessage += string.Join(", ", photos.Select(kv => $"{kv.Key}: {kv.Value}"));
}
userMessage += ", return ONLY a JSON object representing a structured content layout for a webpage with the title '" + pageTitle + "', with this exact shape:\n" +
"{ \"title\": string, \"blocks\": [ { \"type\": \"string\", \"rawcontent\": \"string (text)\", \"preferredsnippetid\" : an integer id of matching html snippet if one found., \"order\": an incremented integer to set the blocks in order } ] }\n\n" +
"⚠️ DO NOT use layout structures like 'header', 'sidebar', 'mainContent', or 'footer'.\n" +
"⚠️ DO NOT explain anything. Just return the pure JSON. No markdown, no ``` markers.\n\n" +
"🎯 OBJECTIVE:\n" +
"1. Determine 1 to 5 major content blocks from the input, grouping full paragraphs or sections together.\n" +
"Prefer **1 to 5 blocks max**, unless the content is extremely long. Each block should represent a semantically complete section.\r\n" +
"Group related ideas into one block, even if they are multiple paragraphs. Do NOT break up content just because it is a new sentence. \r\n" +
"2. Use meaningful, semantically grouped layout block types like: 'hero', 'features list', 'text with image', 'text', 'product list', 'call to action', 'videoplayer', 'audioplayer', etc.\n" +
"3. Each block should contain **rich content** and related photo ['photo url': 'the url of the photo'], not just one or two sentences. Group related ideas into a single `rawcontent` field.\n\n" +
"DO NOT REMOVE ANY URLS (photo, wen link, etc) from the section or paragraph that it follows. \n\n" +
"📌 Examples of good block grouping:\n" +
"- All introductory marketing text together in one 'hero' or 'text' block\n" +
"- A group of benefits or features into a single 'features' block\n" +
"- A pitch paragraph into a 'call to action' block\n\n" +
"🎨 Naming:\n" +
"- Try to find related type values in the available snippets list. If you find a relevant snippet for that block, use the name of that snippet for type, and add the id if the snippet as preferredsnippetid." +
"- If there is no relevant snippet, use such layout `type` values: hero, text, features, text with image, call to action, product list, team members, testimonial, event list, blogpost, article, video player, audio player, etc\n" +
"X Restrictions:" +
"- DO **NOT** modify the photo urls in any way." +
"- Do **NOT** generate or assume new photo URLs.\n" +
"- Do **NOT** modify photo URLs in any way.\n" +
"- Do **NOT** skip or deny ANY part of the provided text. All of the provided text should be displayed as html \n" +
"- Do **NOT** assume ANY content, like missing prices, missing links, etc. \n" +
"✅ Final output: JSON only, well grouped, no extra explanation or formatting, no markdown, no ``` markers.\n";
return userMessage;
}
}
public static class HtmlRendering
{
public static string GetHtmlRenderingSystemPromptForTextAndErrorResult(string language, string pageTitle, List<HtmlSnippet> htmlToUse, Dictionary<string, string>? photos, string[]? topics)
{
//var sb = new StringBuilder("You are a helpful assistant generating HTML content in " + language + " using Bootstrap.\n\n" +
// "### Rules to Follow: \n" +
// "- Please generate clean and structured HTML that goes between the menu and the footer of a Bootstrap5 webpage.\n" +
// "- DO NOT include `<head>`, `<body>` tags — only content that goes inside the Bootstrap container.\n" +
// "- Use `<h1 class='p-3'>` for the title: " + pageTitle + ".\n" +
// "- Structure content using **separate `row` elements** for different sections.\n" +
// "- Use Bootstrap classes to ensure proper spacing and alignment.\n" +
// " - Use 'row justify-content-center' for layout, 'col-xx-x' classes for columns (ONLY IF multiple columns are needed), and always use 'img-fluid' class for images.\r\n" +
// " - Do NOT use 'col' class if there is only one column to display in the row.\n" +
// "- Do NOT use additional class for paragraphs!\n" +
// "- Do NOT nest images inside paragraphs!\n" +
// "- Ensure clear **separation of content into multiple sections if multiple snippets are provided**.\n");
//if (htmlToUse != null && htmlToUse.Any())
//{
// sb.AppendLine("### Using Provided Snippets:\n" +
// "- You have been given **multiple HTML snippets**:\n");
// foreach (var snippet in htmlToUse)
// {
// sb.AppendLine($"{snippet.Id}: {snippet.Name}: {snippet.Html}.\n");
// sb.AppendLine($"Type: {snippet.Type}, Tags: {snippet.Tags}, Variant: {snippet.Variant}.\n");
// }
// sb.AppendLine("**DO NOT merge them into one**.\n" +
// "- Use each snippet **as a separate section** inside its own div.\n" +
// "- Using all the snippets is NOT mandatory, use them only if you identified content that fits in a specific snippet.\n" +
// "- Validate if there are multiple variants of a snippet, and choose the best option, for example if the block has text and photo url, choose the variant that has image in it.\n" +
// "- Do NOT push all text of a block in the title <h> tags, always pick a short title, followed by the rest of the content as a paragraph.\n" +
// "- If a snippet contains a button, ensure it is placed inside a `div.text-center` for proper alignment.\n");
//}
//if (photos != null && photos.Any())
//{
// sb.AppendLine("### Handling Photos:\n" +
// "- ONLY use the provided photo URLs **as they are**, without modification.\n" +
// "- Do **NOT** generate or assume image URLs.\n" +
// "- Use these photos where appropriate:\n" +
// " " + string.Join(", ", photos.Select(kv => $"{kv.Key}: {kv.Value}")) + "\n\n" +
// "- Example usage:\n" +
// " <img src='" + photos.First().Value + "' class='img-fluid' alt='" + photos.First().Key + "' />\n" +
// "DO NOT modifiy the photo urls in any way.");
//}
//if (topics != null && topics.Any())
//{
// sb.AppendLine("### Generating Topic Buttons:\n" +
// "Start this section with a title `Related`\n" +
// "- Create a **separate button** for each topic.\n" +
// $"- Make sure the topics are in {language}, if not, translate the name of them." +
// "- Each button should use `btn btn-primary` and call `callAI('{original_non_translated_topicName}')` on click.\n" +
// "- List of topics:\n" +
// " " + string.Join(", ", topics) + "\n\n" +
// "- Example:\n" +
// " <button class='btn btn-primary' onclick='callAI(\"" + topics.FirstOrDefault() + "\")'>" + topics.FirstOrDefault() + "</button>\n" +
// "Put this section always as last, on the bottom of the page.\n");
//}
//else
//{
// sb.AppendLine("- **No topics provided** → **Do NOT generate topic buttons.**");
//}
//sb.AppendLine("- DO **NOT** merge different content sections.\n" +
// "- DO **NOT** wrap the entire content in a single `div`— use separate section tags.\n" +
// "- DO **NOT** modify the photo urls in any way.\n" +
// "- Do **NOT** generate or assume new photo URLs.\n" +
// "- Do **NOT** skip or deny ANY part of the provided text. All of the provided text should be displayed as html\n" +
// "- Do **NOT** add ANY extra content, like assumed prices, or assumed links.\n" +
// "- If the snippet contains an image, but there is no photo url available, SKIP adding the image tag.\n" +
// "- **Never** add explanations or start with ```html syntax markers.\n");
var sb = new StringBuilder($"You are a helpful assistant generating HTML in {language} using Bootstrap 5.\n\n" +
"### General Instructions:\n" +
"- Output only the **HTML content** between the menu and footer.\n" +
"- DO NOT include `<head>`, `<body>`, or markdown formatting.\n" +
"- Use `<h1 class='p-3'>` for the title: " + pageTitle + ".\n" +
"- Structure sections inside separate `<section>` or `<div class='row'>` blocks.\n" +
"- Use Bootstrap spacing and layout classes:\n" +
" - Use `row justify-content-center` for rows.\n" +
" - Use `col-xx-x` only for multiple-column layouts.\n" +
" - Always use `img-fluid` for images.\n" +
"- Avoid: unnecessary paragraph classes, nesting `<img>` inside `<p>`, or using `col` for single-column content.\n\n");
if (htmlToUse != null && htmlToUse.Any())
{
sb.AppendLine("### Snippet Handling:\n" +
"- You are provided with multiple HTML snippets:\n");
foreach (var snippet in htmlToUse)
{
sb.AppendLine($"{snippet.Id}: {snippet.Name}: {snippet.Html}\n");
sb.AppendLine($"Type: {snippet.Type}, Tags: {snippet.Tags}, Variant: {snippet.Variant}\n");
}
sb.AppendLine(
"- Use each snippet as a separate `<section>`.\n" +
"- DO NOT merge snippets into one block.\n" +
"- Use snippets only if they match the content block.\n" +
"- Prefer variants with images if image content is present.\n" +
"- Always use a short title in `<h>` and place body text in a `<p>`.\n" +
"- If snippet includes a button, wrap it in `<div class='text-center'>`.\n");
}
if (photos != null && photos.Any())
{
sb.AppendLine("### Photo Usage:\n" +
"- Use ONLY the following image URLs as-is (no changes):\n" +
string.Join(", ", photos.Select(kv => $"{kv.Key}: {kv.Value}")) + "\n" +
"- Example:\n" +
$" <img src='{photos.First().Value}' class='img-fluid' alt='{photos.First().Key}' />\n" +
"- DO NOT generate or modify photo URLs.\n");
}
if (topics != null && topics.Any())
{
sb.AppendLine("### Topic Buttons:\n" +
"- Place this section last, titled `Related`.\n" +
"- Generate a `btn btn-primary` for each topic, calling `callAI('{original_non_translated_topicName}')`.\n" +
"- Translate topic names to " + language + " if needed.\n" +
"- Example:\n" +
$" <button class='btn btn-primary' onclick='callAI(\"{topics.FirstOrDefault()}\")'>{topics.FirstOrDefault()}</button>\n");
}
else
{
sb.AppendLine("- No topics provided → DO NOT generate topic buttons.");
}
sb.AppendLine("\n### DO NOT:\n" +
"- Merge different content blocks.\n" +
"- Remove javascript or <script> tags"+
"- Wrap the full output in a single `div`.\n" +
"- Skip any provided content.\n" +
"- Add assumed text (e.g. prices, links).\n" +
"- Add `<img>` if no image URL exists.\n" +
"- Include explanation, markdown, or ` ```html` markers.\n");
return sb.ToString();
}
public static string HtmlRenderingUserPromptForTextAndErrorResult = "Create a perfect, production ready, structured, responsive Bootstrap 5 webpage " +
"content from the provided layout and available html snippets. \r \n" +
"Read the layout plan, analyze the blocks, and identify if there is any matching html snippets for each block folowing these rules: \r \n" +
"- If there is no matching html snippet, generate the bootstrap 5 based layout for the given block. Else parse the block content into the available code snippet." +
"- For each block, add am opening and closing comment with the name of the block like: <!-- Hero start --> \r \n" +
"- Do not remove script tags from the provided snippets. \r \n" +
"If the block's rawcontent contains photo url, display that photo within that section, not elsewhere.";
public static string GetHtmlRenderingAssistantMessageForTextAndErrorResult(LayoutPlan layoutPlan)
{
string assistantMessage = "`Provided layout plan, that contains the text to be displayed as HTML`:";
foreach (var block in layoutPlan.Blocks)
{
assistantMessage += $"Block type: {block.Type}, block content: {block.RawContent}";
}
return assistantMessage;
}
public static string GetHtmlRenderingSystemPromptForMethodResult(string language, string methodToCall, string methodParameter)
{
var sb = new StringBuilder("You are a helpful assistant generating HTML content in " + language + " using Bootstrap. \n\n" +
"### Rules to Follow: \n" +
"- Please generate clean and structured HTML that goes inside a Bootstrap container.\n" +
"- DO NOT include `<head>`, `<body>` tags — only content inside the Bootstrap container.\n" +
"- Use `<h1 class='p-3'>` for a short title followed by the content, based on the user's query.\n" +
"- Structure content using **separate `row` elements** for different sections.\n" +
"- Use Bootstrap classes to ensure proper spacing and alignment.\n" +
" - Use 'row justify-content-center' for layout, 'col-xx-x' classes for columns (ONLY IF multiple columns are needed), and always use 'img-fluid' class for images.\r\n" +
" - Do NOT use 'col' class if there is only one column to display in the row. \n" +
"- Do NOT use additional class for paragraphs! \n" +
"- Do NOT nest images inside paragraphs! \n" +
"- Ensure clear **separation of content into multiple sections if multiple snippets are provided**.\n");
if (!string.IsNullOrEmpty(methodToCall))
{
sb.AppendLine("- At the END of the content, include a **single** Bootstrap button (`btn btn-primary`) that calls `" + methodToCall + "` with `" + methodParameter + "` on click.\n");
}
sb.AppendLine("- DO **NOT** merge different content sections.\n" +
"- DO **NOT** wrap the entire content in a single `div`— use separate `row` divs.\n" +
"- Do **NOT** skip or deny ANY part of the provided text. All of the provided text should be displayed as html\n" +
"- Do **NOT** assume ANY content, like missing prices, missing links. \n" +
"- If the snippet contains an image, but there is no photo url available, SKIP adding the image tag." +
"- **Never** add explanations or start with ```html syntax markers.\n");
return sb.ToString();
}
}
}
}

11
Models/AppLog.cs Normal file
View File

@ -0,0 +1,11 @@
namespace BLAIzor.Models
{
public class AppLog
{
public int Id { get; set; }
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public string Severity { get; set; } = "Info"; // "Info", "Warning", "Error"
public string Message { get; set; }
public string? Details { get; set; }
}
}

14
Models/ContentChunk.cs Normal file
View File

@ -0,0 +1,14 @@
namespace BLAIzor.Models
{
public class ContentChunk
{
public int Id { get; set; }
public int ContentItemId { get; set; } // FK to ContentItem
public string QdrantPointId { get; set; }
public int ChunkIndex { get; set; }
public string? VectorHash { get; set; }
public DateTime CreatedAt { get; set; }
public ContentItem ContentItem { get; set; }
}
}

21
Models/ContentGroup.cs Normal file
View File

@ -0,0 +1,21 @@
using NuGet.ContentModel;
namespace BLAIzor.Models
{
public class ContentGroup
{
public int Id { get; set; }
public int SiteInfoId { get; set; } // FK to SiteInfo
public string Name { get; set; } // e.g. "Blog", "Manuals"
public string Slug { get; set; } // e.g. "blog", "manuals"
public string Type { get; set; } // "blogpost", "manual", etc.
public int VectorSize { get; set; }
public string EmbeddingModel { get; set; }
public int Version { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime LastUpdated { get; set; }
public SiteInfo SiteInfo { get; set; }
public ICollection<ContentItem> Items { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace BLAIzor.Models
{
public class ContentGroupModel
{
public int SiteInfoId { get; set; }
public ContentGroup ContentGroup { get; set; }
public List<ContentItemModel> ContentItems { get; set; } = new();
}
}

20
Models/ContentItem.cs Normal file
View File

@ -0,0 +1,20 @@
namespace BLAIzor.Models
{
public class ContentItem
{
public int Id { get; set; }
public int ContentGroupId { get; set; } // FK to ContentGroup
public string Title { get; set; }
public string Description { get; set; }
public string Content { get; set; }
public string Language { get; set; }
public string Tags { get; set; } // comma-separated tags
public bool IsPublished { get; set; }
public int Version { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime LastUpdated { get; set; }
public ContentGroup ContentGroup { get; set; }
public ICollection<ContentChunk> Chunks { get; set; }
}
}

View File

@ -0,0 +1,11 @@
namespace BLAIzor.Models
{
public class ContentItemModel
{
public ContentItem ContentItem { get; set; }
public List<ContentChunk> Chunks { get; set; } = new();
// Optional: enriched vector data pulled from Qdrant
public List<WebPageContent> VectorPoints { get; set; } = new();
}
}

View File

@ -2,8 +2,14 @@
{ {
public class UploadedFilesModel public class UploadedFilesModel
{ {
public List<string> Images { get; set; } = new(); public List<UploadedImage> Images { get; set; } = new();
public List<string> Videos { get; set; } = new(); public List<string> Videos { get; set; } = new();
public List<string> Audio { get; set; } = new(); public List<string> Audio { get; set; } = new();
} }
public class UploadedImage
{
public string OriginalUrl { get; set; } = string.Empty;
public string ThumbnailUrl { get; set; } = string.Empty;
}
} }

View File

@ -12,18 +12,32 @@ namespace BLAIzor.Models
[Required] [Required]
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public int PointId { get; set; } public string? Slug { get; set; }
//public int PointId { get; set; }
public int SortOrder { get; set; } public int SortOrder { get; set; }
public bool ShowInMainMenu { get; set; } = false; public bool ShowInMainMenu { get; set; } = false;
public string? StoredHtml { get; set; } public string? StoredHtml { get; set; }
public Guid? QdrantPointId { get; set; } //public Guid? QdrantPointId { get; set; }
[ForeignKey("ContentGroup")]
public int? ContentGroupId { get; set; } // optional link to a ContentGroup
public ContentGroup? ContentGroup { get; set; }
public int? ContentItemId { get; set; } // optional link to a specific ContentItem
public ContentItem? ContentItem { get; set; }
[ForeignKey("SiteInfo")] [ForeignKey("SiteInfo")]
public int SiteInfoId { get; set; } public int SiteInfoId { get; set; }
public SiteInfo SiteInfo { get; set; } = null!; public SiteInfo SiteInfo { get; set; } = null!;
[ForeignKey("MenuItem")]
public int? ParentId { get; set; }
public MenuItem? Parent { get; set; }
public ICollection<MenuItem> Children { get; set; } = new List<MenuItem>();
} }
} }

View File

@ -32,8 +32,9 @@ namespace BLAIzor.Models
public string? Entity { get; set; } public string? Entity { get; set; }
public string? DefaultLanguage { get; set; } public string? DefaultLanguage { get; set; }
public string? BackgroundVideo { get; set; } public string? BackgroundVideo { get; set; }
public string? VectorCollectionName { get; set; }
public string? EmbeddingService { get; set; } public string? EmbeddingService { get; set; }
public List<ContentGroup> ContentGroups { get; set; } = new();
// Navigation property for IdentityUser // Navigation property for IdentityUser
public IdentityUser User { get; set; } public IdentityUser User { get; set; }

View File

@ -1,10 +1,11 @@
using DateTime = System.DateTime; using Qdrant.Client.Grpc;
using DateTime = System.DateTime;
namespace BLAIzor.Models namespace BLAIzor.Models
{ {
public class WebPageContent public class WebPageContent
{ {
public int Id { get; set; } public PointId Id { get; set; }
public string UId { get; set; } public string UId { get; set; }
public string Type { get; set; } public string Type { get; set; }
public int SiteId { get; set; } public int SiteId { get; set; }
@ -13,6 +14,24 @@ namespace BLAIzor.Models
public string Content { get; set; } public string Content { get; set; }
public float[] Vectors { get; set; } public float[] Vectors { get; set; }
public DateTime LastUpdated { get; set; } public DateTime LastUpdated { get; set; }
public WebPageContent()
{
}
public WebPageContent(PointId id, string uId, string type, int siteId, string name, string description, string content, float[] vectors, DateTime lastUpdated)
{
Id = id;
UId = uId;
Type = type;
SiteId = siteId;
Name = name;
Description = description;
Content = content;
Vectors = vectors;
LastUpdated = lastUpdated;
}
} }
} }

View File

@ -0,0 +1,9 @@
namespace BLAIzor.Models
{
public class WebsiteContentModel
{
public int SiteInfoId { get; set; }
public List<ContentGroup> ContentGroups { get; set; }
public List<ContentItemModel> ContentItems { get; set; } = new();
}
}

View File

@ -66,7 +66,9 @@ builder.Services.AddScoped<OpenAiRealtimeService>();
builder.Services.AddScoped<CerebrasAPIService>(); builder.Services.AddScoped<CerebrasAPIService>();
builder.Services.AddScoped<CssInjectorService>(); builder.Services.AddScoped<CssInjectorService>();
builder.Services.AddScoped<LocalVectorSearchService>(); builder.Services.AddScoped<LocalVectorSearchService>();
builder.Services.AddScoped<WebsiteContentLoaderService>();
builder.Services.AddHostedService<TempFileCleanupService>(); builder.Services.AddHostedService<TempFileCleanupService>();
builder.Services.AddScoped<ISimpleLogger, SimpleLogger>();
builder.Services.AddServerSideBlazor().AddCircuitOptions(options => options.DetailedErrors = true).AddHubOptions(options => builder.Services.AddServerSideBlazor().AddCircuitOptions(options => options.DetailedErrors = true).AddHubOptions(options =>
{ {
options.MaximumReceiveMessageSize = 1024000; // e.g. 100 KB options.MaximumReceiveMessageSize = 1024000; // e.g. 100 KB

View File

@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BLAIzor.Data;
using BLAIzor.Models;
using BLAIzor.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
namespace SeemGen.Tests;
public class ContentEditorServiceTests
{
[Fact]
public async Task SaveAndSyncContentItemAsync_UpdatesFieldsAndRechunks_WhenContentChanged()
{
// Arrange
var contentItemId = 1;
var oldContent = "Old content";
var newContent = "New content for chunking";
var collectionName = "test_collection";
var contentGroup = new ContentGroup { Id = 1, Name = "Group", SiteInfoId = 1, Items = new List<ContentItem>() };
var existingChunks = new List<ContentChunk>
{
new ContentChunk { Id = 1, ContentItemId = contentItemId, QdrantPointId = Guid.NewGuid().ToString(), ChunkIndex = 0, CreatedAt = DateTime.UtcNow }
};
var existingItem = new ContentItem
{
Id = contentItemId,
Title = "Old Title",
Description = "Old Desc",
Content = oldContent,
Language = "en",
Tags = "tag1",
IsPublished = true,
LastUpdated = DateTime.UtcNow,
ContentGroup = contentGroup,
Chunks = existingChunks
};
contentGroup.Items.Add(existingItem);
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
var dbContext = new ApplicationDbContext(options);
dbContext.ContentItems.Add(existingItem);
dbContext.ContentChunks.AddRange(existingChunks);
dbContext.SaveChanges();
var serviceScopeFactoryMock = new Mock<IServiceScopeFactory>();
var scopeMock = new Mock<IServiceScope>();
var providerMock = new Mock<IServiceProvider>();
providerMock.Setup(x => x.GetService(typeof(ApplicationDbContext))).Returns(dbContext);
scopeMock.Setup(x => x.ServiceProvider).Returns(providerMock.Object);
serviceScopeFactoryMock.Setup(x => x.CreateScope()).Returns(scopeMock.Object);
var qdrantServiceMock = new Mock<QDrantService>(null);
qdrantServiceMock.Setup(x => x.DeletePointsAsync(It.IsAny<Guid[]>(), collectionName)).Returns(Task.CompletedTask);
qdrantServiceMock.Setup(x => x.QDrantInsertManyAsync(It.IsAny<List<WebPageContent>>(), collectionName)).Returns(Task.CompletedTask);
var openAIEmbeddingServiceMock = new Mock<OpenAIEmbeddingService>();
openAIEmbeddingServiceMock.Setup(x => x.GenerateEmbeddingAsync(It.IsAny<string>())).ReturnsAsync(new float[] { 0.1f, 0.2f });
var localEmbeddingServiceMock = new Mock<LocalEmbeddingService>();
localEmbeddingServiceMock.Setup(x => x.GenerateEmbeddingAsync(It.IsAny<string>())).ReturnsAsync(new float[] { 0.1f, 0.2f });
var openAIApiServiceMock = new Mock<OpenAIApiService>(MockBehavior.Loose, null, null);
var htmlSnippetProcessorMock = new Mock<HtmlSnippetProcessor>(qdrantServiceMock.Object, openAIEmbeddingServiceMock.Object, localEmbeddingServiceMock.Object, null);
var scopedContentServiceMock = new Mock<ScopedContentService>(dbContext, serviceScopeFactoryMock.Object);
var configMock = new Mock<IConfiguration>();
configMock.Setup(x => x.GetSection("AiSettings").GetValue<string>("EmbeddingService")).Returns("openai");
var service = new ContentEditorService(
openAIApiServiceMock.Object,
qdrantServiceMock.Object,
openAIEmbeddingServiceMock.Object,
localEmbeddingServiceMock.Object,
htmlSnippetProcessorMock.Object,
serviceScopeFactoryMock.Object,
scopedContentServiceMock.Object,
configMock.Object
);
var dto = new ContentItem
{
Id = contentItemId,
Title = "New Title",
Description = "New Desc",
Content = newContent,
Language = "fr",
Tags = "tag2",
IsPublished = false,
LastUpdated = DateTime.UtcNow
};
// Act
var result = await service.SaveAndSyncContentItemAsync(dto, collectionName, false);
// Assert
Assert.Equal(dto.Title, result.Title);
Assert.Equal(dto.Description, result.Description);
Assert.Equal(dto.Content, result.Content);
Assert.Equal(dto.Language, result.Language);
Assert.Equal(dto.Tags, result.Tags);
Assert.Equal(dto.IsPublished, result.IsPublished);
Assert.True(result.Version > 0);
Assert.NotEmpty(dbContext.ContentChunks.Where(c => c.ContentItemId == contentItemId));
qdrantServiceMock.Verify(x => x.DeletePointsAsync(It.IsAny<Guid[]>(), collectionName), Times.Once);
qdrantServiceMock.Verify(x => x.QDrantInsertManyAsync(It.IsAny<List<WebPageContent>>(), collectionName), Times.Once);
}
}

View File

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.6" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BLAIzor.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,10 @@
namespace SeemGen.Tests;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}

File diff suppressed because one or more lines are too long

View File

@ -82,6 +82,10 @@ namespace BLAIzor.Services
using var responseStream = await response.Content.ReadAsStreamAsync(); using var responseStream = await response.Content.ReadAsStreamAsync();
using var document = await JsonDocument.ParseAsync(responseStream); using var document = await JsonDocument.ParseAsync(responseStream);
var inputTokens = document.RootElement.GetProperty("usage").GetProperty("prompt_tokens").GetInt32();
var outputTokens = document.RootElement.GetProperty("usage").GetProperty("completion_tokens").GetInt32();
var sum = inputTokens + outputTokens;
Console.WriteLine($"USAGE STATS - Tokens: {inputTokens.ToString()} + {outputTokens.ToString()} = {sum.ToString()}");
return document.RootElement return document.RootElement
.GetProperty("choices")[0] .GetProperty("choices")[0]

View File

@ -0,0 +1,215 @@
using BLAIzor.Data;
using BLAIzor.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Identity.Client;
namespace BLAIzor.Services
{
public class ContentEditorAIService
{
//private readonly AIService _aiService;
private readonly OpenAIApiService _openAIApiService;
//private readonly ApplicationDbContext _context;
private readonly QDrantService _qDrantService;
private readonly HtmlSnippetProcessor _htmlSnippetProcessor;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ScopedContentService _scopedContentService;
private readonly ContentEditorService _contentEditorService;
public static IConfiguration? _configuration;
public ContentEditorAIService(/*AIService aiService,*/ OpenAIApiService openAIApiService, /*ApplicationDbContext context,*/ QDrantService qDrantService, HtmlSnippetProcessor htmlSnippetProcessor, IServiceScopeFactory serviceScopeFactory, ScopedContentService scopedContentService, IConfiguration? configuration, ContentEditorService contentEditorService)
{
//_aiService = aiService;
_openAIApiService = openAIApiService;
//_context = context;
_qDrantService = qDrantService;
_htmlSnippetProcessor = htmlSnippetProcessor;
_serviceScopeFactory = serviceScopeFactory;
_scopedContentService = scopedContentService;
_configuration = configuration;
_contentEditorService = contentEditorService;
}
private string GetAiEmbeddingSettings() =>
_configuration?.GetSection("AiSettings")?.GetValue<string>("EmbeddingService") ?? string.Empty;
// Existing methods
public async Task<List<string>> GetMenuSuggestionsAsync(string sessionId, string prompt)
{
string systemMessage = "You are a helpful assistant that helps the user in creating a website.";
var result = await _openAIApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, prompt);
return result.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim('-').Trim())
.ToList() ?? new List<string>();
}
public async Task<string> GetGeneratedContentAsync(string sessionId, string prompt)
{
string systemMessage = "You are a helpful assistant that helps the user in creating a website. Do not generate html, just plain text.";
var result = await _openAIApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, prompt);
return result;
}
public async Task<string> ProcessMenuItems(int SiteId, bool hasCollection, List<MenuItemModel> ExtractedMenuItems, string subject, bool menuItemsSaved, bool updateVectorDatabase)
{
try
{
if (!hasCollection)
{
//create colection
await _qDrantService.CreateQdrantCollectionAsync("Site" + SiteId.ToString());
hasCollection = true;
}
else
{
//check if Collection is really created already
var collection = await _qDrantService.GetCollectionCount("Site" + SiteId.ToString());
if (collection != null)
{
var siteInfo = await _contentEditorService.GetSiteInfoByIdAsync(SiteId);
if (siteInfo.EmbeddingService != null)
{
var currentEmbeddingServiceName = GetAiEmbeddingSettings();
if (siteInfo.EmbeddingService == currentEmbeddingServiceName)
{
//drop existing collection
await _qDrantService.DeleteCollectionAsync("Site" + SiteId.ToString());
//create will automatically handle the new vector size
await _qDrantService.CreateQdrantCollectionAsync("Site" + SiteId.ToString());
}
}
}
}
var updateQdrantList = ExtractedMenuItems.OrderBy(mi => mi.MenuItem.ContentGroup.Items.FirstOrDefault().Id).ToList(); //FIXXXXXXX
for (int i = 0; i < updateQdrantList.Count(); i++)
{
MenuItem menuItem = new MenuItem();
menuItem.Name = ExtractedMenuItems[i].MenuItem.Name;
menuItem.SortOrder = i;
menuItem.SiteInfoId = SiteId;
menuItem.ShowInMainMenu = true;
if (!string.IsNullOrEmpty(ExtractedMenuItems[i].Content)) //if we have content in the menuItem, let's update the vector DB
{
if (updateVectorDatabase)
{
Guid guid = Guid.NewGuid();
WebPageContent content = new WebPageContent();
content.Id = guid;
content.UId = guid.ToString();
content.Type = "page";
content.SiteId = SiteId;
content.Name = ExtractedMenuItems[i].MenuItem.Name;
if (ExtractedMenuItems[i].ContentDescription == null)
{
content.Description = $"A section called {ExtractedMenuItems[i].MenuItem.Name} of a website of {subject}";
}
else
{
content.Description = ExtractedMenuItems[i].ContentDescription;
}
content.Content = ExtractedMenuItems[i].Content;
content.LastUpdated = DateTime.UtcNow;
//menuItem.QdrantPointId = guid; //FIXXXXXXX
//menuItem.PointId = i; //FIXXXXXXX
await _htmlSnippetProcessor.ProcessAndStoreWebContentAsync(guid, content, SiteId);
}
else
{
//menuItem.PointId = i; //FIXXXXXXX
//menuItem.QdrantPointId = ExtractedMenuItems[i].MenuItem.QdrantPointId; //FIXXXXXXX
}
}
if (!menuItemsSaved) //menuItems just extracted, no content yet because that can be generated only after menuItems have been saved first
{
//menuItem.Name = ExtractedMenuItems[i].MenuItem.Name;
//menuItem.SortOrder = i;
//menuItem.SiteInfoId = SiteId;
//menuItem.ShowInMainMenu = true;
await _contentEditorService.AddMenuItemAsync(menuItem);
}
else //menuItems available already
{
var menuItems = await _contentEditorService.GetMenuItemsBySiteIdAsync(SiteId);
var thisItem = menuItems.FirstOrDefault(x => x.SiteInfoId == SiteId && x.Id == ExtractedMenuItems[i].MenuItem.Id);
if (thisItem == null)
{
//new menu item?
await _contentEditorService.AddMenuItemAsync(menuItem);
}
else
{
//thisItem.PointId = menuItem.PointId; //FIXXXXXXX
//thisItem.QdrantPointId = menuItem.QdrantPointId; //FIXXXXXXX
thisItem.SortOrder = ExtractedMenuItems[i].MenuItem.SortOrder;
thisItem.ShowInMainMenu = ExtractedMenuItems[i].MenuItem.ShowInMainMenu;
await _contentEditorService.UpdateMenuItemAsync(thisItem);
}
}
}
//await ContentEditorService.SaveMenuItemsAsync(menuItems);
//menuItemsSaved = true;
return "OK";
}
catch
{
return "Error";
}
}
//public async Task<string> AnalyzeFormFieldsFromText(string text)
//{
// string systemMessage = "You are a helpful assistant that extracts structured form field data from plain text.";
// string userMessage =
// "This is the plain text extracted from a form document. " +
// "Please extract all input fields a user is expected to fill in and return a JSON array of objects.\n\n" +
// "Each object should have the following structure:\n" +
// "- `label`: The label or prompt for the input\n" +
// "- `type`: One of `text`, `number`, `date`, `checkbox`, `radio`, or `textarea`\n" +
// "- `options`: If type is `radio` or `checkbox`, include the possible options as a list of strings; otherwise, return an empty list\n" +
// "- `section`: The section title this field belongs to (if any)\n" +
// "- `repeatable`: true if this field (or set of fields) is expected to be repeated for multiple entries (like multiple employers or qualifications), otherwise false\n\n" +
// "Return only the JSON array, without any explanation or formatting like ```json.\n\n" +
// "Here is the form text:\n\n" +
// $"---\n{text}\n---";
// return await _openAIApiService.GetSimpleChatGPTResponseNoSession(systemMessage, userMessage).ConfigureAwait(false);
//}
public async Task<string> AnalyzeGroupedFormFieldsFromText(string text)
{
string systemMessage = "You are a helpful assistant that analyzes plain form text and extracts structured, grouped form field definitions.";
string userMessage =
"This is a plain text version of a printed form. Your task is to extract the **input fields** that a user must fill in. " +
"Group related fields under sections where applicable, and return them in a JSON structure.\n\n" +
"Each group should follow this structure:\n" +
"- `groupName`: Name of the logical section or group (e.g. 'Work Experience')\n" +
"- `repeatable`: true if this section can appear multiple times (e.g. multiple employers, qualifications)\n" +
"- `fields`: an array of fields within the group\n\n" +
"Each field must have:\n" +
"- `label`: The field label/question\n" +
"- `type`: one of `text`, `number`, `date`, `textarea`, `checkbox`, or `radio`\n" +
"- `options`: list of string values if type is `radio` or `checkbox`, or an empty list otherwise\n" +
"- `section`: optional section name (can match group name or be more specific)\n\n" +
"Return only the JSON array. Do not include any explanation or formatting like ```json.\n\n" +
$"Here is the form text:\n\n---\n{text}\n---";
return await _openAIApiService.GetSimpleChatGPTResponseNoSession(systemMessage, userMessage).ConfigureAwait(false);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -212,7 +212,7 @@ namespace BLAIzor.Services
// } // }
//} //}
public async Task ProcessAndStoreWebContentAsync(int id, WebPageContent pageContent, int siteId) public async Task ProcessAndStoreWebContentAsync(PointId id, WebPageContent pageContent, int siteId)
{ {
_qdrantApiKey = GetApiKey(); _qdrantApiKey = GetApiKey();

20
Services/ISimpleLogger.cs Normal file
View File

@ -0,0 +1,20 @@
namespace BLAIzor.Services
{
public interface ISimpleLogger
{
Task InfoAsync(string message, string? details = null);
Task WarnAsync(string message, string? details = null);
Task ErrorAsync(string message, string? details = null);
void SetLevel(LogLevel level);
void EnableConsole(bool enabled);
}
public enum LogLevel
{
Info = 1,
Warning = 2,
Error = 3,
None = 4 // disables all logs
}
}

View File

@ -85,7 +85,10 @@ namespace BLAIzor.Services
using var responseStream = await response.Content.ReadAsStreamAsync(); using var responseStream = await response.Content.ReadAsStreamAsync();
using var document = await JsonDocument.ParseAsync(responseStream); using var document = await JsonDocument.ParseAsync(responseStream);
var inputTokens = document.RootElement.GetProperty("usage").GetProperty("prompt_tokens").GetInt32();
var outputTokens = document.RootElement.GetProperty("usage").GetProperty("completion_tokens").GetInt32();
var sum = inputTokens + outputTokens;
Console.WriteLine($"USAGE STATS - Tokens: {inputTokens.ToString()} + {outputTokens.ToString()} = {sum.ToString()}");
return document.RootElement return document.RootElement
.GetProperty("choices")[0] .GetProperty("choices")[0]
.GetProperty("message") .GetProperty("message")

View File

@ -1,10 +1,12 @@
using BLAIzor.Models; using BLAIzor.Models;
using Google.Protobuf;
using Google.Protobuf.Collections; using Google.Protobuf.Collections;
using Newtonsoft.Json; using Newtonsoft.Json;
using Qdrant.Client; using Qdrant.Client;
using Qdrant.Client.Grpc; using Qdrant.Client.Grpc;
using System.Numerics; using System.Numerics;
using System.Text; using System.Text;
using static Qdrant.Client.Grpc.PointsUpdateOperation.Types;
namespace BLAIzor.Services namespace BLAIzor.Services
{ {
@ -41,7 +43,7 @@ namespace BLAIzor.Services
public async Task<int> GetCollectionCount(string collectionName) public async Task<int> GetCollectionCount(string collectionName)
{ {
_apiKey = GetApiKey();
var client = new QdrantClient(_qdrantHost, 6334, true, _apiKey); var client = new QdrantClient(_qdrantHost, 6334, true, _apiKey);
var result = await client.CountAsync( var result = await client.CountAsync(
collectionName: collectionName, collectionName: collectionName,
@ -51,52 +53,6 @@ namespace BLAIzor.Services
return Convert.ToInt32(result); return Convert.ToInt32(result);
} }
//public async Task CreateQdrantCollectionAsync()
//{
// _apiKey = GetApiKey();
// var httpClient = new HttpClient();
// httpClient.DefaultRequestHeaders.Clear();
// httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");
// var collectionName = "web_content";
// var createCollectionPayload = new
// {
// vectors = new { size = 0, distance = "" } // Adjust size based on embedding model
// };
// var embeddingServiceProvider = GetAiEmbeddingSettings();
// if (embeddingServiceProvider == "local")
// {
// createCollectionPayload = new
// {
// vectors = new { size = 1024, distance = "Cosine" } // Adjust size based on embedding model
// };
// }
// else
// {
// createCollectionPayload = new
// {
// vectors = new { size = 1536, distance = "Cosine" } // Adjust size based on embedding model
// };
// }
// var content = new StringContent(JsonConvert.SerializeObject(createCollectionPayload), Encoding.UTF8, "application/json");
// var response = await httpClient.PutAsync($"{qdrantUrl}/collections/{collectionName}", content);
// if (response.IsSuccessStatusCode)
// {
// Console.WriteLine("Collection created successfully!" + response.Content.ReadAsStringAsync());
// }
// else
// {
// Console.WriteLine($"Failed to create collection: {response.StatusCode}");
// }
//}
public async Task<string> GetCollectionBySiteIdAsync(int siteId) public async Task<string> GetCollectionBySiteIdAsync(int siteId)
{ {
return await GetCollectionByNameAsync("Site" + siteId); return await GetCollectionByNameAsync("Site" + siteId);
@ -109,12 +65,19 @@ namespace BLAIzor.Services
} }
public async Task<bool> CollectionExistsAsync(string collectionName)
{
_apiKey = GetApiKey();
var client = new QdrantClient(_qdrantHost, 6334, true, _apiKey);
return await client.CollectionExistsAsync(collectionName);
}
public async Task<string> GetCollectionByNameAsync(string collectionName) public async Task<string> GetCollectionByNameAsync(string collectionName)
{ {
_apiKey = GetApiKey(); _apiKey = GetApiKey();
var client = new QdrantClient(_qdrantHost, 6334, true, _apiKey); var client = new QdrantClient(_qdrantHost, 6334, true, _apiKey);
bool doesExist = await client.CollectionExistsAsync(collectionName); bool doesExist = await CollectionExistsAsync(collectionName);
if (doesExist) if (doesExist)
{ {
var response = await client.GetCollectionInfoAsync(collectionName); var response = await client.GetCollectionInfoAsync(collectionName);
@ -140,7 +103,7 @@ namespace BLAIzor.Services
} }
public async Task CreateQdrantCollectionAsync(string collectionName) public async Task<bool> CreateQdrantCollectionAsync(string collectionName)
{ {
_apiKey = GetApiKey(); _apiKey = GetApiKey();
var httpClient = new HttpClient(); var httpClient = new HttpClient();
@ -175,35 +138,204 @@ namespace BLAIzor.Services
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
Console.WriteLine("Collection created successfully!" + response.Content.ReadAsStringAsync()); Console.WriteLine("Collection created successfully!" + response.Content.ReadAsStringAsync());
return true;
} }
else else
{ {
Console.WriteLine($"Failed to create collection: {response.StatusCode}"); Console.WriteLine($"Failed to create collection: {response.StatusCode}");
return false;
} }
} }
public async Task<string> GetContentAsync(int siteId, int contentId) public async Task<List<WebPageContent>> GetPointsFromQdrantAsyncByPointIds(SiteInfo site, PointId[] pointIds)
{ {
_apiKey = GetApiKey(); _apiKey = GetApiKey();
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Clear(); List<WebPageContent> pageContent = new();
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");
if (string.IsNullOrEmpty(site.VectorCollectionName))
var response = await httpClient.GetAsync($"{qdrantUrl}/collections/Site{siteId.ToString()}/points/{contentId.ToString()}");
if (response.IsSuccessStatusCode)
{ {
var result = await response.Content.ReadAsStringAsync(); pageContent.Add(new WebPageContent(Guid.Empty,
//Console.WriteLine($"Query result: {result}"); Guid.Empty.ToString(),
return result; "404",
site.Id,
"ErrorPage",
"A 404 error page for non existing content",
"The page doesn't exist",
null,
DateTime.Now
));
} }
else else
{ {
Console.WriteLine($"Failed to query snippet: {response.StatusCode}"); var client = new QdrantClient(_qdrantHost, 6334, true, _apiKey);
return string.Empty; bool doesExist = await client.CollectionExistsAsync(site.VectorCollectionName);
if (!doesExist)
{
pageContent.Add(new WebPageContent(Guid.Empty,
Guid.Empty.ToString(),
"404",
site.Id,
"ErrorPage",
"A 404 error page for non existing content",
"The page doesn't exist",
null,
DateTime.Now
));
return pageContent;
}
else
{
var result = await client.RetrieveAsync(
collectionName: site.VectorCollectionName,
ids: pointIds,
withPayload: true,
withVectors: true
);
if (result.Count == 0)
{
pageContent.Add(new WebPageContent(Guid.Empty,
Guid.Empty.ToString(),
"404",
site.Id,
"ErrorPage",
"A 404 error page for non existing content",
"The page doesn't exist",
null,
DateTime.Now
));
}
else
{
foreach (var retrievedPoint in result)
{
float[] vectorArray = retrievedPoint.Vectors.Vector.Data.ToArray();
string idValue;
if (retrievedPoint.Id.HasNum)
{
idValue = retrievedPoint.Id.Num.ToString();
}
else
{
idValue = retrievedPoint.Id.Uuid.ToString();
}
Console.WriteLine($"POINTID: {idValue}, {retrievedPoint.Payload["name"]}");
pageContent.Add(new WebPageContent(retrievedPoint.Id,
retrievedPoint.Payload["uid"].StringValue,
retrievedPoint.Payload["type"].StringValue,
Convert.ToInt32(retrievedPoint.Payload["siteId"].IntegerValue),
retrievedPoint.Payload["name"].StringValue,
retrievedPoint.Payload["description"].StringValue,
retrievedPoint.Payload["content"].StringValue,
vectorArray,
Convert.ToDateTime(retrievedPoint.Payload["lastUpdated"].StringValue)
));
}
}
}
} }
return pageContent;
}
public async Task<List<WebPageContent>> GetPointsFromQdrantAsyncByIntegerPointIds(string collectionName, PointId[] pointIds)
{
_apiKey = GetApiKey();
List<WebPageContent> pageContent = new();
var client = new QdrantClient(_qdrantHost, 6334, true, _apiKey);
bool doesExist = await client.CollectionExistsAsync(collectionName);
var result = await client.RetrieveAsync(
collectionName: collectionName,
ids: pointIds,
withPayload: true,
withVectors: true
);
foreach (var retrievedPoint in result)
{
float[] vectorArray = retrievedPoint.Vectors.Vector.Data.ToArray();
string idValue;
if (retrievedPoint.Id.HasNum)
{
idValue = retrievedPoint.Id.Num.ToString();
}
else
{
idValue = retrievedPoint.Id.Uuid.ToString();
}
Console.WriteLine($"POINTID: {idValue}, {retrievedPoint.Payload["name"]}");
pageContent.Add(new WebPageContent(retrievedPoint.Id,
retrievedPoint.Payload["uid"].StringValue,
retrievedPoint.Payload["type"].StringValue,
Convert.ToInt32(retrievedPoint.Payload["siteId"].IntegerValue),
retrievedPoint.Payload["name"].StringValue,
retrievedPoint.Payload["description"].StringValue,
retrievedPoint.Payload["content"].StringValue,
vectorArray,
Convert.ToDateTime(retrievedPoint.Payload["lastUpdated"].StringValue)
));
}
return pageContent;
}
public async Task<List<WebPageContent>> GetPointFromQdrantAsyncByPointId(int siteId, int pointId)
{
_apiKey = GetApiKey();
//var httpClient = new HttpClient();
//httpClient.DefaultRequestHeaders.Clear();
//httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");
List<WebPageContent> contentList = new();
var client = new QdrantClient(_qdrantHost, 6334, true, _apiKey);
var result = await client.RetrieveAsync(
collectionName: $"Site{siteId.ToString()}",
id: Convert.ToUInt64(pointId),
withPayload: true,
withVectors: true
);
foreach (var retrievedPoint in result)
{
float[] vectorArray = retrievedPoint.Vectors.Vector.Data.ToArray();
string idValue;
if (retrievedPoint.Id.HasNum)
{
idValue = retrievedPoint.Id.Num.ToString();
}
else
{
idValue = retrievedPoint.Id.Uuid.ToString();
}
Console.WriteLine($"POINTID: {idValue}, {retrievedPoint.Payload["name"]}");
contentList.Add(new WebPageContent(retrievedPoint.Id,
retrievedPoint.Payload["uid"].StringValue,
retrievedPoint.Payload["type"].StringValue,
Convert.ToInt32(retrievedPoint.Payload["siteId"].IntegerValue),
retrievedPoint.Payload["name"].StringValue,
retrievedPoint.Payload["description"].StringValue,
retrievedPoint.Payload["content"].StringValue,
vectorArray,
Convert.ToDateTime(retrievedPoint.Payload["lastUpdated"].StringValue)
));
}
return contentList;
} }
public async Task<string> GetSnippetAsync(int snippetId, string collectionName) public async Task<string> GetSnippetAsync(int snippetId, string collectionName)
@ -233,22 +365,9 @@ namespace BLAIzor.Services
public async Task<int> QuerySnippetAsync(float[] queryVector, int limit = 1, string collectionName = "html_snippets") public async Task<int> QuerySnippetAsync(float[] queryVector, int limit = 1, string collectionName = "html_snippets")
{ {
_apiKey = GetApiKey(); _apiKey = GetApiKey();
//var httpClient = new HttpClient();
//httpClient.DefaultRequestHeaders.Clear();
//httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");
//var queryPayload = new var client = new QdrantClient(_qdrantHost, 6334, true, _apiKey);
//{
// vector = queryVector,
// limit = limit
//};
var client = new QdrantClient(
host: _qdrantHost,
https: true,
apiKey: _apiKey
);
var doesCollectionExist = await client.CollectionExistsAsync(collectionName); var doesCollectionExist = await client.CollectionExistsAsync(collectionName);
if (doesCollectionExist) if (doesCollectionExist)
@ -284,16 +403,15 @@ namespace BLAIzor.Services
Console.WriteLine($"Failed to query snippet: no collection"); Console.WriteLine($"Failed to query snippet: no collection");
return 0; return 0;
} }
//var content = new StringContent(JsonConvert.SerializeObject(queryPayload), Encoding.UTF8, "application/json");
//var response = await httpClient.PostAsync($"{qdrantUrl}/collections/{collectionName}/points/search", content);
} }
public async Task<int[]> QueryContentAsync(int siteId, float[] queryVector, int limit = 1) public async Task<List<WebPageContent>> QueryContentAsync(string collectionName, float[] queryVector, int limit = 1)
{ {
_apiKey = GetApiKey(); _apiKey = GetApiKey();
var httpClient = new HttpClient(); //var httpClient = new HttpClient();
List<WebPageContent> pageContent = new();
var client = new QdrantClient( var client = new QdrantClient(
host: _qdrantHost, host: _qdrantHost,
@ -303,88 +421,58 @@ namespace BLAIzor.Services
var response = await client.SearchAsync( var response = await client.SearchAsync(
collectionName: $"Site{siteId}", collectionName: collectionName,
vector: queryVector, vector: queryVector,
//filter: MatchKeyword("city", "London"), payloadSelector: true, //TODO comes with payload now, get the payload
vectorsSelector: true, //filter: MatchKeyword("city", "London"),
limit: 3 limit: 3
); );
//var content = new StringContent(JsonConvert.SerializeObject(queryPayload), Encoding.UTF8, "application/json");
//var response = await httpClient.PostAsync($"{qdrantUrl}/collections/site{siteId}/points/search", content);
//if (response.IsSuccessStatusCode)
//{
// int[] sId = [];
// var result = await response.Content.ReadAsStringAsync();
// //Console.Write(result);
// if (!string.IsNullOrEmpty(result))
// {
// QDrantQueryResult qdr = JsonConvert.DeserializeObject<QDrantQueryResult>(result)!;
// int[] valami = new int[qdr.result.Count()];
// for (int i = 0; i < qdr.result.Count(); i++)
// {
// sId[i] = qdr.result[i].id;
// }
// sId.AddRange(valami);
// }
// //var result = await response.Content.ReadFromJsonAsync<QDrantQueryResult>();
// //Console.Write($"Query result: {sId}");
// return sId;
//}
//else
//{
// Console.WriteLine($"Failed to query snippet: {response.StatusCode}");
// int[] nullResult = [];
// return nullResult;
//}
if (response.Count() == 0) if (response.Count() == 0)
{ {
int[] nullResult = []; int[] nullResult = [];
Console.Write("None found"); Console.Write("None found");
return nullResult; return pageContent;
} }
else else
{ {
int[] intResult = new int[response.Count()];
Console.Write("Found: " + response.FirstOrDefault()!.Id.Num); foreach (var retrievedPoint in response)
for (int i = 0; i < response.Count(); i++)
{ {
intResult[i] = Convert.ToInt32(response[i].Id.Num); float[] vectorArray = retrievedPoint.Vectors.Vector.Data.ToArray();
string idValue;
if (retrievedPoint.Id.HasNum)
{
idValue = retrievedPoint.Id.Num.ToString();
}
else
{
idValue = retrievedPoint.Id.Uuid.ToString();
}
Console.WriteLine($"POINTID: {idValue}, {retrievedPoint.Payload["name"]}");
pageContent.Add(new WebPageContent(retrievedPoint.Id,
retrievedPoint.Payload["uid"].StringValue,
retrievedPoint.Payload["type"].StringValue,
Convert.ToInt32(retrievedPoint.Payload["siteId"].IntegerValue),
retrievedPoint.Payload["name"].StringValue,
retrievedPoint.Payload["description"].StringValue,
retrievedPoint.Payload["content"].StringValue,
vectorArray,
Convert.ToDateTime(retrievedPoint.Payload["lastUpdated"].StringValue)
));
} }
return intResult;
return pageContent;
} }
} }
public async Task QDrantInsertTest(List<int> ids, List<float[]> vectors, List<MapField<string, Value>> payloads) public async Task QDrantInsertPointAsync(PointId id, float[] vectors, MapField<string, Value> payload, string collectionName)
{ {
_apiKey = GetApiKey();
var client = new QdrantClient(_qdrantHost, 6334, true, _apiKey);
var valamii = new List<PointStruct>();
for (int i = 0; i < ids.Count; i++)
{
valamii.Add(new PointStruct
{
Id = (ulong)ids[i],
Vectors = vectors[i],
Payload = { payloads[i] }
});
Console.WriteLine($"{valamii[i].Id} val bekerült {valamii[i].Payload["html"]}");
}
Console.WriteLine(valamii.Count);
await client.UpsertAsync(
collectionName: "html_snippets",
points: valamii
);
}
public async Task QDrantInsertPointAsync(int id, float[] vectors, MapField<string, Value> payload, string collectionName)
{
_apiKey = GetApiKey(); _apiKey = GetApiKey();
var client = new QdrantClient(_qdrantHost, 6334, true, _apiKey); var client = new QdrantClient(_qdrantHost, 6334, true, _apiKey);
@ -392,7 +480,7 @@ namespace BLAIzor.Services
pointStruct = new PointStruct pointStruct = new PointStruct
{ {
Id = (ulong)id, Id = id,
Vectors = vectors, Vectors = vectors,
Payload = { payload } Payload = { payload }
@ -413,6 +501,8 @@ namespace BLAIzor.Services
public async Task QDrantInsertManyAsync(List<int> ids, List<float[]> vectors, List<MapField<string, Value>> payloads, string collectionName) public async Task QDrantInsertManyAsync(List<int> ids, List<float[]> vectors, List<MapField<string, Value>> payloads, string collectionName)
{ {
//FOR HTMLSNIPPETS, USES INT IDS FOR NOW
_apiKey = GetApiKey(); _apiKey = GetApiKey();
var client = new QdrantClient(_qdrantHost, 6334, true, _apiKey); var client = new QdrantClient(_qdrantHost, 6334, true, _apiKey);
@ -438,6 +528,77 @@ namespace BLAIzor.Services
} }
public async Task QDrantInsertManyAsync(List<WebPageContent> chunks, string collectionName)
{
//FOR WEBPAGECONTENT, USES POINTID IDS ALREADY
_apiKey = GetApiKey();
var client = new QdrantClient(_qdrantHost, 6334, true, _apiKey);
var pointStructList = new List<PointStruct>();
for (int i = 0; i < chunks.Count; i++)
{
//get payload from WebPageContent
//generate code to convert WebpageContent to MapField<string, Value>
var payload = new MapField<string, Value>
{
{ "uid", new Value { StringValue = chunks[i].UId } }, // Correct usage of Value for string assignment
{ "type", new Value { StringValue = chunks[i].Type } },
{ "siteId", new Value { IntegerValue = chunks[i].SiteId } },
{ "name", new Value { StringValue = chunks[i].Name } },
{ "description", new Value { StringValue = chunks[i].Description } },
{ "content", new Value { StringValue = chunks[i].Content } },
{ "lastUpdated", new Value { StringValue = chunks[i].LastUpdated.ToString("o") } } // ISO 8601 format
};
pointStructList.Add(new PointStruct
{
Id = chunks[i].Id,
Vectors = chunks[i].Vectors,
Payload = { payload }
});
Console.WriteLine($"{pointStructList[i].Id} val bekerül {pointStructList[i].Payload["name"]}");
}
Console.WriteLine(pointStructList.Count);
await client.UpsertAsync(
collectionName: collectionName,
points: pointStructList
);
}
public async Task DeletePointAsync(int pointId, string collectionName)
{
_apiKey = GetApiKey();
var client = new QdrantClient(_qdrantHost, 6334, true, _apiKey);
var result = await client.DeleteAsync(collectionName: "{collection_name}", ids: [(ulong)pointId]);
Console.WriteLine(result.Status);
}
public async Task DeletePointsAsync(ulong[] pointIds, string collectionName)
{
_apiKey = GetApiKey();
var client = new QdrantClient(_qdrantHost, 6334, true, _apiKey);
var result = await client.DeleteAsync(collectionName: collectionName, ids: pointIds);
Console.WriteLine(result.Status);
}
public async Task DeletePointsAsync(Guid[] pointIds, string collectionName)
{
_apiKey = GetApiKey();
var client = new QdrantClient(_qdrantHost, 6334, true, _apiKey);
var result = await client.DeleteAsync(collectionName: collectionName, ids: pointIds);
Console.WriteLine(result.Status);
}
public async Task DeleteCollectionAsync(string collectionName) public async Task DeleteCollectionAsync(string collectionName)
{ {
@ -449,6 +610,7 @@ namespace BLAIzor.Services
} }
public class PointResult public class PointResult
{ {
public int id { get; set; } public int id { get; set; }
@ -463,15 +625,6 @@ namespace BLAIzor.Services
public double time { get; set; } public double time { get; set; }
} }
// Root myDeserializedClass = JsonConvert.DeserializeObject<Root>(myJsonResponse);
//public class Payload
//{
// public string description { get; set; }
// public string type { get; set; }
// public string name { get; set; }
// public string html { get; set; }
//}
public class PointData public class PointData
{ {
public int id { get; set; } public int id { get; set; }

View File

@ -32,10 +32,12 @@ namespace BLAIzor.Services
} }
} }
public List<HtmlSnippet> AvailableTemplateSnippets { get; set; }
public List<WebPageContent> AvailableSiteContent { get; set; }
public event Action OnBrandNameChanged; public event Action OnBrandNameChanged;
public int SelectedSiteId { get; set; } = 1; public int SelectedSiteId { get; set; } = 1;
public WebsiteContentModel WebsiteContentModel { get; set; }
public List<HtmlSnippet> AvailableTemplateSnippets { get; set; }
//public List<WebPageContent> AvailableSiteContent { get; set; }
//public string SelectedDocument { get; set; } = "Poppixel.docx"; //public string SelectedDocument { get; set; } = "Poppixel.docx";
//public string SelectedBrandName //public string SelectedBrandName
//{ //{
@ -47,155 +49,7 @@ namespace BLAIzor.Services
public string SessionId { get; set; } public string SessionId { get; set; }
public async Task<SiteInfo> GetSiteInfoByIdAsync(int SiteInfoId)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
var result = await _context.SiteInfos.Where(x => x.Id == SiteInfoId).FirstOrDefaultAsync();
if (result == null)
{
return await _context.SiteInfos.FirstOrDefaultAsync();
}
else
{
return result;
}
}
}
public async Task<SiteInfo?> GetSiteInfoWithFormsByIdAsync(int siteId)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
var result = await _context.SiteInfos.Include(s => s.FormDefinitions).FirstOrDefaultAsync(s => s.Id == siteId);
if (result == null)
{
return new SiteInfo();
}
else
{
return result;
}
}
}
public async Task<SiteInfo> GetSiteInfoByNameAsync(string siteName)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
var result = await _context.SiteInfos.Where(x => x.SiteName == siteName).FirstOrDefaultAsync();
if (result == null)
{
return await _context.SiteInfos.FirstOrDefaultAsync();
}
else
{
return result;
}
}
}
public async Task<SiteInfo> GetSiteInfoByUrlAsync(string url)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
var result = await _context.SiteInfos.Where(x => x.DefaultUrl == url).FirstOrDefaultAsync();
if (result == null)
{
return await _context.SiteInfos.FirstOrDefaultAsync();
}
else
{
return result;
}
}
}
public async Task UpdateSiteInfoAsync(SiteInfo siteInfo)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
_context.SiteInfos.Update(siteInfo);
await _context.SaveChangesAsync();
}
}
public async Task<List<SiteInfo>> GetUserSitesAsync(string userId)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
return await _context.SiteInfos
.Where(s => s.UserId == userId)
.ToListAsync();
}
}
public async Task<List<SiteInfo>> GetSitesAsync()
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
return await _context.SiteInfos.ToListAsync();
}
}
public async Task<SiteInfo> AddSiteInfoAsync(SiteInfo siteInfo)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
if (siteInfo == null)
throw new ArgumentNullException(nameof(siteInfo), "SiteInfo cannot be null.");
try
{
await _context.SiteInfos.AddAsync(siteInfo);
await _context.SaveChangesAsync();
return siteInfo;
}
catch (Exception ex)
{
// Log the exception (using a logging framework like Serilog, NLog, etc.)
throw new InvalidOperationException("An error occurred while adding the site info.", ex);
}
}
}
} }
} }

70
Services/SimpleLogger.cs Normal file
View File

@ -0,0 +1,70 @@
using BLAIzor.Data;
using BLAIzor.Models;
using System;
namespace BLAIzor.Services
{
public class SimpleLogger : ISimpleLogger
{
private readonly ApplicationDbContext _dbContext;
private readonly IWebHostEnvironment _env;
private LogLevel _currentLevel = LogLevel.Info;
private bool _consoleEnabled = true;
public SimpleLogger(ApplicationDbContext dbContext, IWebHostEnvironment env)
{
_dbContext = dbContext;
_env = env;
}
public void SetLevel(LogLevel level) => _currentLevel = level;
public void EnableConsole(bool enabled) => _consoleEnabled = enabled;
public Task InfoAsync(string message, string? details = null) =>
LogAsync(LogLevel.Info, message, details);
public Task WarnAsync(string message, string? details = null) =>
LogAsync(LogLevel.Warning, message, details);
public Task ErrorAsync(string message, string? details = null) =>
LogAsync(LogLevel.Error, message, details);
private async Task LogAsync(LogLevel level, string message, string? details)
{
if (level < _currentLevel || _currentLevel == LogLevel.None)
return;
var log = new AppLog
{
Severity = level.ToString(),
Message = message,
Details = details,
Timestamp = DateTime.UtcNow
};
if (_env.IsProduction())
{
_dbContext.Logs.Add(log);
await _dbContext.SaveChangesAsync();
}
if (_consoleEnabled)
{
var color = Console.ForegroundColor;
Console.ForegroundColor = level switch
{
LogLevel.Info => ConsoleColor.Gray,
LogLevel.Warning => ConsoleColor.Yellow,
LogLevel.Error => ConsoleColor.Red,
_ => ConsoleColor.White
};
Console.WriteLine($"[{log.Timestamp:HH:mm:ss}] [{log.Severity}] {log.Message}");
if (!string.IsNullOrWhiteSpace(details))
Console.WriteLine($" > {details}");
Console.ForegroundColor = color;
}
}
}
}

View File

@ -0,0 +1,142 @@
using BLAIzor.Data;
using BLAIzor.Models;
using Microsoft.EntityFrameworkCore;
using Qdrant.Client.Grpc;
using System.Linq;
namespace BLAIzor.Services
{
public class WebsiteContentLoaderService
{
private readonly IServiceScopeFactory _scopeFactory;
public WebsiteContentLoaderService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task<ContentGroupModel?> LoadAsync(
int siteInfoId,
string contentGroupType,
Func<int, PointId[], Task<List<WebPageContent>>> fetchVectorsFromQdrant)
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var contentGroup = await db.ContentGroups
.Include(cg => cg.Items)
.ThenInclude(ci => ci.Chunks)
.FirstOrDefaultAsync(cg => cg.SiteInfoId == siteInfoId && cg.Type == contentGroupType);
if (contentGroup == null)
return null;
// Extract PointId[] from all ContentChunks
var allPointIds = contentGroup.Items
.SelectMany(i => i.Chunks)
.Where(c => c.ChunkIndex != null)
.Select(c => new PointId(Convert.ToUInt64(c.ChunkIndex)))
.ToArray();
foreach (var item in contentGroup.Items)
{
foreach (var qdraintpoint in item.Chunks)
{
Console.WriteLine("ContentData: " + item.Title + "," + qdraintpoint.QdrantPointId);
}
}
// Fetch vector data from Qdrant
var vectorData = await fetchVectorsFromQdrant(siteInfoId, allPointIds);
// Map back to chunks and items
var contentItems = contentGroup.Items.Select(ci =>
{
var chunkPoints = ci.Chunks
.Select(chunk =>
{
var match = vectorData.FirstOrDefault(v => v.UId.Trim().ToLowerInvariant() == chunk.QdrantPointId.Trim().ToLowerInvariant());
Console.WriteLine("COMPARE: " + chunk.QdrantPointId + ", " + vectorData.Any(v => v.UId == chunk.QdrantPointId));
//Console.WriteLine("COMPARE: " + chunk.QdrantPointId + ", " + vectorData.FirstOrDefault().UId);
return match;
})
.Where(p => p != null)
.ToList();
return new ContentItemModel
{
ContentItem = ci,
Chunks = ci.Chunks.ToList(),
VectorPoints = chunkPoints!
};
}).ToList();
return new ContentGroupModel
{
SiteInfoId = siteInfoId,
ContentGroup = contentGroup,
ContentItems = contentItems
};
}
public async Task<WebsiteContentModel?> LoadAllAsync(
SiteInfo siteInfo,
Func<SiteInfo, PointId[], Task<List<WebPageContent>>> fetchVectorsFromQdrant)
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var contentGroups = await db.ContentGroups
.Include(cg => cg.Items)
.ThenInclude(ci => ci.Chunks)
.Where(cg => cg.SiteInfoId == siteInfo.Id)
.ToListAsync();
if (!contentGroups.Any())
return null;
// Get all chunk point IDs across all content groups
var allPointIds = contentGroups
.SelectMany(cg => cg.Items)
.SelectMany(i => i.Chunks)
.Where(c => c.QdrantPointId != null)
//.Select(c => new PointId(Convert.ToUInt64(c.ChunkIndex)))
.Select(c => new PointId(Guid.Parse(c.QdrantPointId)))
.ToArray();
//.Select(c => new PointId(Guid.Parse(c.QdrantPointId)))
//.ToArray();
// Fetch vector data from Qdrant
var vectorData = await fetchVectorsFromQdrant(siteInfo, allPointIds);
// Build full list of content items with resolved vectors
var allContentItems = contentGroups
.SelectMany(group => group.Items.Select(ci =>
{
var chunkPoints = ci.Chunks
.Select(chunk =>
vectorData.FirstOrDefault(v =>
v.UId.Trim().ToLowerInvariant() == chunk.QdrantPointId.Trim().ToLowerInvariant()))
.Where(p => p != null)
.ToList();
return new ContentItemModel
{
ContentItem = ci,
Chunks = ci.Chunks.ToList(),
VectorPoints = chunkPoints
};
}))
.ToList();
return new WebsiteContentModel
{
SiteInfoId = siteInfo.Id,
ContentGroups = contentGroups, // <-- You may need to add this prop to WebsiteContentModel
ContentItems = allContentItems
};
}
}
}

View File

@ -42,8 +42,8 @@
//"CredentialsPath": "D:\\GOOGLECREDENTIALS\\client_secret_359861037120-m3mjvr3kg51i2c2qb38dav62uuqoqs5k.apps.googleusercontent.com.json" //"CredentialsPath": "D:\\GOOGLECREDENTIALS\\client_secret_359861037120-m3mjvr3kg51i2c2qb38dav62uuqoqs5k.apps.googleusercontent.com.json"
"ApiKey": "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", "ApiKey": "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",
//"ApiKey": "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" //VG3Law //"ApiKey": "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" //VG3Law
"Model": "gpt-4.1-mini" //"Model": "gpt-4.1-mini"
//"Model": "gpt-4o-mini" "Model": "gpt-4o-mini"
//"Model": "gpt-4.1-nano" //"Model": "gpt-4.1-nano"
}, },
"QDrant": { "QDrant": {

251
wwwroot/admin.css Normal file
View File

@ -0,0 +1,251 @@
/*@import url('https://fonts.googleapis.com/css2?family=Quicksand&display=swap');
@import url('https://fonts.googleapis.com/css?family=Comfortaa:400,700,300');*/
label {
display: unset !important;
color: #fff !important;
}
/* Firefox */
* {
scrollbar-width: auto;
scrollbar-color: #87b1d6 #ffffff00;
}
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 16px;
}
*::-webkit-scrollbar-track {
background: #ffffff;
}
*::-webkit-scrollbar-thumb {
background-color: #3e9fa3;
border-radius: 10px;
border: 3px solid #ffffff;
}
.reference-button {
display: flex;
justify-content: space-between;
align-items: center;
/*background: linear-gradient(to bottom, #533e7e, #3c3666);*/
border-radius: 20px;
padding: 16px;
width: 100%;
color: #e0e0f0;
/*box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);*/
/*margin-bottom: 15px;*/
}
.text-content h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #ffffff;
}
.text-content p {
margin: 4px 0 0 0;
font-size: 13px;
color: #ccccdd;
}
.icon-buttons {
display: flex;
gap: 8px; /* spacing between the icons */
}
.icon-circle {
display: flex;
justify-content: center;
align-items: center;
background: radial-gradient(circle at center, #6782c0, #3c4c8a);
color: #ffffff;
font-size: 18px;
font-weight: bold;
width: 36px;
height: 36px;
border-radius: 50%;
flex-shrink: 0;
}
.bg-transparent {
background: transparent !important;
}
.bg-panel {
background-color: rgba(0,0,0,0.2);
}
.bg-panel-gradient {
background: linear-gradient(to bottom, #533e7e, #3c3666) !important;
}
.bg-panel-gradient-highlight {
background: linear-gradient(to bottom, #63358d, #7d3d7b) !important;
}
.pointer {
cursor: pointer !important;
}
.rz-dialog-mask {
background-color: transparent !important;
}
.rz-dialog-wrapper{
z-index: 10008 !important;
border-radius: 20px;
}
.rz-dialog-confirm {
z-index: 10010 !important;
}
.rz-dialog-title {
color: #b0daff !important;
}
.rz-dialog {
border-radius: 20px !important;
background-color: #0c2533 !important;
/*background: linear-gradient(153deg,rgba(12, 37, 51, 0.83) 0%, rgba(87, 188, 199, 0.81) 50%, rgba(237, 83, 196, 0.84) 100%);*/
background: linear-gradient(307deg, rgba(12, 37, 51, 0.83) 7%, rgb(152 87 199 / 73%) 96%);
box-shadow: 8px 6px 8px 2px rgba(0, 0, 0, 0.4);
--rz-dialog-title-background-color: transparent;
--rz-primary: #87b1d6;
}
.rz-dialog .rz-button {
border-radius: 20px !important;
color: #000;
}
.rz-dialog h3 {
color: #b0daff !important;
}
.rz-dialog .text-muted {
--bs-text-opacity: 1;
color: #608AAD !important;
}
.bg-panel .list-group {
--bs-list-group-color: unset !important;
--bs-list-group-bg: unset !important;
}
.bg-panel .list-group-item {
background-color: unset !important;
border: unset !important;
}
.bg-panel .list-group-item .btn {
margin: 5px;
}
.bg-panel .content-item-list {
max-height: 300px;
overflow-y: scroll;
}
.rz-dialog-confirm-buttons .rz-base {
background-color: #87B1D6 !important;
}
.rz-dialog-confirm-buttons .rz-primary {
background-color: red !important;
}
.draggable-popup-dialog {
z-index: 10008 !important;
}
.draggable-popup-dialog .form-control {
border-radius: 20px !important;
background-color: #ffffff38 !important;
/* background: linear-gradient(153deg, rgba(12, 37, 51, 0.83) 0%, rgba(87, 188, 199, 0.81) 50%, rgba(237, 83, 196, 0.84) 100%); */
/* background
Shorthand property for setting most background properties at the same place in the style sheet.
Learn more
Don't show
: linear-gradient(307deg, rgba(12, 37, 51, 0.83) 7%, rgb(152 87 199 / 73%) 96%); */
border-style: unset;
color: #fff;
}
.draggable-popup-dialog-wrapper {
/*background-color: rgba(0,0,0,0.3);*/
/*backdrop-filter: blur(6px);*/
z-index: 10008 !important;
}
.editor-button {
position: fixed;
z-index: 10008 !important;
top: 100px;
left: 0px;
border-bottom-left-radius: 0px;
border-top-left-radius: 0px;
border-bottom-right-radius: 20px;
border-top-right-radius: 20px;
}
.editor-window {
position: fixed;
background-color: #0c2533 !important;
/*background: linear-gradient(153deg,rgba(12, 37, 51, 0.83) 0%, rgba(87, 188, 199, 0.81) 50%, rgba(237, 83, 196, 0.84) 100%);*/
background: linear-gradient(307deg, rgba(12, 37, 51, 0.83) 7%, rgb(152 87 199 / 73%) 96%);
backdrop-filter: blur(6px);
color: #b0daff;
z-index: 10008 !important;
top: 150px;
border-radius: 20px;
min-width: 200px;
max-width: 200px;
height: 80vh;
margin-left: 10px;
box-shadow: 8px 6px 8px 2px rgba(0, 0, 0, 0.4);
--rz-primary: #87b1d6;
overflow-y: scroll;
}
.editor-window .btn {
margin: 5px;
}
.radzen-popup {
display: none;
position: absolute;
overflow: hidden;
height: 360px;
width: 600px;
border: var(--rz-panel-border);
background-color: var(--rz-panel-background-color);
box-shadow: var(--rz-panel-shadow);
border-radius: var(--rz-border-radius);
background-color: #0a4261;
/*background-color: #396586;*/
/*background-color: #608aad;*/
color: #87b1d6;
/*background-color: #b0daff;*/
}
.radzen-popup p {
color: #fff
}
.rz-panel {
--rz-panel-padding: 0.6rem !important;
}
.upload-image-container .img-fluid {
border-radius: 0px;
}

View File

@ -1,8 +1,31 @@
/*@import url('https://fonts.googleapis.com/css2?family=Quicksand&display=swap'); /*@import url('https://fonts.googleapis.com/css2?family=Quicksand&display=swap');
@import url('https://fonts.googleapis.com/css?family=Comfortaa:400,700,300');*/ @import url('https://fonts.googleapis.com/css?family=Comfortaa:400,700,300');*/
.radzen-popup {
display: none;
position: absolute;
overflow: hidden;
height: 360px;
width: 600px;
border: var(--rz-panel-border);
background-color: var(--rz-panel-background-color);
box-shadow: var(--rz-panel-shadow);
border-radius: var(--rz-border-radius);
background-color: #0a4261;
/*background-color: #396586;*/
/*background-color: #608aad;*/
color: #87b1d6;
/*background-color: #b0daff;*/
}
.radzen-popup p {
color: #fff
}
.rz-panel { .rz-panel {
--rz-panel-padding: 0.6rem !important; --rz-panel-padding: 0.6rem !important;
} }
.rz-accordion .rz-accordion-header > a { .rz-accordion .rz-accordion-header > a {
@ -32,6 +55,10 @@
} }
.floating {
animation: float 5s ease-in-out infinite;
}
@keyframes float { @keyframes float {
0% { 0% {
box-shadow: 0 5px 15px 0px rgba(0,0,0,0.6); box-shadow: 0 5px 15px 0px rgba(0,0,0,0.6);
@ -66,7 +93,9 @@
object-fit: cover; /* Ensures the image fills the container */ object-fit: cover; /* Ensures the image fills the container */
} }
.footer {
margin-bottom: 0px !important;
}
/*Search2*/ /*Search2*/
.searchBox { .searchBox {
@ -79,6 +108,9 @@
margin: 0 auto; margin: 0 auto;
transition: 0.8s; transition: 0.8s;
overflow: hidden; overflow: hidden;
animation: fadeInUp;
animation-duration: 2s;
backdrop-filter: blur(5px);
} }
.searchInput:active > .searchBox{ .searchInput:active > .searchBox{
@ -147,6 +179,16 @@
transition: all 0.15s ease; transition: all 0.15s ease;
} }
.show {
color: red;
}
.navbar > .container-fluid {
margin-bottom: 0px;
padding-bottom: 0px;
padding-top: 0px;
}
.navbar-toggler-icon { .navbar-toggler-icon {
/*background-image: var(--bs-navbar-toggler-icon-bg);*/ /*background-image: var(--bs-navbar-toggler-icon-bg);*/
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e;"); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e;");
@ -155,7 +197,8 @@
.navbar-brand { .navbar-brand {
font-size: 1.7rem; font-size: 1.7rem;
color:aqua; color: aqua;
text-transform: capitalize;
} }
.form-select { .form-select {
@ -316,7 +359,7 @@ p {
} }
.navbar-collapse .nav-link { .navbar-collapse .nav-link {
font-size: 1.1rem; font-size: 0.6rem;
} }
p { p {
@ -342,7 +385,7 @@ p {
} }
.navbar-collapse .nav-link { .navbar-collapse .nav-link {
font-size: 1rem; font-size: 0.6rem;
} }
p { p {

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,471 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Quicksand&display=swap');
/*search*/
p {
font-size: x-large;
}
label {
display: none;
}
li {
list-style: none;
}
.btn {
background: rgba(255, 255, 255, 0.5);
border: 0;
color: #000000;
/* width: 98%; */
font-weight: bold;
border-radius: 10px;
/*min-height: 50px;*/
transition: all 0.2s ease;
padding: 10px;
margin: 10px;
}
.menubtn {
background: rgba(255, 255, 255, 0.5);
border: 0;
color: #000000;
/* width: 98%; */
font-weight: bold;
border-radius: 10px;
height: 40px;
transition: all 0.2s ease;
padding: 10px;
margin: 10px;
}
.btn:active {
background: rgba(255, 255, 255, 1);
}
.btn:hover {
background: rgba(255, 255, 255, 0.8);
}
img {
border-radius: 20px !important;
}
input.search_bar{
border: none;
outline: none;
width: 75px;
border-radius: 55px;
margin: 0 auto;
font-size: 1.3em;
color: #0d2840;
padding: 15px 30px 15px 45px;
transition: all .3s cubic-bezier(0,0,.5,1.5);
box-shadow: 0 3px 10px -2px rgba(0,0,0,.1);
background: rgba(255, 255, 255, 0.3) url(https://i.imgur.com/seveWIw.png) no-repeat center center;
}
input.search_bar:focus{
width: 100%;
background-position: calc(100% - 35px) center
}
/*Removes default x in search fields (webkit only i guess)*/
input[type=search]::-webkit-search-cancel-button {
-webkit-appearance: none;
}
/*Changes the color of the placeholder*/
::-webkit-input-placeholder {
color: #0d2840;
opacity: .5;
}
:-moz-placeholder {
color: #0d2840;
opacity: .5;
}
::-moz-placeholder {
color: #0d2840;
opacity: .5;
}
:-ms-input-placeholder {
color: #0d2840;
opacity: .5;
}
/*search*/
/*Search2*/
.searchBox {
width: 60px;
background: rgba(255, 255, 255, 0.3);
height: 60px;
border-radius: 40px;
padding: 10px;
margin: 0 auto;
transition: 0.8s;
}
.searchInput:active > .searchBox{
width:100%
}
.searchInput:focus > .searchBox {
width: 100%
}
.searchInput::placeholder {
color: #fff;
}
.searchBox:hover {
width: 100%;
}
.searchBox:hover > .searchInput {
width: calc(100% - 60px);
padding: 0 6px;
}
.searchBox:hover > .searchButton {
background: white;
color: #2f3640;
}
.searchButton {
color: white;
float: right;
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #e9bb86;
display: flex;
justify-content: center;
align-items: center;
}
.searchInput {
border: none;
background: none;
outline: none;
font-size: 1.3em !important;
color: #e9bb86 !important;
float: left;
padding: 0;
color: white;
font-size: 16px;
transition: 0.4s;
line-height: 40px;
width: 0px;
}
/*Search2*/
.event {
border-radius: 20px !important;
background-color: rgba(255, 255, 255, 0.2) !important;
backdrop-filter: blur(20px);
border: 0;
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.06), 0 2px 4px rgba(0, 0, 0, 0.07);
transition: all 0.15s ease;
}
/*card design*/
.card {
border-radius: 10px !important;
overflow: hidden;
background-color: rgba(255, 255, 255, 0.2) !important;
backdrop-filter: blur(20px);
border: 0;
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.06), 0 2px 4px rgba(0, 0, 0, 0.07);
transition: all 0.15s ease;
margin: 10px;
}
.card:hover {
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.1), 0 10px 8px rgba(0, 0, 0, 0.015);
}
.card-body .card-title {
font-family: 'Lato', sans-serif;
font-weight: 700;
letter-spacing: 0.3px;
font-size: 24px;
color: #121212;
}
.card-text {
font-family: 'Lato', sans-serif;
font-weight: 400;
font-size: 15px;
letter-spacing: 0.3px;
color: #fff;
}
.card .container {
width: 88%;
/*background: #F0EEF8;*/
border-radius: 30px;
/*height: 140px;*/
display: flex;
align-items: center;
justify-content: center;
}
.container:hover > img {
transform: scale(1.2);
}
.container img {
/*padding: 75px;*/
/*margin-top: -40px;
margin-bottom: -40px;*/
transition: 0.4s ease;
cursor: pointer;
}
.btn:hover {
background: #9b7d5a;
}
.btn:focus {
background: #9b7d5a;
outline: 0;
}
/*card design*/
/*bg*/
:root {
font-size: 15px;
}
body {
font-family: 'Quicksand', sans-serif;
color: #f2d8bb;
margin: 0;
min-height: 100vh;
background-color: #000;
/*background-image: radial-gradient(closest-side, rgba(235, 105, 78, 1), rgba(235, 105, 78, 0)), radial-gradient(closest-side, rgba(243, 11, 164, 1), rgba(243, 11, 164, 0)), radial-gradient(closest-side, rgba(254, 234, 131, 1), rgba(254, 234, 131, 0)), radial-gradient(closest-side, rgba(170, 142, 245, 1), rgba(170, 142, 245, 0)), radial-gradient(closest-side, rgba(248, 192, 147, 1), rgba(248, 192, 147, 0));*/
/*background-size: 130vmax 130vmax, 80vmax 80vmax, 90vmax 90vmax, 110vmax 110vmax, 90vmax 90vmax;*/
/*background-position: -80vmax -80vmax, 60vmax -30vmax, 10vmax 10vmax, -30vmax -10vmax, 50vmax 50vmax;*/
background-repeat: no-repeat;
/*animation: 10s movement linear infinite;*/
}
body::after {
content: '';
display: block;
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.myspan {
position: relative;
z-index: 10;
display: flex;
min-height: 100vh;
width: 100%;
justify-content: center;
align-items: center;
font-size: 5rem;
color: transparent;
text-shadow: 0px 0px 1px rgba(255, 255, 255, .6), 0px 4px 4px rgba(0, 0, 0, .05);
letter-spacing: .2rem;
}
/*@keyframes movement {
0%, 100% {
background-size: 130vmax 130vmax, 80vmax 80vmax, 90vmax 90vmax, 110vmax 110vmax, 90vmax 90vmax;
background-position: -80vmax -80vmax, 60vmax -30vmax, 10vmax 10vmax, -30vmax -10vmax, 50vmax 50vmax;
}
25% {
background-size: 100vmax 100vmax, 90vmax 90vmax, 100vmax 100vmax, 90vmax 90vmax, 60vmax 60vmax;
background-position: -60vmax -90vmax, 50vmax -40vmax, 0vmax -20vmax, -40vmax -20vmax, 40vmax 60vmax;
}
50% {
background-size: 80vmax 80vmax, 110vmax 110vmax, 80vmax 80vmax, 60vmax 60vmax, 80vmax 80vmax;
background-position: -50vmax -70vmax, 40vmax -30vmax, 10vmax 0vmax, 20vmax 10vmax, 30vmax 70vmax;
}
75% {
background-size: 90vmax 90vmax, 90vmax 90vmax, 100vmax 100vmax, 90vmax 90vmax, 70vmax 70vmax;
background-position: -50vmax -40vmax, 50vmax -30vmax, 20vmax 0vmax, -10vmax 10vmax, 40vmax 60vmax;
}
}*/
/*bg*/
.mytextarea {
background-color: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(20px);
padding: 10px;
border-radius: 10px;
border-width: 0px;
height: unset !important;
}
.mytextarea:active {
border-width: 0px;
}
.mytextarea:focus-visible {
background-color: rgba(255, 255, 255, 0.5);
border-width: 0px !important;
outline: -webkit-focus-ring-color auto 0px;
outline-color: transparent;
}
.navbar-toggler {
color: #fff;
}
.navbar-brand {
font-size: 1.7rem;
color: #e9bb86;
}
.form-select {
background-color: rgba(255, 255, 255, 0.2);
border-radius: 15px;
display: unset !important;
}
.form-select > option {
background-color: rgba(255, 255, 255, 0.2)
}
.contactform-overlay {
position: fixed;
z-index: 100;
height: 100vh;
width: 100%;
padding: 100px;
top: 0px;
left: 0px;
/* padding-top: 10vh; */
backdrop-filter: blur(20px);
/* background-color: rgba(1, 1, 1, .4); */
}
.form-control {
background-color: rgba(255,255,255,0.4);
border-radius: 15px;
height: 50px;
}
form-control::placeholder{
color: #fff;
}
.contactform-close-overlay {
position: relative;
height: 10vh;
}
.contactform-popup-content {
height: 80vh;
margin: 0px;
padding: 0px;
}
.contactform-popup-close {
position: relative;
height: 10vh;
z-index: 80;
}
.calendly-overlay {
position: absolute;
z-index: 100;
height: 100vh;
width: 100%;
top: 0px;
/* padding-top: 10vh; */
backdrop-filter: blur(20px);
/* background-color: rgba(1, 1, 1, .4); */
}
.calendly-close-overlay {
position: relative;
height: 10vh;
}
.calendly-popup-content {
height: 80vh;
margin: 0px;
padding: 0px;
}
.calendly-popup-close {
position: relative;
height: 10vh;
z-index: 80;
}
#myVideo {
position: fixed;
right: 0;
bottom: 0;
min-width: 100%;
min-height: 100%;
}
.table {
color: #f2d8bb !important;
}
.navbar-collapse {
height: 100vh;
/*display: flex;
align-items: center;
justify-content: center;*/
text-align: center !important;
align-content: center;
}
.navbar-collapse .nav-link {
font-size: 1.4em;
letter-spacing: 2px;
}
.navbar-collapse .nav-item:not(:last-child) {
border-bottom: 0px solid white;
padding: 0.2em 1em;
}
.navbar {
background-color: #1c120c;
color: #fff;
}
.nav-link {
color: #f2d8bb !important;
}
.content {
top: 60px;
}
.text-dark {
color: #fff !important;
}
.img-fluid {
max-height: 50vh !important;
width: auto;
}

View File

@ -1,471 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Quicksand&display=swap');
/*search*/
p {
font-size: x-large;
}
label {
display: none;
}
li {
list-style: none;
}
.btn {
background: rgba(255, 255, 255, 0.5);
border: 0;
color: #000000;
/* width: 98%; */
font-weight: bold;
border-radius: 10px;
/*min-height: 50px;*/
transition: all 0.2s ease;
padding: 10px;
margin: 10px;
}
.menubtn {
background: rgba(255, 255, 255, 0.5);
border: 0;
color: #000000;
/* width: 98%; */
font-weight: bold;
border-radius: 10px;
height: 40px;
transition: all 0.2s ease;
padding: 10px;
margin: 10px;
}
.btn:active {
background: rgba(255, 255, 255, 1);
}
.btn:hover {
background: rgba(255, 255, 255, 0.8);
}
img {
border-radius: 20px !important;
}
input.search_bar{
border: none;
outline: none;
width: 75px;
border-radius: 55px;
margin: 0 auto;
font-size: 1.3em;
color: #0d2840;
padding: 15px 30px 15px 45px;
transition: all .3s cubic-bezier(0,0,.5,1.5);
box-shadow: 0 3px 10px -2px rgba(0,0,0,.1);
background: rgba(255, 255, 255, 0.3) url(https://i.imgur.com/seveWIw.png) no-repeat center center;
}
input.search_bar:focus{
width: 100%;
background-position: calc(100% - 35px) center
}
/*Removes default x in search fields (webkit only i guess)*/
input[type=search]::-webkit-search-cancel-button {
-webkit-appearance: none;
}
/*Changes the color of the placeholder*/
::-webkit-input-placeholder {
color: #0d2840;
opacity: .5;
}
:-moz-placeholder {
color: #0d2840;
opacity: .5;
}
::-moz-placeholder {
color: #0d2840;
opacity: .5;
}
:-ms-input-placeholder {
color: #0d2840;
opacity: .5;
}
/*search*/
/*Search2*/
.searchBox {
width: 60px;
background: rgba(255, 255, 255, 0.3);
height: 60px;
border-radius: 40px;
padding: 10px;
margin: 0 auto;
transition: 0.8s;
}
.searchInput:active > .searchBox{
width:100%
}
.searchInput:focus > .searchBox {
width: 100%
}
.searchInput::placeholder {
color: #fff;
}
.searchBox:hover {
width: 100%;
}
.searchBox:hover > .searchInput {
width: calc(100% - 60px);
padding: 0 6px;
}
.searchBox:hover > .searchButton {
background: white;
color: #2f3640;
}
.searchButton {
color: white;
float: right;
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #e9bb86;
display: flex;
justify-content: center;
align-items: center;
}
.searchInput {
border: none;
background: none;
outline: none;
font-size: 1.3em !important;
color: #e9bb86 !important;
float: left;
padding: 0;
color: white;
font-size: 16px;
transition: 0.4s;
line-height: 40px;
width: 0px;
}
/*Search2*/
.event {
border-radius: 20px !important;
background-color: rgba(255, 255, 255, 0.2) !important;
backdrop-filter: blur(20px);
border: 0;
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.06), 0 2px 4px rgba(0, 0, 0, 0.07);
transition: all 0.15s ease;
}
/*card design*/
.card {
border-radius: 10px !important;
overflow: hidden;
background-color: rgba(255, 255, 255, 0.2) !important;
backdrop-filter: blur(20px);
border: 0;
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.06), 0 2px 4px rgba(0, 0, 0, 0.07);
transition: all 0.15s ease;
margin: 10px;
}
.card:hover {
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.1), 0 10px 8px rgba(0, 0, 0, 0.015);
}
.card-body .card-title {
font-family: 'Lato', sans-serif;
font-weight: 700;
letter-spacing: 0.3px;
font-size: 24px;
color: #121212;
}
.card-text {
font-family: 'Lato', sans-serif;
font-weight: 400;
font-size: 15px;
letter-spacing: 0.3px;
color: #fff;
}
.card .container {
width: 88%;
/*background: #F0EEF8;*/
border-radius: 30px;
/*height: 140px;*/
display: flex;
align-items: center;
justify-content: center;
}
.container:hover > img {
transform: scale(1.2);
}
.container img {
/*padding: 75px;*/
/*margin-top: -40px;
margin-bottom: -40px;*/
transition: 0.4s ease;
cursor: pointer;
}
.btn:hover {
background: #9b7d5a;
}
.btn:focus {
background: #9b7d5a;
outline: 0;
}
/*card design*/
/*bg*/
:root {
font-size: 15px;
}
body {
font-family: 'Quicksand', sans-serif;
color: #f2d8bb;
margin: 0;
min-height: 100vh;
background-color: #000;
/*background-image: radial-gradient(closest-side, rgba(235, 105, 78, 1), rgba(235, 105, 78, 0)), radial-gradient(closest-side, rgba(243, 11, 164, 1), rgba(243, 11, 164, 0)), radial-gradient(closest-side, rgba(254, 234, 131, 1), rgba(254, 234, 131, 0)), radial-gradient(closest-side, rgba(170, 142, 245, 1), rgba(170, 142, 245, 0)), radial-gradient(closest-side, rgba(248, 192, 147, 1), rgba(248, 192, 147, 0));*/
/*background-size: 130vmax 130vmax, 80vmax 80vmax, 90vmax 90vmax, 110vmax 110vmax, 90vmax 90vmax;*/
/*background-position: -80vmax -80vmax, 60vmax -30vmax, 10vmax 10vmax, -30vmax -10vmax, 50vmax 50vmax;*/
background-repeat: no-repeat;
/*animation: 10s movement linear infinite;*/
}
body::after {
content: '';
display: block;
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.myspan {
position: relative;
z-index: 10;
display: flex;
min-height: 100vh;
width: 100%;
justify-content: center;
align-items: center;
font-size: 5rem;
color: transparent;
text-shadow: 0px 0px 1px rgba(255, 255, 255, .6), 0px 4px 4px rgba(0, 0, 0, .05);
letter-spacing: .2rem;
}
/*@keyframes movement {
0%, 100% {
background-size: 130vmax 130vmax, 80vmax 80vmax, 90vmax 90vmax, 110vmax 110vmax, 90vmax 90vmax;
background-position: -80vmax -80vmax, 60vmax -30vmax, 10vmax 10vmax, -30vmax -10vmax, 50vmax 50vmax;
}
25% {
background-size: 100vmax 100vmax, 90vmax 90vmax, 100vmax 100vmax, 90vmax 90vmax, 60vmax 60vmax;
background-position: -60vmax -90vmax, 50vmax -40vmax, 0vmax -20vmax, -40vmax -20vmax, 40vmax 60vmax;
}
50% {
background-size: 80vmax 80vmax, 110vmax 110vmax, 80vmax 80vmax, 60vmax 60vmax, 80vmax 80vmax;
background-position: -50vmax -70vmax, 40vmax -30vmax, 10vmax 0vmax, 20vmax 10vmax, 30vmax 70vmax;
}
75% {
background-size: 90vmax 90vmax, 90vmax 90vmax, 100vmax 100vmax, 90vmax 90vmax, 70vmax 70vmax;
background-position: -50vmax -40vmax, 50vmax -30vmax, 20vmax 0vmax, -10vmax 10vmax, 40vmax 60vmax;
}
}*/
/*bg*/
.mytextarea {
background-color: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(20px);
padding: 10px;
border-radius: 10px;
border-width: 0px;
height: unset !important;
}
.mytextarea:active {
border-width: 0px;
}
.mytextarea:focus-visible {
background-color: rgba(255, 255, 255, 0.5);
border-width: 0px !important;
outline: -webkit-focus-ring-color auto 0px;
outline-color: transparent;
}
.navbar-toggler {
color: #fff;
}
.navbar-brand {
font-size: 1.7rem;
color: #e9bb86;
}
.form-select {
background-color: rgba(255, 255, 255, 0.2);
border-radius: 15px;
display: unset !important;
}
.form-select > option {
background-color: rgba(255, 255, 255, 0.2)
}
.contactform-overlay {
position: fixed;
z-index: 100;
height: 100vh;
width: 100%;
padding: 100px;
top: 0px;
left: 0px;
/* padding-top: 10vh; */
backdrop-filter: blur(20px);
/* background-color: rgba(1, 1, 1, .4); */
}
.form-control {
background-color: rgba(255,255,255,0.4);
border-radius: 15px;
height: 50px;
}
form-control::placeholder{
color: #fff;
}
.contactform-close-overlay {
position: relative;
height: 10vh;
}
.contactform-popup-content {
height: 80vh;
margin: 0px;
padding: 0px;
}
.contactform-popup-close {
position: relative;
height: 10vh;
z-index: 80;
}
.calendly-overlay {
position: absolute;
z-index: 100;
height: 100vh;
width: 100%;
top: 0px;
/* padding-top: 10vh; */
backdrop-filter: blur(20px);
/* background-color: rgba(1, 1, 1, .4); */
}
.calendly-close-overlay {
position: relative;
height: 10vh;
}
.calendly-popup-content {
height: 80vh;
margin: 0px;
padding: 0px;
}
.calendly-popup-close {
position: relative;
height: 10vh;
z-index: 80;
}
#myVideo {
position: fixed;
right: 0;
bottom: 0;
min-width: 100%;
min-height: 100%;
}
.table {
color: #f2d8bb !important;
}
.navbar-collapse {
height: 100vh;
/*display: flex;
align-items: center;
justify-content: center;*/
text-align: center !important;
align-content: center;
}
.navbar-collapse .nav-link {
font-size: 1.4em;
letter-spacing: 2px;
}
.navbar-collapse .nav-item:not(:last-child) {
border-bottom: 0px solid white;
padding: 0.2em 1em;
}
.navbar {
background-color: #1c120c;
color: #fff;
}
.nav-link {
color: #f2d8bb !important;
}
.content {
top: 60px;
}
.text-dark {
color: #fff !important;
}
.img-fluid {
max-height: 50vh !important;
width: auto;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,52 +1,78 @@
/*@import url('https://fonts.googleapis.com/css2?family=Quicksand&display=swap');*/ /*@import url('https://fonts.googleapis.com/css2?family=Quicksand&display=swap');
@import url('https://fonts.googleapis.com/css?family=Comfortaa:400,700,300'); @import url('https://fonts.googleapis.com/css?family=Comfortaa:400,700,300');*/
.rz-html-editor-content {
background-color: #00000033;
}
.form-group {
margin-bottom: 5px;
}
/*search*/ /*search*/
p {
font-size: large;
/*text-align: justify;*/
}
li { li {
list-style: none; list-style: none;
} }
label { label {
display: none; color: #000;
/* display: none; */
}
.img-fluid {
max-height: 50vh;
width: auto;
}
.pop-img {
border-radius: 20px !important;
border-color: white;
}
#maincontrol {
position: fixed;
width: 100%;
height: fit-content;
margin-bottom: 10px;
z-index:10000;
}
#currentContent {
margin-top: 50px;
}
.displaysearch {
padding-left: 5vw;
padding-right: 5vw;
}
.card-img-top {
max-height: 50vh;
width: auto;
border-radius: 20px;
} }
.btn { .btn {
background: rgba(255, 255, 255, 0.5); background: #87b1d6;
border: 0; border: 0;
color: #000000; color: #000000;
/* width: 98%; */ width: fit-content;
font-weight: bold; font-weight: bold;
border-radius: 20px; transition: all 0.2s ease;
/*min-height: 50px;*/ margin: 15px;
transition: all 0.2s ease; border-radius: 20px;
padding: 10px;
margin-top: 10px;
} }
.btn:hover{
background: #e5b984;
color:#000000;
.voicebutton {
border-radius: 50% !important;
padding: 10px !important;
width: 40px;
height:40px;
}
.rounded {
border-radius: 20px !important;
} }
.menubtn { .menubtn {
background: rgba(255, 255, 255, 0.5); background: rgba(255, 255, 255, 0.5);
border: 0; border: 0;
color: #000000; color: #000000;
width: fit-content; /* width: 98%; */
font-weight: bold; font-weight: bold;
border-radius: 20px; border-radius: 20px;
height: 40px; height: 40px;
@ -59,14 +85,17 @@ label {
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
} }
.btn:hover {
background: rgba(255, 255, 255, 0.8);
}
img { img {
border-radius: 20px !important; border-radius: 20px !important;
} }
.bg-dark-secondary {
background-color: #111422 !important;
}
.footer {
background-color: #111422;
}
input.search_bar{ input.search_bar{
border: none; border: none;
@ -132,6 +161,10 @@ input[type=search]::-webkit-search-cancel-button {
width: 100% width: 100%
} }
.searchInput::placeholder {
color:#fff;
}
.searchBox:hover { .searchBox:hover {
width: 100%; width: 100%;
} }
@ -151,7 +184,7 @@ input[type=search]::-webkit-search-cancel-button {
float: right; float: right;
width: 40px; width: 40px;
height: 40px; height: 40px;
border-radius: 50%; border-radius: 50px;
background-color: #e493d0; background-color: #e493d0;
background-image: radial-gradient(closest-side, rgba(235, 105, 78, 1), rgba(235, 105, 78, 0)), radial-gradient(closest-side, rgba(243, 11, 164, 1), rgba(243, 11, 164, 0)), radial-gradient(closest-side, rgba(254, 234, 131, 1), rgba(254, 234, 131, 0)), radial-gradient(closest-side, rgba(170, 142, 245, 1), rgba(170, 142, 245, 0)), radial-gradient(closest-side, rgba(248, 192, 147, 1), rgba(248, 192, 147, 0)); background-image: radial-gradient(closest-side, rgba(235, 105, 78, 1), rgba(235, 105, 78, 0)), radial-gradient(closest-side, rgba(243, 11, 164, 1), rgba(243, 11, 164, 0)), radial-gradient(closest-side, rgba(254, 234, 131, 1), rgba(254, 234, 131, 0)), radial-gradient(closest-side, rgba(170, 142, 245, 1), rgba(170, 142, 245, 0)), radial-gradient(closest-side, rgba(248, 192, 147, 1), rgba(248, 192, 147, 0));
background-size: 130vmax 130vmax, 80vmax 80vmax, 90vmax 90vmax, 110vmax 110vmax, 90vmax 90vmax; background-size: 130vmax 130vmax, 80vmax 80vmax, 90vmax 90vmax, 110vmax 110vmax, 90vmax 90vmax;
@ -243,6 +276,23 @@ input[type=search]::-webkit-search-cancel-button {
cursor: pointer; cursor: pointer;
} }
.btn:hover {
background-color: #e493d0;
background-image: radial-gradient(closest-side, rgba(235, 105, 78, 1), rgba(235, 105, 78, 0)), radial-gradient(closest-side, rgba(243, 11, 164, 1), rgba(243, 11, 164, 0)), radial-gradient(closest-side, rgba(254, 234, 131, 1), rgba(254, 234, 131, 0)), radial-gradient(closest-side, rgba(170, 142, 245, 1), rgba(170, 142, 245, 0)), radial-gradient(closest-side, rgba(248, 192, 147, 1), rgba(248, 192, 147, 0));
background-size: 130vmax 130vmax, 80vmax 80vmax, 90vmax 90vmax, 110vmax 110vmax, 90vmax 90vmax;
background-position: -80vmax -80vmax, 60vmax -30vmax, 10vmax 10vmax, -30vmax -10vmax, 50vmax 50vmax;
background-repeat: no-repeat;
animation: 10s movement linear infinite;
}
.btn:focus {
background-color: #e493d0;
background-image: radial-gradient(closest-side, rgba(235, 105, 78, 1), rgba(235, 105, 78, 0)), radial-gradient(closest-side, rgba(243, 11, 164, 1), rgba(243, 11, 164, 0)), radial-gradient(closest-side, rgba(254, 234, 131, 1), rgba(254, 234, 131, 0)), radial-gradient(closest-side, rgba(170, 142, 245, 1), rgba(170, 142, 245, 0)), radial-gradient(closest-side, rgba(248, 192, 147, 1), rgba(248, 192, 147, 0));
background-size: 130vmax 130vmax, 80vmax 80vmax, 90vmax 90vmax, 110vmax 110vmax, 90vmax 90vmax;
background-position: -80vmax -80vmax, 60vmax -30vmax, 10vmax 10vmax, -30vmax -10vmax, 50vmax 50vmax;
background-repeat: no-repeat;
animation: 10s movement linear infinite;
}
/*card design*/ /*card design*/
/*bg*/ /*bg*/
@ -253,16 +303,16 @@ input[type=search]::-webkit-search-cancel-button {
} }
body { body {
font-family: 'Comfortaa', 'Arial Narrow', Arial, sans-serif; /*font-family: 'Comfortaa', 'Arial Narrow', Arial, sans-serif;*/
/*font-family: 'Quicksand', sans-serif;*/ /*font-family: 'Quicksand', sans-serif;*/
color: #fff !important;
margin: 0; margin: 0;
min-height: 100vh; min-height: 100vh;
background-color: #e493d0; background-color: #060816;
background-image: radial-gradient(closest-side, rgba(235, 105, 78, 1), rgba(235, 105, 78, 0)), radial-gradient(closest-side, rgba(243, 11, 164, 1), rgba(243, 11, 164, 0)), radial-gradient(closest-side, rgba(254, 234, 131, 1), rgba(254, 234, 131, 0)), radial-gradient(closest-side, rgba(170, 142, 245, 1), rgba(170, 142, 245, 0)), radial-gradient(closest-side, rgba(248, 192, 147, 1), rgba(248, 192, 147, 0)); /*background: linear-gradient(295deg,#060816,#090f59,#440959,#000888);
background-size: 130vmax 130vmax, 80vmax 80vmax, 90vmax 90vmax, 110vmax 110vmax, 90vmax 90vmax; background-size: 240% 240%;
background-position: -80vmax -80vmax, 60vmax -30vmax, 10vmax 10vmax, -30vmax -10vmax, 50vmax 50vmax; animation: gradient-animation 24s ease infinite;*/
background-repeat: no-repeat; background-repeat: no-repeat;
/*animation: 10s movement linear infinite;*/
} }
body::after { body::after {
@ -277,6 +327,18 @@ body {
-webkit-backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
} }
@keyframes gradient-animation {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.myspan { .myspan {
position: relative; position: relative;
z-index: 10; z-index: 10;
@ -291,7 +353,7 @@ body {
letter-spacing: .2rem; letter-spacing: .2rem;
} }
/*@keyframes movement { @keyframes movement {
0%, 100% { 0%, 100% {
background-size: 130vmax 130vmax, 80vmax 80vmax, 90vmax 90vmax, 110vmax 110vmax, 90vmax 90vmax; background-size: 130vmax 130vmax, 80vmax 80vmax, 90vmax 90vmax, 110vmax 110vmax, 90vmax 90vmax;
background-position: -80vmax -80vmax, 60vmax -30vmax, 10vmax 10vmax, -30vmax -10vmax, 50vmax 50vmax; background-position: -80vmax -80vmax, 60vmax -30vmax, 10vmax 10vmax, -30vmax -10vmax, 50vmax 50vmax;
@ -311,7 +373,7 @@ body {
background-size: 90vmax 90vmax, 90vmax 90vmax, 100vmax 100vmax, 90vmax 90vmax, 70vmax 70vmax; background-size: 90vmax 90vmax, 90vmax 90vmax, 100vmax 100vmax, 90vmax 90vmax, 70vmax 70vmax;
background-position: -50vmax -40vmax, 50vmax -30vmax, 20vmax 0vmax, -10vmax 10vmax, 40vmax 60vmax; background-position: -50vmax -40vmax, 50vmax -30vmax, 20vmax 0vmax, -10vmax 10vmax, 40vmax 60vmax;
} }
}*/ }
/*bg*/ /*bg*/
@ -320,7 +382,7 @@ body {
background-color: rgba(255, 255, 255, 0.3); background-color: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
padding: 10px; padding: 10px;
border-radius: 25px !important; border-radius: 10px;
border-width: 0px; border-width: 0px;
height: unset !important; height: unset !important;
} }
@ -336,19 +398,20 @@ body {
outline-color: transparent; outline-color: transparent;
} }
.navbar-toggler-icon { .navbar-toggler {
/*background-image: var(--bs-navbar-toggler-icon-bg);*/ color: #fff;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e;");
z-index: 10;
} }
.navbar-brand { .navbar-brand {
font-size: 1.7rem; font-size: 1.7rem;
color:#fff;
} }
.form-select { .form-select {
background-color: rgba(255, 255, 255, 0.2); background-color: rgba(255, 255, 255, 0.2);
border-radius: 55px; border-radius: 5px;
display: unset !important;
color: #fff;
} }
.form-select > option { .form-select > option {
@ -370,9 +433,12 @@ body {
.form-control { .form-control {
background-color: rgba(255,255,255,0.4); background-color: rgba(255,255,255,0.4);
border-radius: 25px; border-radius: 5px;
height: 50px; height: 50px;
} }
.form-control::placeholder {
color:#fff;
}
.contactform-close-overlay { .contactform-close-overlay {
position: relative; position: relative;
@ -425,49 +491,86 @@ body {
bottom: 0; bottom: 0;
min-width: 100%; min-width: 100%;
min-height: 100%; min-height: 100%;
opacity: 0.2;
} }
.form-select { .table {
display: unset !important; color: #fff !important;
padding-top: 10px;
padding-bottom: 10px;
background-color: transparent;
}
.show {
font-size: 1.6rem;
letter-spacing: 2px;
/*height: 100vh;*/
} }
.navbar-collapse { .navbar-collapse {
height: 100vh; /*height: 100vh;*/
/*display: flex; /*display: flex;
align-items: center; align-items: center;
justify-content: center;*/ justify-content: center;*/
text-align: center !important; text-align: center !important;
align-content: center; align-content: center;
overflow-y: scroll; /*overflow-y: scroll;*/
} }
.navbar-collapse .nav-link { .navbar-collapse .nav-link {
font-size: 1.5em; font-size: 1rem;
letter-spacing: 2px;
} }
.navbar-collapse .nav-item:not(:last-child) { .navbar-collapse .nav-item:not(:last-child) {
border-bottom: 1px solid gray; border-bottom: 0px solid white;
padding: 0.2em 4em; /*padding: 0.2em 4em;*/
} }
.navbar { .navbar {
background-color: #ffffff; background-color: #111422;
color: #000; color: #fff;
} }
.nav-link { .nav-link {
color: #000; color: #fff !important;
}
.voicebutton {
width: 42.5px;
} }
.content { .content {
top: 60px; top: 60px;
} }
.img-fluid { .row {
max-height: 50vh; margin-bottom: 10px;
}
h1 {
color: #fff;
}
p {
color: #EEEEEE;
font-size: 1,1rem;
}
.container-fluid {
margin-bottom:20px;
padding: 20px;
}
/* 🔽 Mobile Responsiveness */
@media (max-width: 768px) {
h1 { font-size: 2rem; }
h2 { font-size: 1.6rem; }
h3 { font-size: 1.4rem; }
p, li { font-size: 1rem; }
.navbar-collapse .nav-link { font-size: 1.1rem; }
p {text-align: justify;}
}
@media (max-width: 480px) {
h1 { font-size: 1.6rem; }
h2 { font-size: 1.3rem; }
h3 { font-size: 1.1rem; }
p, li { font-size: 0.95rem; }
.navbar-collapse .nav-link { font-size: 1rem; }
p {text-align: justify;}
} }

View File

@ -1,168 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap');
.card {
background-color: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(2px);
}
.table {
color: #f2d8bb !important;
}
.navbar-collapse {
height: 100vh;
text-align: center !important;
align-content: center;
}
.navbar-collapse .nav-link {
font-size: 1.2rem;
letter-spacing: 2px;
}
.navbar-collapse .nav-item:not(:last-child) {
padding: 0.2em 1em;
}
.navbar {
background-color: #022c28;
color: #d0eae9;
}
.nav-link {
color: #d0eae9 !important;
}
.content {
top: 60px;
}
body {
background-color: #022c28;
background-attachment: fixed;
background-position: center;
background-size: cover;
color: aqua;
font-family: "Montserrat", sans-serif;
font-optical-sizing: auto;
font-weight: 300;
font-style: normal;
font-size: 1rem; /* Base size */
}
h1 {
font-weight: 700;
font-size: 2.5rem;
padding-top: 30px;
padding-bottom: 10px;
}
h2 {
font-weight: 500;
font-size: 2rem;
padding-top: 15px;
padding-bottom: 10px;
}
h3 {
font-weight: 400;
font-size: 1.6rem;
padding-top: 10px;
padding-bottom: 10px;
}
p {
color: #fff;
font-size: 1.1rem;
}
li {
color: #fff;
font-size: 1rem;
text-align: center;
}
.btn-primary {
color: #d0eae9;
background-color: #014d4e;
border: 0px;
margin: 5px;
}
.btn-primary:hover {
color: #fff;
background-color: #086262;
border: 0px;
}
.row {
padding-bottom: 30px;
margin: 0 auto !important;
}
.searchInput::placeholder {
color: #d0eae9;
}
#myVideo {
position: fixed;
right: 0;
top: -100px;
min-width: 100%;
min-height: 100%;
transform: translateX(calc((100% - 100vw) / 2));
}
.img-fluid {
max-height: 50vh;
width: auto;
border-radius: 15px;
}
.sp-img {
box-shadow: 10px 10px 30px 0px rgba(0,0,0,0.75);
-webkit-box-shadow: 10px 10px 30px 0px rgba(0,0,0,0.75);
-moz-box-shadow: 10px 10px 30px 0px rgba(0,0,0,0.75);
}
.col {
align-items: center;
display: flex;
}
.form-select {
background-color: rgba(255, 255, 255, 0.2);
border-radius: 5px;
display: unset !important;
color: #fff;
}
.list-group-item,
.bg-light {
background-color: rgb(11 24 23 / 76%) !important;
backdrop-filter: blur(8px) !important;
}
.text-primary {
--bs-text-opacity: 1;
color: aqua;
}
/* 🔽 Mobile Responsiveness */
@media (max-width: 768px) {
h1 { font-size: 2rem; }
h2 { font-size: 1.6rem; }
h3 { font-size: 1.4rem; }
p, li { font-size: 1rem; }
.navbar-collapse .nav-link { font-size: 1.1rem; }
p {text-align: justify;}
}
@media (max-width: 480px) {
h1 { font-size: 1.6rem; }
h2 { font-size: 1.3rem; }
h3 { font-size: 1.1rem; }
p, li { font-size: 0.95rem; }
.navbar-collapse .nav-link { font-size: 1rem; }
p {text-align: justify;}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,168 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap');
.card {
background-color: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(2px);
}
.table {
color: #f2d8bb !important;
}
.navbar-collapse {
height: 100vh;
text-align: center !important;
align-content: center;
}
.navbar-collapse .nav-link {
font-size: 1.2rem;
letter-spacing: 2px;
}
.navbar-collapse .nav-item:not(:last-child) {
padding: 0.2em 1em;
}
.navbar {
background-color: #022c28;
color: #d0eae9;
}
.nav-link {
color: #d0eae9 !important;
}
.content {
top: 60px;
}
body {
background-color: #022c28;
background-attachment: fixed;
background-position: center;
background-size: cover;
color: aqua;
font-family: "Montserrat", sans-serif;
font-optical-sizing: auto;
font-weight: 300;
font-style: normal;
font-size: 1rem; /* Base size */
}
h1 {
font-weight: 700;
font-size: 2.5rem;
padding-top: 30px;
padding-bottom: 10px;
}
h2 {
font-weight: 500;
font-size: 2rem;
padding-top: 15px;
padding-bottom: 10px;
}
h3 {
font-weight: 400;
font-size: 1.6rem;
padding-top: 10px;
padding-bottom: 10px;
}
p {
color: #fff;
font-size: 1.1rem;
}
li {
color: #fff;
font-size: 1rem;
text-align: center;
}
.btn-primary {
color: #d0eae9;
background-color: #014d4e;
border: 0px;
margin: 5px;
}
.btn-primary:hover {
color: #fff;
background-color: #086262;
border: 0px;
}
.row {
padding-bottom: 30px;
margin: 0 auto !important;
}
.searchInput::placeholder {
color: #d0eae9;
}
#myVideo {
position: fixed;
right: 0;
top: -100px;
min-width: 100%;
min-height: 100%;
transform: translateX(calc((100% - 100vw) / 2));
}
.img-fluid {
max-height: 50vh;
width: auto;
border-radius: 15px;
}
.sp-img {
box-shadow: 10px 10px 30px 0px rgba(0,0,0,0.75);
-webkit-box-shadow: 10px 10px 30px 0px rgba(0,0,0,0.75);
-moz-box-shadow: 10px 10px 30px 0px rgba(0,0,0,0.75);
}
.col {
align-items: center;
display: flex;
}
.form-select {
background-color: rgba(255, 255, 255, 0.2);
border-radius: 5px;
display: unset !important;
color: #fff;
}
.list-group-item,
.bg-light {
background-color: rgb(11 24 23 / 76%) !important;
backdrop-filter: blur(8px) !important;
}
.text-primary {
--bs-text-opacity: 1;
color: aqua;
}
/* 🔽 Mobile Responsiveness */
@media (max-width: 768px) {
h1 { font-size: 2rem; }
h2 { font-size: 1.6rem; }
h3 { font-size: 1.4rem; }
p, li { font-size: 1rem; }
.navbar-collapse .nav-link { font-size: 1.1rem; }
p {text-align: justify;}
}
@media (max-width: 480px) {
h1 { font-size: 1.6rem; }
h2 { font-size: 1.3rem; }
h3 { font-size: 1.1rem; }
p, li { font-size: 0.95rem; }
.navbar-collapse .nav-link { font-size: 1rem; }
p {text-align: justify;}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Some files were not shown because too many files have changed in this diff Show More