Compare commits

..

6 Commits

655 changed files with 19713 additions and 2485 deletions

View File

@ -7,6 +7,17 @@
<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>
<Content Remove="Components\Partials\OverlayEditor.razor" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="Components\Pages\Home.razorOLD" /> <None Remove="Components\Pages\Home.razorOLD" />
</ItemGroup> </ItemGroup>
@ -30,9 +41,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 +70,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

@ -6,13 +6,22 @@
<html lang="en"> <html lang="en">
<head> <head>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-E9J9J414DF');
</script>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" /> <base href="/" />
<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="animate.css" /> <link rel="stylesheet" href="admin.css" />
@* <link rel="stylesheet" href="animate.css" /> *@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.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" />
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
@ -22,20 +31,12 @@
<script src="https://kit.fontawesome.com/12c469cb8f.js" crossorigin="anonymous"></script> <script src="https://kit.fontawesome.com/12c469cb8f.js" crossorigin="anonymous"></script>
<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> <script type="text/javascript" src="scripts/whisperRecorder.js"></script>
window.applyDynamicCss = (cssContent) => {
let styleTag = document.getElementById('seemgen-style');
if (!styleTag) { <RadzenTheme Theme="material-dark" @rendermode="InteractiveServer" />
styleTag = document.createElement('style'); @* <RadzenTheme Theme="material" @rendermode="InteractiveServer" /> *@
styleTag.id = 'seemgen-style';
document.head.appendChild(styleTag);
}
styleTag.textContent = cssContent;
};
</script> *@
@* <RadzenTheme Theme="material-dark" @rendermode="InteractiveServer" /> *@
<RadzenTheme Theme="material" @rendermode="InteractiveServer" />
<HeadOutlet /> <HeadOutlet />
<SectionOutlet SectionName="HeadContentFromPage" /> <SectionOutlet SectionName="HeadContentFromPage" />
</head> </head>
@ -49,6 +50,11 @@
<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>
<script type="text/javascript" src="scripts/SeemGenCss.js"> </script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-E9J9J414DF"></script>
</body> </body>
</html> </html>

View File

@ -42,7 +42,7 @@
</div> </div>
</nav> </nav>
<div class="page" style="z-index: 1"> <div class="page admin-body" style="z-index: 1">
<main> <main>
@* <div class="top-row px-4" style="z-index: 2"> @* <div class="top-row px-4" style="z-index: 2">

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

@ -1,5 +1,6 @@
<div class="top-row ps-3 navbar navbar-dark"> <div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid"> <div class="container-fluid">
@* <div style="float: left"> *@
<a class="navbar-brand" href="">BLAIzor</a> <a class="navbar-brand" href="">BLAIzor</a>
</div> </div>
</div> </div>

View File

@ -35,6 +35,29 @@
}); });
} }
} }
const hostSelector = 'elevenlabs-convai'; // Replace with your real tag name
const targetClass = '_poweredBy_1f9vw_251'; // Replace with your class
const intervalId = setInterval(() => {
const host = document.querySelector(hostSelector);
if (host && host.shadowRoot) {
const target = host.shadowRoot.querySelector(`.${targetClass}`);
if (target) {
target.style.setProperty('display', 'none', 'important');
console.log('Element found inside shadow DOM and hidden!');
clearInterval(intervalId);
} else {
console.log('Waiting for target inside shadowRoot...');
}
} else {
console.log('Waiting for host or shadowRoot...');
}
}, 500);
</script> </script>
<script src="https://elevenlabs.io/convai-widget/index.js" async type="text/javascript"></script> <script src="https://elevenlabs.io/convai-widget/index.js" async type="text/javascript"></script>
@ -47,18 +70,20 @@
@if (ImageUrl is not null) @if (ImageUrl is not null)
{ {
<div class="mt-5"> <div class="mt-5">
<img class="illustration" src="@ImageUrl"/> <img class="illustration" src="@ImageUrl"/>
</div> </div>
} }
@if (IsLoading) @if (IsLoading)
{ {
<p>🎨 Kép készül... kis türelmet kérek!</p> <p>🎨 Kép készül... kis türelmet kérek!</p>
} }
</article> </article>
</main> </main>
</div> </div>
@code { @code {
private string Menu = "Tanulás, Gyakorlás, Tesztelés, Vizsgázás"; private string Menu = "Tanulás, Gyakorlás, Tesztelés, Vizsgázás";

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

@ -27,43 +27,51 @@ else
<DataAnnotationsValidator /> <DataAnnotationsValidator />
<ValidationSummary /> <ValidationSummary />
<div class="row"> <div class="row">
<div class="form-group col-md-4"> <div class="col-12 col-md-6">
<label>Template Name</label> <div class="form-group">
<InputText class="form-control" @bind-Value="currentTemplate.TemplateName" /> <label>Template Name</label>
<InputText class="form-control" @bind-Value="currentTemplate.TemplateName" />
</div>
<div class="form-group">
<label>Template photo url</label>
<InputText class="form-control" @bind-Value="currentTemplate.TemplatePhotoUrl" />
</div>
<div class="form-group">
<label>Tags (comma-separated)</label>
<InputText class="form-control" @bind-Value="currentTemplate.Tags" />
</div>
<div class="form-group">
<label>Description</label>
<InputTextArea class="form-control" @bind-Value="currentTemplate.Description" />
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-group">
<label>CSS Content</label>
<RadzenHtmlEditor @bind-Value=@currentCssTemplate.CssContent
style="height: 450px; color:#000; background-color: rgba(255,255,255,0.4)"
Input=@OnInput
Change=@OnChange
Paste=@OnPaste
UploadComplete=@OnUploadComplete
Execute=@OnExecute
UploadUrl="upload/image"
Mode=@HtmlEditorMode.Source>
<RadzenHtmlEditorUndo />
<RadzenHtmlEditorRedo />
<RadzenHtmlEditorSource />
</RadzenHtmlEditor>
@* <InputTextArea class="form-control" @bind-Value="currentCssTemplate.CssContent" /> *@
</div>
</div> </div>
<div class="form-group col-md-4">
<label>Template photo url</label>
<InputText class="form-control" @bind-Value="currentTemplate.TemplatePhotoUrl" />
</div>
<div class="form-group col-md-4">
<label>Tags (comma-separated)</label>
<InputText class="form-control" @bind-Value="currentTemplate.Tags" />
</div>
</div> </div>
<div class="form-group">
<label>Description</label>
<InputTextArea class="form-control" @bind-Value="currentTemplate.Description" />
</div>
<div class="form-group">
<label>CSS Content</label>
<RadzenHtmlEditor @bind-Value=@currentCssTemplate.CssContent
style="height: 450px; color:#000; background-color: rgba(255,255,255,0.4)"
Input=@OnInput
Change=@OnChange
Paste=@OnPaste
UploadComplete=@OnUploadComplete
Execute=@OnExecute
UploadUrl="upload/image">
<RadzenHtmlEditorUndo />
<RadzenHtmlEditorRedo />
<RadzenHtmlEditorSource />
</RadzenHtmlEditor>
@* <InputTextArea class="form-control" @bind-Value="currentCssTemplate.CssContent" /> *@
</div>
<button class="btn btn-success" type="submit">Save Changes</button> <button class="btn btn-success" type="submit">Save Changes</button>
</EditForm> </EditForm>

View File

@ -5,25 +5,29 @@
@using BLAIzor.Services @using BLAIzor.Services
@layout AdminLayout @layout AdminLayout
@inject ContentEditorService ContentEditorService @inject ContentEditorService ContentEditorService
@inject ScopedContentService ScopedContentService
<h3>Generate Website Content</h3> <h3>Generate Website Content</h3>
<p>How would you start?</p> <div class="row">
<div class="rz-p-4 rz-text-align-center" style="width: fit-content; margin: 0 auto;">
<div class="rz-p-12 rz-text-align-center"> <p>How would you start?</p>
<RadzenRadioButtonList @bind-Value=@FromDocument TValue="bool"> <RadzenRadioButtonList @bind-Value=@FromDocument TValue="bool" AlignItems="AlignItems.Center" Style="margin: 0 auto;">
<Items> <Items>
<RadzenRadioButtonListItem Text="I have a document" Value="true" /> <RadzenRadioButtonListItem Text="I have a document" Value="true" />
<RadzenRadioButtonListItem Text="Start from scratch" Value="false" /> <RadzenRadioButtonListItem Text="Start from scratch" Value="false" />
</Items> </Items>
</RadzenRadioButtonList> </RadzenRadioButtonList>
</div>
</div> </div>
<p>@errorMessage</p> <p>@errorMessage</p>
@{ @{
if(!FromDocument) if(!FromDocument)
{ {
<GenerateFromScratch SiteId=@SiteId SessionId="sessionId"></GenerateFromScratch> // <GenerateFromScratch SiteId=@SiteId SessionId="sessionId"></GenerateFromScratch>
<GenerateSitePages SiteId=@SiteId SessionId="sessionId"></GenerateSitePages>
} }
else else
{ {
@ -40,6 +44,7 @@
protected override Task OnParametersSetAsync() protected override Task OnParametersSetAsync()
{ {
sessionId = ScopedContentService.SessionId;
//TODO get sessionId //TODO get sessionId
return base.OnParametersSetAsync(); return base.OnParametersSetAsync();
} }

View File

@ -1,4 +1,6 @@
@page "/" @page "/"
@page "/menu/{topic?}"
@inherits MainPageBase
@using BLAIzor.Models @using BLAIzor.Models
@using BLAIzor.Services @using BLAIzor.Services
@using BLAIzor.Components.Partials @using BLAIzor.Components.Partials
@ -8,116 +10,163 @@
@using System.Net @using System.Net
@using System.Text.Json @using System.Text.Json
@using Sidio.Sitemap.Blazor @using Sidio.Sitemap.Blazor
@inject AIService ChatGptService
@rendermode InteractiveServer @rendermode InteractiveServer
@inject IJSRuntime jsRuntime;
@inject IConfiguration configuration
@inject ContentService _contentService
@inject ContentEditorService _contentEditorService
@inject ScopedContentService _scopedContentService
@* @inject IEmailSender _emailService *@
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject IHttpContextAccessor HttpContextAccessor @inject IHttpContextAccessor HttpContextAccessor
@inject DesignTemplateService DesignTemplateService @inject DesignTemplateService DesignTemplateService
@inject CssTemplateService CssTemplateService @inject CssTemplateService CssTemplateService
@inject CssInjectorService CssService @inject CssInjectorService CssService
@inject HttpClient Http
@attribute [Sitemap] @attribute [Sitemap]
<div class="page" style="z-index: 1"> <ErrorBoundary>
<NavMenu MenuString="@Menu" OnMenuClicked=@MenuClick></NavMenu> <ChildContent>
<main>
<article class="content container text-center" style="position: relative; z-index: 4;"> <div class="page" style="z-index: 1">
<PageTitle>Home</PageTitle> <NewNavMenu Menu="@MenuItems" BrandName="@SelectedBrandName" SiteId="SiteId" OnMenuClicked=@MenuClick></NewNavMenu>
<VideoComponent SelectedBrandName="@selectedBrandName" /> <main>
@* <HeadContent>
<article class="content text-center" style="position: relative; z-index: 4;">
<PageTitle>Home</PageTitle>
@{
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">
@if (VoiceEnabled)
<input @oninput="(e) => UserInput = e.Value.ToString()" {
@onkeydown="@Enter" class="searchInput" type="text" name="" value="@UserInput" placeholder="Ask any question"> if (STTEnabled)
<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> <button id="recButton" class="voicebutton bg-panel-gradient" onclick="startRecording()"><i class="fa-solid fa-microphone"></i></button>
</button> <button id="stopButton" class="voicebutton bg-danger" onclick="stopRecording()" hidden><i class="fa-solid fa-microphone-slash"></i></button>
</div> }
}
@{ <input @oninput="(e) => UserInput = e.Value.ToString()"
@if(VoiceEnabled) @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="#">
if(STTEnabled) <i class="fa-solid fa-hexagon-nodes-bolt" style="font-size:20px"></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>
}
if(TTSEnabled)
{
<button class="btn btn-primary voicebutton" @onclick="MuteAI"><i class="fa-solid fa-volume-high"></i></button>
<audio id="audioPlayer" hidden style="display: none;"></audio>
}
}
}
</div>
@* Type anything *@
@* </div> *@
</div>
<p id="recordingText"></p>
<div class="row" id="currentContent">
@{
if (!string.IsNullOrEmpty(HtmlContent.ToString()))
{
<div class="pt-5 @FirstColumnClass">
@((MarkupString)HtmlContent.ToString())
</div>
if (isEmailFormVisible)
{
<div class="pt-5 col-12 col-md-6">
<ContactFormComponent ContactFormModel="@ContactFormModel" DocumentEmailAddress="@DocumentEmailAddress" OnDataChanged="@ContentChangedInForm"></ContactFormComponent>
<button @onclick="CancelEmail" class="btn btn-primary">Cancel</button>
</div> </div>
} @{
} @if (VoiceEnabled)
else {
{ // if (STTEnabled)
<div class="text-center row" style="height: 70vh;"> // {
<p>@StatusContent</p> // <button id="recButton" class="btn btn-primary voicebutton" onclick="startRecording()"><i class="fa-solid fa-microphone"></i></button>
<div class="mydiv"></div> // <button id="stopButton" class="btn btn-primary voicebutton" onclick="stopRecording()" hidden><i class="fa-solid fa-microphone-slash"></i></button>
<div class="mydiv"></div> // }
<div class="mydiv"></div>
<div class="mydiv"></div> if (TTSEnabled)
<div class="mydiv"></div> {
if (!AiVoicePermitted)
{
<button data-hint="listen" class="btn btn-primary voicebutton" style="display: inline-block" @onclick="AllowAIVoice">
<i class="fa-solid fa-volume-xmark"></i>
</button>
}
else
{
<button data-hint="listen" class="btn btn-primary voicebutton" style="display: inline-block" @onclick="MuteAI"><i class="fa-solid fa-volume-high"></i></button>
}
<audio id="audioPlayer" hidden style="display: none;"></audio>
}
}
}
</div> </div>
}
@* Type anything *@
@* </div> *@
</div>
} <div id="currentContent">
</div> <p id="recordingText"></p>
<AnimateOnRender CssClass="animate__animated animate__backInUp">
@{
if (!string.IsNullOrEmpty(HtmlContent.ToString()))
{
<button class="btn btn-primary" @onclick="HomeClick"><i class="fa-solid fa-rotate"></i></button> if (isEmailFormVisible)
@* <FormWizardComponent SessionId="@sessionId"></FormWizardComponent> *@ {
<div class="container-fluid">
<div class="row">
<div class="pt-5 @FirstColumnClass">
@((MarkupString)HtmlContent.ToString())
</div>
<div class="pt-5 col-12 col-md-6">
<ContactFormComponent ContactFormModel="@ContactFormModel" DocumentEmailAddress="@DocumentEmailAddress" OnDataChanged="@ContentChangedInForm"></ContactFormComponent>
<button @onclick="CancelEmail" class="btn btn-primary">Cancel</button>
</div>
</div>
</div>
</article> }
</main> else
</div> {
<div class="pt-5 @FirstColumnClass">
@((MarkupString)HtmlContent.ToString())
</div>
}
}
else
{
<div class="text-center row" style="height: 70vh;">
<p>@StatusContent</p>
<div class="mydiv"></div>
<div class="mydiv"></div>
<div class="mydiv"></div>
<div class="mydiv"></div>
<div class="mydiv"></div>
</div>
}
}
</AnimateOnRender>
</div>
<button class="btn btn-primary" @onclick="HomeClick"><i class="fa-solid fa-rotate"></i></button>
@* <FormWizardComponent SessionId="@sessionId"></FormWizardComponent> *@
</article>
</main>
<FooterComponent MenuString="@Menu" OnMenuClicked=@MenuClick></FooterComponent>
</div>
</ChildContent>
<ErrorContent Context="ex">
<p role="alert">An error occurred: @ex.Message</p>
<p>Please try again later.</p>
@* You can log the exception here if you want *@
@{
Console.WriteLine($"Error caught by ErrorBoundary: {ex.Message}");
}
</ErrorContent>
</ErrorBoundary>
<script> <script>
@ -155,119 +204,47 @@
</script> </script>
@code { @code {
public static Index myHome; [Parameter]
public string? topic { get; set; }
// public static Index myHome;
private string? Subdomain; private string? Subdomain;
public int SiteId;
public SiteInfo SiteInfo;
private StringBuilder HtmlContent = new StringBuilder("");
private string TextContent = "";
private string StatusContent = "";
private string UserInput = string.Empty;
private string ChatGptResponse = string.Empty; private string ChatGptResponse = string.Empty;
private bool isRecording = false; private bool isRecording = false;
private string FirstColumnClass = ""; private void AllowAIVoice()
private bool isEmailFormVisible = false; {
AiVoicePermitted = true;
private ContactFormModel ContactFormModel = new(); }
// private string? SuccessMessage;
// private string? ErrorMessage;
private string? DocumentEmailAddress = "";
private string selectedBrandName = "default";
private string dynamicallyLoadedCss = string.Empty;
private string collectionName = "html_snippets";
private string sessionId;
private static readonly Dictionary<string, Index> _instances = new();
private string Menu;
private bool VoiceEnabled;
private bool TTSEnabled;
private bool STTEnabled;
private bool _initVoicePending = false;
private bool welcomeStage = true;
private string GetApiKey() =>
configuration?.GetSection("ElevenLabsAPI")?.GetValue<string>("ApiKey") ?? string.Empty;
private void MuteAI() private void MuteAI()
{ {
TTSEnabled = false; AiVoicePermitted = false;
}
private async Task ConvertTextToSpeech()
{
// string plainText = WebUtility.HtmlDecode(HtmlContent.ToString());
if (string.IsNullOrWhiteSpace(TextContent) || VoiceEnabled == false || TTSEnabled == false || welcomeStage)
return;
Console.WriteLine("------------------------------OMGOMGOMG TTS call!!!!-------------");
var requestContent = new
{
text = TextContent,
// model_id = "eleven_multilingual_v2",
model_id = "eleven_flash_v2_5",
voice_settings = new
{
stability = 0.5,
similarity_boost = 0.75,
speed = 1
}
};
var requestJson = JsonSerializer.Serialize(requestContent);
string voiceId = "rE22Kc7UGoQj4zdHNYvd";
// string voiceId = "yyPLNYHg3CvjlSdSOdLh";
var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"https://api.elevenlabs.io/v1/text-to-speech/{voiceId}/stream")
{
Content = new StringContent(requestJson, Encoding.UTF8, "application/json")
};
httpRequest.Headers.Add("xi-api-key", GetApiKey());
var response = await Http.SendAsync(httpRequest);
if (response.IsSuccessStatusCode)
{
var audioBytes = await response.Content.ReadAsByteArrayAsync();
var base64Audio = Convert.ToBase64String(audioBytes);
var audioDataUrl = $"data:audio/mpeg;base64,{base64Audio}";
await jsRuntime.InvokeVoidAsync("playAudio", audioDataUrl);
}
else
{
// Handle error response
var errorContent = await response.Content.ReadAsStringAsync();
Console.Error.WriteLine($"Error: {errorContent}");
}
} }
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)
{ {
await jsRuntime.InvokeVoidAsync("setSessionId", sessionId); await _logger.InfoAsync("Index component OnafterRender.", $"{SiteId}");
await jsRuntime.InvokeVoidAsync("setSessionId", SessionId);
await jsRuntime.InvokeVoidAsync("initHints"); await jsRuntime.InvokeVoidAsync("initHints");
// sessionId = Guid.NewGuid().ToString(); // sessionId = Guid.NewGuid().ToString();
// _instances[sessionId] = this; // _instances[sessionId] = this;
Console.Write($"\n\n SessionId: {sessionId}\n\n"); Console.Write($"\n\n SessionId: {SessionId}\n\n");
// _scopedContentService.OnBrandNameChanged += HandleBrandNameChanged; // _scopedContentService.OnBrandNameChanged += HandleBrandNameChanged;
if (!string.IsNullOrEmpty(_scopedContentService.SelectedBrandName)) if (!string.IsNullOrEmpty(_scopedContentService.SelectedBrandName))
{ {
selectedBrandName = _scopedContentService.SelectedBrandName; SelectedBrandName = _scopedContentService.SelectedBrandName;
} }
else else
{ {
_scopedContentService.SelectedBrandName = "default"; _scopedContentService.SelectedBrandName = "default";
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)
{ {
@ -286,17 +263,19 @@
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
var designTemplate = await DesignTemplateService.GetByIdAsync((int)SiteInfo.TemplateId!); var designTemplate = await DesignTemplateService.GetByIdAsync((int)SiteInfo.TemplateId!);
var cssTemplate = await CssTemplateService.GetByDesignTemplateIdAsync((int)SiteInfo.TemplateId); var cssTemplate = await CssTemplateService.GetByDesignTemplateIdAsync((int)SiteInfo.TemplateId);
collectionName = designTemplate.QDrandCollectionName; TemplateCollectionName = designTemplate.QDrandCollectionName;
if (cssTemplate != null) if (cssTemplate != null)
{ {
dynamicallyLoadedCss = cssTemplate.CssContent; // Assuming Content holds the CSS string dynamicallyLoadedCss = cssTemplate.CssContent; // Assuming Content holds the CSS string
var cssPath = await CssTemplateService.SaveTempCssFileAsync(dynamicallyLoadedCss, sessionId); var cssPath = await CssTemplateService.SaveTempCssFileAsync(dynamicallyLoadedCss, SessionId);
await jsRuntime.InvokeVoidAsync("seemgen.injectCssFile", cssPath); await jsRuntime.InvokeVoidAsync("seemgen.injectCssFile", cssPath);
//await CssService.ApplyCssAsync(dynamicallyLoadedCss); //await CssService.ApplyCssAsync(dynamicallyLoadedCss);
@ -305,13 +284,26 @@
Console.Write($"------------------------ {SiteInfo.MenuItems}, {SiteId}, {SiteInfo.TemplateId}, {SiteInfo.SiteName}"); Console.Write($"------------------------ {SiteInfo.MenuItems}, {SiteId}, {SiteInfo.TemplateId}, {SiteInfo.SiteName}");
Menu = await GetMenuList(); Menu = await GetMenuList(SiteId);
MenuItems = await GetMenuItems(SiteId);
if (string.IsNullOrEmpty(HtmlContent.ToString())) if (string.IsNullOrEmpty(HtmlContent.ToString()))
{ {
if (!string.IsNullOrWhiteSpace(topic))
{
UserInput = topic;
await ChatGptService.InitSite(SessionId, SiteInfo, TemplateCollectionName, Menu);
await ChatGptService.ProcessContentRequest(SessionId, UserInput, SiteId, (int)SiteInfo.TemplateId!, ContentCollectionName, Menu, true);
}
else
{
await ChatGptService.InitSite(SessionId, SiteInfo, 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();
await ChatGptService.GetChatGptWelcomeMessage(sessionId, SiteId, Menu);
// 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?";
// await ChatGptService.ProcessUserIntent(sessionId, UserInput, SiteId, (int)SiteInfo.TemplateId!, collectionName, Menu); // await ChatGptService.ProcessUserIntent(SessionId, UserInput, SiteId, (int)SiteInfo.TemplateId!, CollectionName, Menu);
} }
UserInput = string.Empty; UserInput = string.Empty;
// await InvokeAsync(StateHasChanged); // await InvokeAsync(StateHasChanged);
@ -328,13 +320,13 @@
public async void MenuClick(string menuName) public async void MenuClick(string menuName)
{ {
await CallCSharpMethod2(menuName, sessionId, true); await CallCSharpMethod2(menuName, SessionId, true);
} }
public void HomeClick() public void HomeClick()
{ {
//ChatGptService.OnContentReceived -= UpdateContent; //ChatGptService.OnContentReceived -= UpdateContent;
AIService.OnContentReceived -= UpdateContent; ChatGptService.OnContentReceived -= UpdateContent;
_navigationManager.Refresh(true); _navigationManager.Refresh(true);
} }
@ -350,123 +342,24 @@
myHome = this; // Set the static reference to the current instance myHome = this; // Set the static reference to the current instance
} }
[JSInvokable("OpenEmailForm2")]
public static async void OpenEmailForm2(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";
DocumentEmailAddress = emailAddress;
isEmailFormVisible = true;
StateHasChanged();
var result = await jsRuntime.InvokeAsync<object>("getDivContent", "currentContent");
_scopedContentService.CurrentDOM = JsonSerializer.Serialize(result);
// Console.Write($"{_scopedContentService.CurrentDOM}");
}
[JSInvokable("CallCSharpMethod2")]
public static async Task CallCSharpMethod2(string input, string sessionId, bool forceUnModified = false)
{
// if (myHome != null)
// {
// await myHome.HandleJsCall(input, );
// }
if (_instances.TryGetValue(sessionId, out var instance))
{
await instance.HandleJsCall(input, sessionId, forceUnModified);
}
Console.Write("Button clicked:" + input);
}
[JSInvokable("ProcessAudio2")]
public static async Task ProcessAudio2(string base64Audio, string sessionId)
{
Console.Write("audio incoming");
if (myHome != null)
{
if (myHome.STTEnabled == false) return;
Console.WriteLine("STT ENABLED -------------------------------------------------------------------------------");
var languageCode = "hu-HU";
if (myHome._scopedContentService.SelectedLanguage == "Hungarian")
{
languageCode = "hu-HU";
}
else if (myHome._scopedContentService.SelectedLanguage == "English")
{
languageCode = "en-US";
}
else if (myHome._scopedContentService.SelectedLanguage == "German")
{
languageCode = "de-DE";
}
var credentialsPath = myHome.configuration.GetSection("GoogleAPI").GetValue<string>("CredentialsPath");
Console.Write(credentialsPath);
var builder = new SpeechClientBuilder
{
CredentialsPath = credentialsPath
};
var speech = builder.Build();
byte[] audioBytes = Convert.FromBase64String(base64Audio);
myHome.HtmlContent.Clear();
var response = await speech.RecognizeAsync(new RecognitionConfig
{
Encoding = RecognitionConfig.Types.AudioEncoding.Mp3,
SampleRateHertz = 48000, // Match the actual sample rate
LanguageCode = languageCode
}, RecognitionAudio.FromBytes(audioBytes));
Console.Write("BILLED: " + response.TotalBilledTime);
foreach (var result in response.Results)
{
//Console.Write("RESULT: " + result.Alternatives.Count);
foreach (var alternative in result.Alternatives)
{
//Console.WriteLine($"Transcription: {alternative.Transcript}");
await myHome.HandleVoiceCommand(alternative.Transcript, sessionId);
}
}
}
}
private async Task SendMessage()
{
Console.WriteLine("Button clicked!");
var menu = await GetMenuList();
HtmlContent.Clear();
await ChatGptService.ProcessUserIntent(sessionId, UserInput, SiteId, (int)SiteInfo.TemplateId!, collectionName, menu);
UserInput = string.Empty;
}
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await _logger.InfoAsync("Index component initialized.", $"{SiteId}");
_scopedContentService.OnBrandNameChanged += HandleBrandNameChanged; _scopedContentService.OnBrandNameChanged += HandleBrandNameChanged;
// ChatGptService.OnContentReceived += UpdateContent; // ChatGptService.OnContentReceived += UpdateContent;
AIService.OnContentReceived += UpdateContent; ChatGptService.OnContentReceived += UpdateContent;
AIService.OnContentReceiveFinished += UpdateFinished; ChatGptService.OnContentReceiveFinished += UpdateFinished;
// ChatGptService.OnStatusChangeReceived += UpdateStatus; // ChatGptService.OnStatusChangeReceived += UpdateStatus;
AIService.OnStatusChangeReceived += UpdateStatus; ChatGptService.OnStatusChangeReceived += UpdateStatus;
AIService.OnTextContentAvailable += UpdateTextContentForVoice; ChatGptService.OnTextContentAvailable += UpdateTextContentForVoice;
sessionId = Guid.NewGuid().ToString(); SessionId = _scopedContentService.SessionId;
_instances[sessionId] = this; _instances[SessionId] = this;
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
{ {
HtmlContent.Clear(); HtmlContent.Clear();
@ -480,20 +373,21 @@
} }
} }
private async void UpdateTextContentForVoice(string receivedSessionId, string content) // private async void UpdateTextContentForVoice(string receivedSessionId, string content)
{ // {
if (receivedSessionId == sessionId) // Only accept messages meant for this tab // Console.WriteLine("UPDATETEXTCONTENT called");
{ // if (receivedSessionId == SessionId) // Only accept messages meant for this tab
// {
TextContent = content; // TextContent = content;
await ConvertTextToSpeech(); // await ConvertTextToSpeech(content);
//_scopedContentService.CurrentDOM = await jsRuntime.InvokeAsync<string>("getDivContent", "currentContent"); // //_scopedContentService.CurrentDOM = await jsRuntime.InvokeAsync<string>("getDivContent", "currentContent");
} // }
} // }
private async void UpdateFinished(string receivedSessionId) private async void UpdateFinished(string receivedSessionId)
{ {
if (receivedSessionId == sessionId) // Only accept messages meant for this tab if (receivedSessionId == SessionId) // Only accept messages meant for this tab
{ {
Console.WriteLine("Content update finished"); Console.WriteLine("Content update finished");
var result = await jsRuntime.InvokeAsync<object>("getDivContent", "currentContent"); var result = await jsRuntime.InvokeAsync<object>("getDivContent", "currentContent");
@ -512,7 +406,7 @@
private async void UpdateStatus(string receivedSessionId, string content) private async void UpdateStatus(string receivedSessionId, string content)
{ {
if (receivedSessionId == sessionId) // Only accept messages meant for this tab if (receivedSessionId == SessionId) // Only accept messages meant for this tab
{ {
StatusContent = content; StatusContent = content;
//InvokeAsync(StateHasChanged); // Ensures UI updates dynamically //InvokeAsync(StateHasChanged); // Ensures UI updates dynamically
@ -527,107 +421,33 @@
{ {
if (e.Code == "Enter" || e.Code == "NumpadEnter") if (e.Code == "Enter" || e.Code == "NumpadEnter")
{ {
var menu = await GetMenuList(); var menu = await GetMenuList(SiteId);
HtmlContent.Clear(); HtmlContent.Clear();
string input = "Please tell me more about: " + UserInput; string input = "Please tell me more about: " + UserInput;
await ChatGptService.ProcessUserIntent(sessionId, input, SiteId, (int)SiteInfo.TemplateId!, collectionName, menu); await ChatGptService.ProcessUserIntent(SessionId, input, SiteId, (int)SiteInfo.TemplateId!, TemplateCollectionName, menu);
UserInput = string.Empty; UserInput = string.Empty;
} }
} }
public async Task HandleVoiceCommand(string input, string sessionId)
{
// HtmlContent = string.Empty;
UserInput = input;
await InvokeAsync(StateHasChanged);
await SendUserQuery();
//UserInput = string.Empty;
}
public async Task HandleJsCall(string input, string sessionId, bool forceUnmodified)
{
HtmlContent.Clear();
await InvokeAsync(StateHasChanged);
UserInput = input;
await DisplayMenuContent(input, forceUnmodified);
UserInput = string.Empty;
await InvokeAsync(StateHasChanged);
}
private async Task SendUserQuery()
{
welcomeStage = false;
if (!string.IsNullOrEmpty(UserInput))
{
HtmlContent.Clear();
var menu = await GetMenuList();
await ChatGptService.ProcessUserIntent(sessionId, UserInput, SiteId, (int)SiteInfo.TemplateId!, collectionName, menu);
UserInput = string.Empty;
}
}
private async Task DisplayMenuContent(string input, bool forceUnmodified)
{
welcomeStage = false;
if (!string.IsNullOrEmpty(UserInput))
{
HtmlContent.Clear();
var menu = await GetMenuList();
var menuItem = (await GetMenuItems()).Where(m => m.Name == input).FirstOrDefault();
if (menuItem == null)
{
await ChatGptService.ProcessContentRequest(sessionId, input, SiteId, (int)SiteInfo.TemplateId!, collectionName, menu, forceUnmodified);
}
else
{
await ChatGptService.ProcessContentRequest(sessionId, menuItem, SiteId, (int)SiteInfo.TemplateId!, collectionName, menu, forceUnmodified);
}
UserInput = string.Empty;
}
}
private async Task<string> GetMenuList()
{
List<MenuItem> menuItems = (await _contentEditorService.GetMenuItemsBySiteIdAsync(SiteId)).Where(m => m.ShowInMainMenu == true).OrderBy(m => m.SortOrder).ToList();
string menuList = "";
foreach (MenuItem item in menuItems)
{
menuList += item.Name + ",";
}
return menuList;
}
private async Task<List<MenuItem>> GetMenuItems()
{
List<MenuItem> menuItems = (await _contentEditorService.GetMenuItemsBySiteIdAsync(SiteId)).Where(m => m.ShowInMainMenu == true).OrderBy(m => m.SortOrder).ToList();
return menuItems;
}
private async void HandleBrandNameChanged()
{
selectedBrandName = _scopedContentService.SelectedBrandName;
//StateHasChanged();
await InvokeAsync(() =>
{
StateHasChanged();
});
}
public void Dispose() public void Dispose()
{ {
dynamicallyLoadedCss = ""; dynamicallyLoadedCss = "";
HtmlContent.Clear(); HtmlContent.Clear();
_scopedContentService.OnBrandNameChanged -= HandleBrandNameChanged; _scopedContentService.OnBrandNameChanged -= HandleBrandNameChanged;
AIService.OnContentReceived -= UpdateContent; ChatGptService.OnContentReceived -= UpdateContent;
AIService.OnContentReceiveFinished -= UpdateFinished; ChatGptService.OnContentReceiveFinished -= UpdateFinished;
AIService.OnStatusChangeReceived -= UpdateStatus; ChatGptService.OnStatusChangeReceived -= UpdateStatus;
AIService.OnTextContentAvailable -= UpdateTextContentForVoice; ChatGptService.OnTextContentAvailable -= UpdateTextContentForVoice;
} }
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
await CssTemplateService.DeleteSessionCssFile(sessionId); await CssTemplateService.DeleteSessionCssFile(SessionId);
_scopedContentService.OnBrandNameChanged -= HandleBrandNameChanged;
ChatGptService.OnContentReceived -= UpdateContent;
ChatGptService.OnContentReceiveFinished -= UpdateFinished;
ChatGptService.OnStatusChangeReceived -= UpdateStatus;
ChatGptService.OnTextContentAvailable -= UpdateTextContentForVoice;
} }

View File

@ -0,0 +1,129 @@
@page "/logs"
@using BLAIzor.Models
@inject ApplicationDbContext Db
@attribute [Authorize]
<h3 class="mb-3">📋 Application Logs</h3>
<RadzenCard>
<div class="row g-3">
<div class="col-md-2">
<RadzenDropDown @bind-Value="selectedSeverity"
Data="@severities"
Placeholder="All Severities"
AllowClear="true"
Style="width: 100%;" />
</div>
<div class="col-md-3">
<RadzenDatePicker @bind-Value="startDate" Placeholder="From date" Style="width: 100%;" />
</div>
<div class="col-md-3">
<RadzenDatePicker @bind-Value="endDate" Placeholder="To date" Style="width: 100%;" />
</div>
<div class="col-md-2">
<RadzenButton Text="Search" Click="LoadLogs" Icon="search" Style="width: 100%;" />
</div>
</div>
</RadzenCard>
<br />
<RadzenDataGrid TItem="AppLog" Data="@logs" Count="@totalCount"
LoadData="@LoadData" AllowPaging="true" PageSize="10"
AllowSorting="true" AllowFiltering="false"
ColumnWidth="200px" ShowPagingSummary="true">
<Columns>
<RadzenDataGridColumn Width="100px" TItem="AppLog" Property="Timestamp" Title="Time" FormatString="{0:yyyy-MM-dd HH:mm:ss}" />
<RadzenDataGridColumn Width="100px" TItem="AppLog" Property="Severity" Title="Severity" />
<RadzenDataGridColumn TItem="AppLog" Property="Message" Title="Message" />
<RadzenDataGridColumn TItem="AppLog" Property="Details" Title="Details" />
</Columns>
</RadzenDataGrid>
<br />
<h4>📊 Log Summary</h4>
<RadzenStack Style="width: 100%;">
<RadzenChart SeriesClick=@OnSeriesClick>
<RadzenPieSeries Data="@logChartData" Title="Logs" CategoryProperty="Severity" ValueProperty="Count">
<RadzenSeriesDataLabels Visible="true" />
</RadzenPieSeries>
</RadzenChart>
</RadzenStack>
@code {
private List<AppLog> logs = new();
private int totalCount = 0;
private string? selectedSeverity;
private DateTime? startDate;
private DateTime? endDate;
private List<string> severities = new() { "Info", "Warning", "Error" };
private List<LogChartItem> logChartData = new();
void OnSeriesClick(SeriesClickEventArgs args)
{
// console.Log(args);
}
private async Task LoadData(LoadDataArgs args)
{
var query = Db.Logs.AsQueryable();
if (!string.IsNullOrEmpty(selectedSeverity))
query = query.Where(l => l.Severity == selectedSeverity);
if (startDate.HasValue)
query = query.Where(l => l.Timestamp >= startDate.Value);
if (endDate.HasValue)
query = query.Where(l => l.Timestamp <= endDate.Value);
totalCount = query.Count();
logs = query
.OrderByDescending(l => l.Timestamp)
.Skip(args.Skip ?? 0)
.Take(args.Top ?? 10).ToList();
await LoadChartData();
}
private async Task LoadLogs()
{
await LoadData(new LoadDataArgs { Skip = 0, Top = 10 });
}
private async Task LoadChartData()
{
var query = Db.Logs.AsQueryable();
if (!string.IsNullOrEmpty(selectedSeverity))
query = query.Where(l => l.Severity == selectedSeverity);
if (startDate.HasValue)
query = query.Where(l => l.Timestamp >= startDate.Value);
if (endDate.HasValue)
query = query.Where(l => l.Timestamp <= endDate.Value);
logChartData = query
.GroupBy(l => l.Severity)
.Select(g => new LogChartItem
{
Severity = g.Key,
Count = g.Count()
})
.ToList();
}
public class LogChartItem
{
public string Severity { get; set; } = "";
public int Count { get; set; }
}
}

View File

@ -0,0 +1,378 @@
@page "/create-logo"
@using BLAIzor.Components.Layout
@attribute [Authorize]
@layout AdminLayout
@using System.Net.Http.Headers
@using System.Text.Json
@using BLAIzor.Services
@using Microsoft.AspNetCore.Components.Authorization
@using SixLabors.ImageSharp
@using SixLabors.ImageSharp.Processing
@inject IJSRuntime JS
@inject IHttpClientFactory HttpClientFactory
@inject WhisperTranscriptionService WhisperService
@inject ReplicateService ReplicateService
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject CustomAuthenticationStateProvider CustomAuthProvider
<div class="card p-3 shadow-sm bg-panel-gradient text-white" style="max-width:100%; width: 700px; height: 70vh; margin: 0 auto; border-radius: 20px;">
<div class="card-header text-center text-white">
<h3 class="mb-3">Let's build your website step by step</h3>
</div>
<div class="card-body" style="overflow: scroll">
<p class="text-white-50 small mb-3">
Step @CurrentStep of @Steps.Count (@(CurrentStep * 100 / Steps.Count)% complete)
</p>
@if (CurrentStep < Steps.Count)
{
<AnimateOnRender CssClass="animate__animated animate__backInUp" @key="CurrentStep">
<div class="my-3 text-center">
<label class="form-label">@Steps[CurrentStep].Question</label>
<p class="text-muted">@Steps[CurrentStep].Description</p>
<InputText class="form-control" @bind-Value="Steps[CurrentStep].Answer" />
</div>
</AnimateOnRender>
<div class="d-flex justify-content-between mt-3">
<button class="btn btn-secondary" @onclick="PreviousStep" disabled="@IsFirstStep">Back</button>
<button class="btn btn-primary" @onclick="NextStep">Next</button>
</div>
}
else
{
<h5 class="mt-4">Site Description Preview</h5>
<div class="p-3 bg-panel-gradient-highlight text-dark mb-3 rounded">
@((MarkupString)generatedDescription)
<button class="btn btn-success" @onclick="ProceedToLogoStep">Use this description</button>
</div>
}
@if (ShowLogoStep)
{
<div class="text-center my-4">
<h5>Would you like to upload a logo or generate one?</h5>
<div class="my-3">
@* <input type="file" @onchange="UploadLogo" class="form-control" /> *@
<InputFile class="btn btn-default" type="file" multiple OnChange=HandleFileUpload accept=".jpg,.png" />
</div>
<div class="my-3">
<button class="btn btn-outline-primary" @onclick="GenerateLogo" disabled="@IsGeneratingLogo">
@(logoGenerationCount == 0 ? "Generate Logo with AI" : "Regenerate Logo")
</button>
@if (logoGenerationCount > 0 && logoGenerationCount < MaxLogoGenerations)
{
<p class="text-muted small mt-2">
You can regenerate the logo @(MaxLogoGenerations - logoGenerationCount) more time(s).
</p>
}
</div>
@if (!string.IsNullOrWhiteSpace(GeneratedLogoUrl))
{
<div class="my-3">
<img src="@GeneratedLogoUrl" class="img-fluid rounded shadow" style="max-height: 512px;" />
</div>
}
</div>
<div class="p-3 bg-panel-gradient-highlight text-dark mb-3 rounded">
<button class="btn btn-success" @onclick="SaveDescription">Save</button>
</div>
}
</div>
<div class="card-footer my-4 d-flex justify-content-between align-items-center text-white small">
@for (int i = 0; i < Steps.Count; i++)
{
<div class="text-center flex-fill">
<div class="mb-1">
<div class="@GetStepCircleClass(i)">
@(i + 1)
</div>
</div>
@* <div style="min-height: 36px;">@Steps[i].Question.Split('?')[0]</div> *@
@* @if (i < Steps.Count - 1)
{ *@
<div class="progress-line mx-auto"></div>
@* } *@
</div>
}
</div>
</div>
@code {
public string UserId { get; set; } = string.Empty;
private string userName = string.Empty;
private AuthenticationState authState;
private int CurrentStep = 0;
private bool IsFirstStep => CurrentStep == 0;
private bool ShowLogoStep = false;
private string? GeneratedLogoUrl;
private string? logoUrl;
private int logoGenerationCount = 0;
private const int MaxLogoGenerations = 10;
private bool IsGeneratingLogo = false;
private List<QuestionStep> Steps = new()
{
new("What is the name of your site?", "The domain name or the brand name"),
new("Is it for a brand, a person, a cause, a blog, a service, a store, a beauty salon or something else?", "The entity that the site will introduce to the users. "),
new("What kind of feel or atmosphere should the site have? (e.g. friendly, professional, mysterious, playful)", "How should your website communicate?"),
new("What color(s) do you associate with your brand or prefer for the visuals?", "The colors you have in mind. This is needed for design suggestions and photo generation"),
};
private string generatedDescription = "";
private string siteName = "";
private string entity = "";
private string persona = "";
private string colors = "";
protected override async Task OnInitializedAsync()
{
_instance = this;
authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (authState.User.Identity?.IsAuthenticated == true)
{
UserId = CustomAuthProvider.GetUserId();
userName = CustomAuthProvider.GetUserName();
}
}
private void NextStep()
{
if (CurrentStep < Steps.Count)
CurrentStep++;
if (CurrentStep == Steps.Count)
GenerateSiteDescription();
}
private void PreviousStep()
{
if (CurrentStep > 0)
CurrentStep--;
}
private void GenerateSiteDescription()
{
var name = Steps[0].Answer;
var siteEntity = Steps[1].Answer;
var feel = Steps[2].Answer;
colors = Steps[3].Answer;
siteName = name;
entity = siteEntity;
persona = feel;
generatedDescription = $@"
<p><strong>{name}</strong> is a {entity.ToLower()}.</p>
<p>The logo should offer {feel.ToLower()} experience for its viewers.</p>
<p>The design should reflect {colors.ToLower()} tones for visual consistency.</p>
";
}
private void ProceedToLogoStep()
{
ShowLogoStep = true;
}
private async Task GenerateLogo()
{
if (logoGenerationCount >= MaxLogoGenerations)
return;
IsGeneratingLogo = true;
var logoPrompt = $"Logo for a {entity} named {siteName}, with a {persona} tone, using {colors} colors. DO NOT ADD taglines, or any text other than the brand name: {siteName}.";
GeneratedLogoUrl = await ReplicateService.GenerateLogoAsync(logoPrompt, true);
logoUrl = GeneratedLogoUrl;
logoGenerationCount++;
IsGeneratingLogo = false;
}
private async Task SaveDescription()
{
if (!string.IsNullOrWhiteSpace(GeneratedLogoUrl) && GeneratedLogoUrl.StartsWith("http"))
{
var savedPath = await DownloadAndSaveImage(GeneratedLogoUrl);
if (!string.IsNullOrEmpty(savedPath))
{
logoUrl = savedPath;
}
}
string[] siteInfo = new string[6];
siteInfo[0] = siteName;
siteInfo[1] = generatedDescription;
siteInfo[2] = entity;
siteInfo[3] = persona;
siteInfo[5] = logoUrl;
}
private class QuestionStep
{
public string Question { get; }
public string Description { get; }
public string Answer { get; set; } = "";
public QuestionStep(string question, string description)
{
Question = question;
Description = description;
}
}
private string GetStepCircleClass(int index)
{
if (index < CurrentStep)
return "step-circle completed";
else if (index == CurrentStep)
return "step-circle current";
else
return "step-circle";
}
private async Task HandleFileUpload(InputFileChangeEventArgs e)
{
if (e.FileCount == 0) return;
try
{
var uploadPath = Path.Combine("wwwroot", "uploads", UserId);
foreach (var file in e.GetMultipleFiles())
{
var folder = GetFolderForFile(file.ContentType);
var folderPath = Path.Combine(uploadPath, folder);
// Create target directory
Directory.CreateDirectory(folderPath);
var filePath = Path.Combine(folderPath, file.Name);
await using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.OpenReadStream(50 * 1024 * 1024).CopyToAsync(stream);
}
var relativePath = $"/uploads/{UserId}/{folder}/{file.Name}";
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)
{
Console.WriteLine($"Error uploading files: {ex.Message}");
}
finally
{
//IsLoading = false;
}
}
private string GetFolderForFile(string contentType)
{
return contentType switch
{
var type when type.StartsWith("image/") => "images",
var type when type.StartsWith("video/") => "videos",
var type when type.StartsWith("audio/") => "audio",
_ => "others"
};
}
private void AppendFilePathToContent(string contentType, string relativePath, string? thumbnailPath = null)
{
if (contentType.StartsWith("image/"))
{
logoUrl = relativePath;
GeneratedLogoUrl = relativePath;
var ThumbnailUrl = thumbnailPath ?? string.Empty;
}
StateHasChanged();
}
private async Task<string?> DownloadAndSaveImage(string imageUrl)
{
try
{
var uploadPath = Path.Combine("wwwroot", "uploads", UserId, "images");
Directory.CreateDirectory(uploadPath);
var fileName = $"logo_{Guid.NewGuid().ToString().Substring(0, 8)}.jpg";
var filePath = Path.Combine(uploadPath, fileName);
var httpClient = HttpClientFactory.CreateClient();
var imageBytes = await httpClient.GetByteArrayAsync(imageUrl);
await File.WriteAllBytesAsync(filePath, imageBytes);
// Generate thumbnail
var thumbnailFolder = Path.Combine(uploadPath, "thumbnails");
Directory.CreateDirectory(thumbnailFolder);
var thumbnailPath = Path.Combine(thumbnailFolder, fileName);
using var image = Image.Load(imageBytes);
image.Mutate(x => x.Resize(new ResizeOptions
{
Size = new Size(300, 0),
Mode = ResizeMode.Max
}));
await image.SaveAsync(thumbnailPath);
return $"/uploads/{UserId}/images/{fileName}";
}
catch (Exception ex)
{
Console.WriteLine($"Error saving logo: {ex.Message}");
return null;
}
}
// STT Hook
private static LogoGenerator? _instance;
}

View File

@ -0,0 +1,389 @@
using BLAIzor.Models;
using BLAIzor.Services;
using Google.Cloud.Speech.V1;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Mvc;
using Microsoft.JSInterop;
using Radzen;
using System.Text;
using System.Text.Json;
namespace BLAIzor.Components.Pages
{
public class MainPageBase : ComponentBase
{
public string SelectedBrandName = "default";
[Inject] protected ScopedContentService _scopedContentService { get; set; }
[Inject] protected ContentEditorService _contentEditorService { get; set; }
[Inject] protected IConfiguration configuration { get; set; }
[Inject] protected HttpClient Http { get; set; }
[Inject] protected IJSRuntime jsRuntime { get; set; }
[Inject] protected ISimpleLogger _logger { get; set; }
[Inject] protected AIService ChatGptService { get; set; }
[Inject] protected NotificationService NotificationService { get; set; }
[Inject] protected CacheService CacheService { get; set; }
public static readonly Dictionary<string, MainPageBase> _instances = new();
public string SessionId;
public static MainPageBase myHome;
public int SiteId;
public SiteInfo SiteInfo;
public StringBuilder HtmlContent = new StringBuilder("");
public string TextContent = "";
public string StatusContent = "";
public string UserInput = string.Empty;
public string TemplateCollectionName = "html_snippets";
public string ContentCollectionName = "";
public bool VoiceEnabled;
public bool TTSEnabled;
public bool STTEnabled;
public bool _initVoicePending = false;
public bool welcomeStage = true;
public bool AiVoicePermitted = true;
public string FirstColumnClass = "";
public bool isEmailFormVisible = false;
public ContactFormModel ContactFormModel = new();
// private string? SuccessMessage;
// private string? ErrorMessage;
public string? DocumentEmailAddress = "";
public string Menu;
public List<MenuItem> MenuItems = new();
public string dynamicallyLoadedCss = string.Empty;
public MenuItem currentMenuItem = new();
public bool IsContentSaved = false;
public WebsiteContentModel SiteModel = new();
public void DoSharedWork()
{
// Logic here
}
public async void HandleBrandNameChanged()
{
SelectedBrandName = _scopedContentService.SelectedBrandName;
//await InvokeAsync(() =>
// {
// StateHasChanged();
// });
try
{
StateHasChanged();
//await Task.Run(() =>
//{
// StateHasChanged();
//}).ConfigureAwait(false);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
public async Task<string> GetMenuList(int siteId)
{
List<MenuItem> menuItems = (await _contentEditorService.GetMenuItemsBySiteIdAsync(siteId)).Where(m => m.ShowInMainMenu == true).OrderBy(m => m.SortOrder).ToList();
string menuList = "";
foreach (MenuItem item in menuItems)
{
menuList += item.Name + ",";
}
return menuList;
}
public async Task<List<MenuItem>> GetMenuItems(int siteId)
{
List<MenuItem> menuItems = (await _contentEditorService.GetMenuItemsBySiteIdWithChildrenAsync(siteId)).Where(m => m.ShowInMainMenu == true).OrderBy(m => m.SortOrder).ToList();
return menuItems;
}
private string GetApiKey() =>
configuration?.GetSection("ElevenLabsAPI")?.GetValue<string>("ApiKey") ?? string.Empty;
public async Task ConvertTextToSpeech(string textContent)
{
// string plainText = WebUtility.HtmlDecode(HtmlContent.ToString());
if (string.IsNullOrWhiteSpace(textContent) || VoiceEnabled == false || TTSEnabled == false || welcomeStage || !AiVoicePermitted)
return;
Console.WriteLine("------------------------------OMGOMGOMG TTS call!!!!-------------");
var requestContent = new
{
text = textContent,
// model_id = "eleven_multilingual_v2",
model_id = "eleven_flash_v2_5",
voice_settings = new
{
stability = 0.5,
similarity_boost = 0.75,
speed = 1.1
}
};
var requestJson = JsonSerializer.Serialize(requestContent);
string voiceId;
if (SiteInfo.VoiceId != null)
{
voiceId = SiteInfo.VoiceId;
}
else
{
voiceId = "rE22Kc7UGoQj4zdHNYvd";
}
// string voiceId = "yyPLNYHg3CvjlSdSOdLh";
var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"https://api.elevenlabs.io/v1/text-to-speech/{voiceId}/stream")
{
Content = new StringContent(requestJson, Encoding.UTF8, "application/json")
};
httpRequest.Headers.Add("xi-api-key", GetApiKey());
var response = await Http.SendAsync(httpRequest);
if (response.IsSuccessStatusCode)
{
var audioBytes = await response.Content.ReadAsByteArrayAsync();
var base64Audio = Convert.ToBase64String(audioBytes);
var audioDataUrl = $"data:audio/mpeg;base64,{base64Audio}";
await jsRuntime.InvokeVoidAsync("playAudio", audioDataUrl);
}
else
{
// Handle error response
var errorContent = await response.Content.ReadAsStringAsync();
Console.Error.WriteLine($"Error: {errorContent}");
}
}
[JSInvokable("CallCSharpMethod2")]
public static async Task CallCSharpMethod2(string input, string sessionId, bool forceUnModified = false)
{
// if (myHome != null)
// {
// await myHome.HandleJsCall(input, );
// }
if (_instances.TryGetValue(sessionId, out var instance))
{
await instance.HandleJsCall(input, sessionId, forceUnModified);
}
Console.Write("Button clicked:" + input);
}
public async Task HandleJsCall(string input, string sessionId, bool forceUnmodified)
{
HtmlContent.Clear();
await InvokeAsync(StateHasChanged);
UserInput = input;
await DisplayMenuContent(input, forceUnmodified);
UserInput = string.Empty;
await InvokeAsync(StateHasChanged);
}
public async Task HandleVoiceCommand(string input, string sessionId)
{
// HtmlContent = string.Empty;
UserInput = input;
await InvokeAsync(StateHasChanged);
await SendUserQuery();
//UserInput = string.Empty;
}
public async Task SendUserQuery()
{
welcomeStage = false;
if (!string.IsNullOrEmpty(UserInput))
{
HtmlContent.Clear();
var menu = await GetMenuList(SiteId);
await ChatGptService.ProcessUserIntent(SessionId, UserInput, SiteId, (int)SiteInfo.TemplateId!, TemplateCollectionName, menu);
UserInput = string.Empty;
}
}
public async Task DisplayMenuContent(string input, bool forceUnmodified)
{
welcomeStage = false;
if (!string.IsNullOrEmpty(UserInput))
{
HtmlContent.Clear();
var menu = await GetMenuList(SiteId);
var menuList = await GetMenuItems(SiteId);
var menuItem = CompareMenuItemNames(input, menuList);
if (menuItem == null)
{
await ChatGptService.ProcessContentRequest(SessionId, input, SiteId, (int)SiteInfo.TemplateId!, ContentCollectionName, menu, forceUnmodified);
}
else
{
currentMenuItem = menuItem;
if (!string.IsNullOrEmpty(menuItem.StoredHtml))
{
HtmlContent.Clear();
HtmlContent.Append(menuItem.StoredHtml);
if (currentMenuItem.ContentItemId != null) {
var content = await _contentEditorService.GetContentItemByIdAsync((int)currentMenuItem.ContentItemId);
if (content != null)
{
string removedNumbers = TextHelper.ReplaceNumbersAndSpecialCharacters(content.Content, _scopedContentService.SelectedLanguage);
Console.WriteLine(removedNumbers);
UpdateTextContentForVoice(SessionId, removedNumbers);
}
}
IsContentSaved = true;
}
else
{
await ChatGptService.ProcessContentRequest(SessionId, menuItem, SiteId, (int)SiteInfo.TemplateId!, ContentCollectionName, menu, forceUnmodified);
}
}
UserInput = string.Empty;
}
}
protected async void UpdateTextContentForVoice(string receivedSessionId, string content)
{
Console.WriteLine("UPDATETEXTCONTENT called");
if (receivedSessionId == SessionId) // Only accept messages meant for this tab
{
TextContent = content;
await ConvertTextToSpeech(content);
//_scopedContentService.CurrentDOM = await jsRuntime.InvokeAsync<string>("getDivContent", "currentContent");
}
}
public async Task DisplayEmailForm(string emailAddress)
{
FirstColumnClass = "col-12 col-md-6";
DocumentEmailAddress = emailAddress;
isEmailFormVisible = true;
StateHasChanged();
var result = await jsRuntime.InvokeAsync<object>("getDivContent", "currentContent");
_scopedContentService.CurrentDOM = JsonSerializer.Serialize(result);
// Console.Write($"{_scopedContentService.CurrentDOM}");
}
[JSInvokable("ProcessAudio2")]
public static async Task ProcessAudio2(string base64Audio, string SessionId)
{
Console.Write("audio incoming");
if (myHome != null)
{
if (myHome.STTEnabled == false) return;
Console.WriteLine("STT ENABLED -------------------------------------------------------------------------------");
var languageCode = "hu-HU";
if (myHome._scopedContentService.SelectedLanguage == "Hungarian")
{
languageCode = "hu-HU";
}
else if (myHome._scopedContentService.SelectedLanguage == "English")
{
languageCode = "en-US";
}
else if (myHome._scopedContentService.SelectedLanguage == "German")
{
languageCode = "de-DE";
}
var credentialsPath = myHome.configuration.GetSection("GoogleAPI").GetValue<string>("CredentialsPath");
Console.Write(credentialsPath);
var builder = new SpeechClientBuilder
{
CredentialsPath = credentialsPath
};
var speech = builder.Build();
byte[] audioBytes = Convert.FromBase64String(base64Audio);
myHome.HtmlContent.Clear();
var response = await speech.RecognizeAsync(new RecognitionConfig
{
Encoding = RecognitionConfig.Types.AudioEncoding.Mp3,
SampleRateHertz = 48000, // Match the actual sample rate
LanguageCode = languageCode
}, RecognitionAudio.FromBytes(audioBytes));
Console.Write("BILLED: " + response.TotalBilledTime);
foreach (var result in response.Results)
{
//Console.Write("RESULT: " + result.Alternatives.Count);
foreach (var alternative in result.Alternatives)
{
//Console.WriteLine($"Transcription: {alternative.Transcript}");
await myHome.HandleVoiceCommand(alternative.Transcript, SessionId);
}
}
}
}
[JSInvokable("OpenEmailForm2")]
public static async void OpenEmailForm2(string emailAddress)
{
if (myHome != null)
{
await myHome.DisplayEmailForm(emailAddress);
}
Console.Write("openEmail with: " + emailAddress);
}
public async Task SendMessage()
{
Console.WriteLine("Button clicked!");
var menu = await GetMenuList(SiteId);
HtmlContent.Clear();
await ChatGptService.ProcessUserIntent(SessionId, UserInput, SiteId, (int)SiteInfo.TemplateId!, ContentCollectionName, menu);
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);
}
public async ValueTask DisposeAsync()
{
//await CssTemplateService.DeleteSessionCssFile(SessionId);
_scopedContentService.OnBrandNameChanged -= HandleBrandNameChanged;
//AIService.OnContentReceived -= UpdateContent;
//AIService.OnContentReceiveFinished -= UpdateFinished;
//AIService.OnStatusChangeReceived -= UpdateStatus;
ChatGptService.OnTextContentAvailable -= UpdateTextContentForVoice;
}
}
}

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,29 +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>
Brand name: 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>
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> <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" />
@ -87,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,25 @@ 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 +114,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 +216,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 +272,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 +290,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

File diff suppressed because it is too large Load Diff

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,23 +6,28 @@
@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
@inject IJSRuntime JSRuntime @inject IJSRuntime JSRuntime
<h3>Your Sites</h3> <h1>Your Sites</h1>
<div class="row g-0"> <div class="row g-0">
<RadzenPanel Collapsed="true" AllowCollapse="true" class="rz-my-10 rz-mx-auto" Style="width: 100%" <AnimateOnRender CssClass="animate__animated animate__backInUp">
<RadzenPanel Collapsed="true" 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>New site</b> <RadzenIcon Icon="note_add" class="rz-me-1" /><b>New site</b>
</RadzenText> </RadzenText>
</HeaderTemplate> </HeaderTemplate>
<ChildContent> <ChildContent>
<RadzenCard class="rz-mt-4">
<CreateSiteWizard OnDescriptionFinalized="HandleDescriptionGenerated" UserId="@userId" />
@* <RadzenCard class="rz-mt-4">
<EditForm Model="newSite" OnValidSubmit="HandleValidSubmit"> <EditForm Model="newSite" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator /> <DataAnnotationsValidator />
<ValidationSummary /> <ValidationSummary />
@ -43,74 +48,99 @@
<label for="domainUrl">Domain URL</label> <label for="domainUrl">Domain URL</label>
<InputText id="domainUrl" placeholder="Domain url" class="form-control" @bind-Value="newSite.DomainUrl" /> <InputText id="domainUrl" placeholder="Domain url" class="form-control" @bind-Value="newSite.DomainUrl" />
</div> </div>
<div class="col-12 col-md-12 mb-3">
<label for="siteName" class="form-label">Site description</label>
<InputText id="siteDescription" placeholder="A description so AI will know what is this site about" class="form-control" @bind-Value="newSite.SiteDescription" />
</div>
<div class="col-12 col-md-12 mb-3">
<label for="siteName" class="form-label">Site language</label>
<InputText id="siteLanguage" placeholder="The default language of the website" class="form-control" @bind-Value="newSite.DefaultLanguage" />
</div>
</div> </div>
<button type="submit" class="btn btn-primary">Create Site</button> <button type="submit" class="btn btn-primary">Create Site</button>
</EditForm> </EditForm>
</RadzenCard> </RadzenCard>*@
</ChildContent> </ChildContent>
</RadzenPanel> </RadzenPanel>
@if (siteInfoList.Any()) @if (siteInfoList.Any())
{ {
<RadzenPanel AllowCollapse="true" class="rz-my-10 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>Your sites</b> <RadzenIcon Icon="account_box" class="rz-me-1" /><b>Sites created</b>
</RadzenText> </RadzenText>
</HeaderTemplate> </HeaderTemplate>
<ChildContent> <ChildContent>
<RadzenCard class="rz-mt-4"> @{
<RadzenDataList PageSize="3" WrapItems="true" AllowPaging="true" if(siteInfoList.Count() > 0)
{
<AnimateOnRender CssClass="animate__animated animate__backInUp">
<RadzenCard class="rz-mt-4">
<RadzenDataList Density="Density.Compact" PageSize="4" WrapItems="true"
AllowPaging="false"
Data="@siteInfoList" TItem="SiteInfo"> Data="@siteInfoList" TItem="SiteInfo">
<Template Context="site"> <Template Context="site">
<RadzenCard Style="width: 250px; background-color: darkgrey"> <RadzenCard class="admin-rz-card" Style="width: 250px;">
<RadzenRow JustifyContent="@JustifyContent.SpaceBetween"> <RadzenRow JustifyContent="@JustifyContent.SpaceBetween">
<RadzenColumn Size="4" class="rz-text-truncate"> <RadzenColumn Size="4" class="rz-text-truncate">
<RadzenBadge BadgeStyle="BadgeStyle.Light" Text=@($"{site.Id}") class="rz-me-1" /> <RadzenBadge BadgeStyle="BadgeStyle.Light" Text=@($"{site.Id}") class="rz-me-1" />
<b>@(site.SiteName)</b> <b>@(site.SiteName)</b>
</RadzenColumn> </RadzenColumn>
<RadzenColumn Size="8" class="rz-text-align-end"> <RadzenColumn Size="8" class="rz-text-align-end">
<RadzenBadge BadgeStyle="BadgeStyle.Success" Text=@($"new") /> <RadzenBadge BadgeStyle="BadgeStyle.Success" Text=@($"new") />
</RadzenColumn> </RadzenColumn>
</RadzenRow> </RadzenRow>
<hr style="border: none; background-color: var(--rz-text-disabled-color); height: 1px; margin: 1rem 0;" /> <hr style="border: none; background-color: var(--rz-text-disabled-color); height: 1px; margin: 1rem 0;" />
<RadzenStack Orientation="Orientation.Vertical" AlignItems="AlignItems.Center" Gap="1rem"> <RadzenStack Orientation="Orientation.Vertical" AlignItems="AlignItems.Center" Gap="1rem">
<RadzenImage Path="@site.BrandLogoUrl" class="img-fluid" Style="max-height: 100px;" AlternateText="@(site.SiteName)" /> <RadzenImage Path="@site.BrandLogoUrl" class="img-fluid" Style="max-height: 100px;" AlternateText="@(site.SiteName)" />
<RadzenStack Gap="0"> <RadzenStack Gap="0">
<RadzenText TextStyle="TextStyle.H6" class="rz-mb-0">@(site.SiteName)</RadzenText> <RadzenText TextStyle="TextStyle.H6" class="rz-mb-0">@(site.SiteName)</RadzenText>
<RadzenText TextStyle="TextStyle.Body1">@site.DefaultUrl</RadzenText> <RadzenText TextStyle="TextStyle.Body1">@site.DefaultUrl</RadzenText>
<RadzenText TextStyle="TextStyle.Body2">Fefault color: @(site.DefaultColor)</RadzenText> <RadzenText TextStyle="TextStyle.Body2">Fefault color: @(site.DefaultColor)</RadzenText>
<RadzenText TextStyle="TextStyle.Body2">Theme: @(site.TemplateId)</RadzenText> <RadzenText TextStyle="TextStyle.Body2">Theme: @(site.TemplateId)</RadzenText>
</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">
<a href="/site-info/@site.Id" class="btn btn-secondary">Edit</a>
<InputText @bind-Value="collectionName" class="form-control" style="width: 100%;" placeholder="Site name" />
<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;" @onclick="()=>Preview(site)" class="btn btn-secondary"> Preview</a>
<a style="font-size: 14px;" href="/site-info/@site.Id" class="btn btn-secondary">Edit</a> </RadzenStack>
<a style="font-size: 14px;" href="/generate-content/@site.Id" class="btn btn-secondary">Manage content</a> </RadzenCard>
<a style="font-size: 14px;" @onclick="()=>Preview(site)" class="btn btn-secondary"> Preview</a> </Template>
</RadzenDataList>
</RadzenStack> </RadzenCard>
</AnimateOnRender>
}
else
{
<RadzenCard class="rz-mt-4">
<p>Let1s create your first website!</p>
</RadzenCard> </RadzenCard>
</Template> }
</RadzenDataList>
</RadzenCard> }
</ChildContent>
<SummaryTemplate> </ChildContent>
<RadzenCard class="rz-mt-4"> <SummaryTemplate>
<b>@siteInfoList.Count() Sites</b> <RadzenCard class="rz-mt-4">
</RadzenCard> <b>@siteInfoList.Count() Sites</b>
</SummaryTemplate> </RadzenCard>
</RadzenPanel> </SummaryTemplate>
</RadzenPanel>
@* foreach (var item in siteInfoList) @* foreach (var item in siteInfoList)
{ {
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<div class="card m-2"> <div class="card m-2">
@ -136,11 +166,12 @@
</div> </div>
</div> </div>
} *@ } *@
} }
else else
{ {
<p>No sites created yet.</p> <p>No sites created yet.</p>
} }
</AnimateOnRender>
</div> </div>
@ -153,6 +184,27 @@
private string? userName; private string? userName;
private AuthenticationState? authState; private AuthenticationState? authState;
int position = 1; int position = 1;
public string SessionId;
//TEMPORARY
private string collectionName = "seemgen-collection";
private string FinalSiteDescription;
private async Task HandleDescriptionGenerated(string[] desc)
{
newSite.UserId = userId;
FinalSiteDescription = desc[1];
newSite.SiteDescription = desc[1];
newSite.SiteName = desc[0];
newSite.Entity = desc[2];
newSite.Persona = desc[3];
newSite.DefaultLanguage = desc[4];
newSite.BrandLogoUrl = desc[5];
newSite.FacebookUrl = desc[6];
await HandleValidSubmit();
// You can now store it in SiteInfo.SiteDescription
// or prefill the full form with the rest of the properties
}
void Change(string text) void Change(string text)
{ {
@ -166,6 +218,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}";
@ -188,17 +246,25 @@
userId = CustomAuthProvider.GetUserId(); userId = CustomAuthProvider.GetUserId();
userName = CustomAuthProvider.GetUserName(); userName = CustomAuthProvider.GetUserName();
} }
siteInfoList = await SiteInfoService.GetUserSitesAsync(userId!); siteInfoList = await _contentEditorService.GetUserSitesAsync(userId!);
} SessionId = SiteInfoService.SessionId;
// SiteInfoService.SessionId = SessionId;
}
private async Task HandleValidSubmit() private async Task HandleValidSubmit()
{ {
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
if(result != null)
{
NavigationManager.NavigateTo($"/generate-content/{result.Id}");
}
} }
public async Task<string> GenerateSubdomainAsync(string siteName) public async Task<string> GenerateSubdomainAsync(string siteName)
{ {

View File

@ -0,0 +1,21 @@
@inject IJSRuntime JS
<div @ref="ElementRef" class="@CssClass">
@ChildContent
</div>
@code {
[Parameter] public string CssClass { get; set; } = "animate__animated animate__fadeIn";
[Parameter] public RenderFragment? ChildContent { get; set; }
private ElementReference ElementRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (ElementRef.Context is not null)
{
await Task.Delay(50); // wait for DOM to settle
await JS.InvokeVoidAsync("seemgenAnimationHelper.restartAnimation", ElementRef);
}
}
}

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 content</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 text-start">
<strong>@item.Title</strong>
<br />
<small class="text-muted">@item.Language - @item.Tags</small>
</div>
<div class="icon-buttons text-end">
<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,501 @@
@using System.Net.Http.Headers
@using System.Text.Json
@using BLAIzor.Interfaces
@using BLAIzor.Services
@using SixLabors.ImageSharp
@using SixLabors.ImageSharp.Processing
@inject IJSRuntime JS
@inject IHttpClientFactory HttpClientFactory
@inject WhisperTranscriptionService WhisperService
@inject ReplicateService ReplicateService
<div class="card p-3 shadow-sm bg-panel-gradient text-white" style="max-width:100%; width: 600px; height: 70vh; margin: 0 auto; border-radius: 20px;">
<div class="card-header text-center text-white">
<h3 class="mb-3">Let's build your website step by step</h3>
</div>
<div class="card-body" style="overflow-y: scroll">
<p class="text-white-50 small mb-3">
Step @CurrentStep of @Steps.Count (@(CurrentStep * 100 / Steps.Count)% complete)
</p>
@if (CurrentStep < Steps.Count)
{
<AnimateOnRender CssClass="animate__animated animate__backInUp" @key="CurrentStep">
<div class="my-3 text-center">
<label class="form-label">@Steps[CurrentStep].Question</label>
<p class="text-muted">@Steps[CurrentStep].Description</p>
@if (CurrentStep == 5)
{
<InputSelect class="form-select" @bind-Value="Steps[CurrentStep].Answer">
<option value="">-- Select Language --</option>
<option>English</option>
<option>German</option>
<option>Hungarian</option>
</InputSelect>
}
else if (CurrentStep == 7)
{
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Gender</label>
<InputSelect class="form-select" @bind-Value="targetGender">
<option value="">Any</option>
<option>Male</option>
<option>Female</option>
<option>Non-binary</option>
</InputSelect>
</div>
<div class="col-md-4">
<label class="form-label">Age Group</label>
<InputSelect class="form-select" @bind-Value="targetAge">
<option value="">All ages</option>
<option>Under 18</option>
<option>1825</option>
<option>2640</option>
<option>4160</option>
<option>60+</option>
</InputSelect>
</div>
<div class="col-md-4">
<label class="form-label">Location</label>
<InputText class="form-control" @bind-Value="targetLocation" />
</div>
<div class="col-12">
<label class="form-label">Interests</label>
<InputText class="form-control" @bind-Value="targetInterests" />
</div>
</div>
}
else if (CurrentStep == 8) // Last step: Facebook
{
<InputText class="form-control" @bind-Value="facebookPageUrl" placeholder="https://www.facebook.com/yourpage" />
}
else
{
<InputText class="form-control" @bind-Value="Steps[CurrentStep].Answer" />
}
<div class="mt-2">
<button class="btn btn-sm btn-warning me-2" @onclick="StartRecording">🎙️ Speak</button>
<button class="btn btn-sm btn-danger" @onclick="StopRecording">⏹️ Stop</button>
</div>
</div>
</AnimateOnRender>
<div class="d-flex justify-content-between mt-3">
<button class="btn btn-secondary" @onclick="PreviousStep" disabled="@IsFirstStep">Back</button>
<button class="btn btn-primary" @onclick="NextStep">Next</button>
</div>
}
else
{
<h5 class="mt-4">Site Description Preview</h5>
<div class="p-3 bg-panel-gradient-highlight text-dark mb-3 rounded">
@((MarkupString)generatedDescriptionToShow)
<button class="btn btn-success" @onclick="ProceedToLogoStep">Use this description</button>
</div>
}
@if (ShowLogoStep)
{
<div class="text-center my-4">
<h5>Would you like to upload a logo or generate one?</h5>
<div class="my-3">
@* <input type="file" @onchange="UploadLogo" class="form-control" /> *@
<InputFile class="btn btn-default" type="file" multiple OnChange=HandleFileUpload accept=".jpg,.png" />
</div>
<div class="my-3">
<button class="btn btn-outline-primary" @onclick="GenerateLogo" disabled="@IsGeneratingLogo">
@(logoGenerationCount == 0 ? "Generate Logo with AI" : "Regenerate Logo")
</button>
@if (logoGenerationCount > 0 && logoGenerationCount < MaxLogoGenerations)
{
<p class="text-muted small mt-2">
You can regenerate the logo @(MaxLogoGenerations - logoGenerationCount) more time(s).
</p>
}
</div>
@if (!string.IsNullOrWhiteSpace(GeneratedLogoUrl))
{
<div class="my-3">
<img src="@GeneratedLogoUrl" class="img-fluid rounded shadow" style="max-height: 500px;" />
</div>
}
</div>
<div class="p-3 bg-panel-gradient-highlight text-dark mb-3 rounded">
<button class="btn btn-success" @onclick="SaveDescription">Save</button>
</div>
}
</div>
<div class="card-footer my-4 d-flex justify-content-between align-items-center text-white small">
@for (int i = 0; i < Steps.Count; i++)
{
<div class="text-center flex-fill">
<div class="mb-1">
<div class="@GetStepCircleClass(i)">
@(i + 1)
</div>
</div>
@* <div style="min-height: 36px;">@Steps[i].Question.Split('?')[0]</div> *@
@* @if (i < Steps.Count - 1)
{ *@
<div class="progress-line mx-auto"></div>
@* } *@
</div>
}
</div>
</div>
@code {
[Parameter] public EventCallback<string[]> OnDescriptionFinalized { get; set; }
[Parameter] public string UserId { get; set; }
private int CurrentStep = 0;
private bool IsFirstStep => CurrentStep == 0;
private bool ShowLogoStep = false;
private string? GeneratedLogoUrl;
private string? logoUrl;
private int logoGenerationCount = 0;
private const int MaxLogoGenerations = 5;
private bool IsGeneratingLogo = false;
private List<QuestionStep> Steps = new()
{
new("What is the name of your site?", "The domain name or the brand name"),
new("Is it for a brand, a person, a cause, a blog, a service, a store, a beauty salon or something else?", "The entity that the site will introduce to the users. "),
new("What is the main purpose of the site?", "What is your goal with the website. Promote the brand, or acquire new customers? This is needed for SEO, and content generation."),
new("What kind of feel or atmosphere should the site have? (e.g. friendly, professional, mysterious, playful)", "How should your website communicate?"),
new("Where is it located (country or city)?", "This is for content generation and SEO purposes."),
new("What is your preferred language?", "The default language of your website. As it is AI driven, AI can answer in almost any language to questions"),
new("What color(s) do you associate with your brand or prefer for the visuals?", "The colors you have in mind. This is needed for design suggestions and photo generation"),
new("Who is your target audience?", "This is needed to generate have a better overall understanding during content generation."),
new("Do you already have a Facebook page for your business?", "Provide the URL of your Facebook page if you have one. We'll use it to generate content more accurately.")
};
private string targetGender = "";
private string targetAge = "";
private string targetLocation = "";
private string targetInterests = "";
private string generatedDescription = "";
private string generatedDescriptionToShow = "";
private string siteName = "";
private string entity = "";
private string persona = "";
private string defaultLanguage = "";
private string facebookPageUrl = "";
// 3. Trigger Brightdata scraping after generating site description
private async Task GenerateSiteDescription()
{
var name = Steps[0].Answer;
var siteEntity = Steps[1].Answer;
var purpose = Steps[2].Answer;
var feel = Steps[3].Answer;
var location = Steps[4].Answer;
var language = Steps[5].Answer;
var color = Steps[6].Answer;
var facebook = Steps[8].Answer;
var audience = $"people who are {(string.IsNullOrEmpty(targetGender) ? "all genders" : targetGender.ToLower())} aged {(string.IsNullOrEmpty(targetAge) ? "any age" : targetAge)}, located in {targetLocation}, interested in {targetInterests.ToLower()}";
siteName = name;
entity = siteEntity;
persona = feel;
defaultLanguage = language;
generatedDescription = $@"
{name} is a {entity.ToLower()} based in {location}, created to {purpose.ToLower()}.
The site aims to offer a {feel.ToLower()} experience for its visitors, primarily targeting {audience}.
The preferred language is {language}, and the design should reflect {color.ToLower()} tones for visual consistency.
";
generatedDescriptionToShow = $@"
<p><strong>{name}</strong> is a {entity.ToLower()} based in {location}, created to {purpose.ToLower()}.</p>
<p>The site aims to offer a {feel.ToLower()} experience for its visitors, primarily targeting {audience}.</p>
<p>The preferred language is {language}, and the design should reflect {color.ToLower()} tones for visual consistency.</p>
";
// --- New: Scrape Facebook posts if provided ---
}
protected override void OnInitialized()
{
_instance = this;
}
private void NextStep()
{
if (CurrentStep < Steps.Count)
CurrentStep++;
if (CurrentStep == Steps.Count)
GenerateSiteDescription();
}
private void PreviousStep()
{
if (CurrentStep > 0)
CurrentStep--;
}
// private void GenerateSiteDescription()
// {
// var name = Steps[0].Answer;
// var siteEntity = Steps[1].Answer;
// var purpose = Steps[2].Answer;
// var feel = Steps[3].Answer;
// var location = Steps[4].Answer;
// var language = Steps[5].Answer;
// var color = Steps[6].Answer;
// var audience = $"people who are {(string.IsNullOrEmpty(targetGender) ? "all genders" : targetGender.ToLower())} aged {(string.IsNullOrEmpty(targetAge) ? "any age" : targetAge)}, located in {targetLocation}, interested in {targetInterests.ToLower()}";
// siteName = name;
// entity = siteEntity;
// persona = feel;
// defaultLanguage = language;
// generatedDescription = $@"
// {name} is a {entity.ToLower()} based in {location}, created to {purpose.ToLower()}.
// The site aims to offer a {feel.ToLower()} experience for its visitors, primarily targeting {audience}.
// The preferred language is {language}, and the design should reflect {color.ToLower()} tones for visual consistency.
// ";
// generatedDescriptionToShow = $@"
// <p><strong>{name}</strong> is a {entity.ToLower()} based in {location}, created to {purpose.ToLower()}.</p>
// <p>The site aims to offer a {feel.ToLower()} experience for its visitors, primarily targeting {audience}.</p>
// <p>The preferred language is {language}, and the design should reflect {color.ToLower()} tones for visual consistency.</p>
// ";
// }
private void ProceedToLogoStep()
{
ShowLogoStep = true;
}
// private async Task GenerateLogo()
// {
// var logoPrompt = $"Logo for a {entity} named {siteName}, with a {persona} tone, using {Steps[6].Answer} colors.";
// GeneratedLogoUrl = await ReplicateService.GenerateImageAsync(logoPrompt);
// logoUrl = GeneratedLogoUrl;
// }
private async Task GenerateLogo()
{
if (logoGenerationCount >= MaxLogoGenerations)
return;
IsGeneratingLogo = true;
var logoPrompt = $"Logo for a {entity} named {siteName}, with a {persona} tone, using {Steps[6].Answer} colors. DO NOT ADD taglines, or any text other than the brand name: {siteName}.";
GeneratedLogoUrl = await ReplicateService.GenerateLogoAsync(logoPrompt, true);
logoUrl = GeneratedLogoUrl;
logoGenerationCount++;
IsGeneratingLogo = false;
}
private async Task SaveDescription()
{
if (!string.IsNullOrWhiteSpace(GeneratedLogoUrl) && GeneratedLogoUrl.StartsWith("http"))
{
var savedPath = await DownloadAndSaveImage(GeneratedLogoUrl);
if (!string.IsNullOrEmpty(savedPath))
{
logoUrl = savedPath;
}
}
string[] siteInfo = new string[7];
siteInfo[0] = siteName;
siteInfo[1] = generatedDescription;
siteInfo[2] = entity;
siteInfo[3] = persona;
siteInfo[4] = defaultLanguage;
siteInfo[5] = logoUrl;
siteInfo[6] = facebookPageUrl;
await OnDescriptionFinalized.InvokeAsync(siteInfo);
}
private async Task StartRecording() => await JS.InvokeVoidAsync("startRecording");
private async Task StopRecording() => await JS.InvokeVoidAsync("stopRecording");
private class QuestionStep
{
public string Question { get; }
public string Description { get; }
public string Answer { get; set; } = "";
public QuestionStep(string question, string description)
{
Question = question;
Description = description;
}
}
private string GetStepCircleClass(int index)
{
if (index < CurrentStep)
return "step-circle completed";
else if (index == CurrentStep)
return "step-circle current";
else
return "step-circle";
}
private async Task HandleFileUpload(InputFileChangeEventArgs e)
{
if (e.FileCount == 0) return;
try
{
var uploadPath = Path.Combine("wwwroot", "uploads", UserId);
foreach (var file in e.GetMultipleFiles())
{
var folder = GetFolderForFile(file.ContentType);
var folderPath = Path.Combine(uploadPath, folder);
// Create target directory
Directory.CreateDirectory(folderPath);
var filePath = Path.Combine(folderPath, file.Name);
await using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.OpenReadStream(50 * 1024 * 1024).CopyToAsync(stream);
}
var relativePath = $"/uploads/{UserId}/{folder}/{file.Name}";
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)
{
Console.WriteLine($"Error uploading files: {ex.Message}");
}
finally
{
//IsLoading = false;
}
}
private string GetFolderForFile(string contentType)
{
return contentType switch
{
var type when type.StartsWith("image/") => "images",
var type when type.StartsWith("video/") => "videos",
var type when type.StartsWith("audio/") => "audio",
_ => "others"
};
}
private void AppendFilePathToContent(string contentType, string relativePath, string? thumbnailPath = null)
{
if (contentType.StartsWith("image/"))
{
logoUrl = relativePath;
GeneratedLogoUrl = relativePath;
var ThumbnailUrl = thumbnailPath ?? string.Empty;
}
StateHasChanged();
}
private async Task<string?> DownloadAndSaveImage(string imageUrl)
{
try
{
var uploadPath = Path.Combine("wwwroot", "uploads", UserId, "images");
Directory.CreateDirectory(uploadPath);
var fileName = $"logo_{Guid.NewGuid().ToString().Substring(0, 8)}.jpg";
var filePath = Path.Combine(uploadPath, fileName);
var httpClient = HttpClientFactory.CreateClient();
var imageBytes = await httpClient.GetByteArrayAsync(imageUrl);
await File.WriteAllBytesAsync(filePath, imageBytes);
// Generate thumbnail
var thumbnailFolder = Path.Combine(uploadPath, "thumbnails");
Directory.CreateDirectory(thumbnailFolder);
var thumbnailPath = Path.Combine(thumbnailFolder, fileName);
using var image = Image.Load(imageBytes);
image.Mutate(x => x.Resize(new ResizeOptions
{
Size = new Size(300, 0),
Mode = ResizeMode.Max
}));
await image.SaveAsync(thumbnailPath);
return $"/uploads/{UserId}/images/{fileName}";
}
catch (Exception ex)
{
Console.WriteLine($"Error saving logo: {ex.Message}");
return null;
}
}
// STT Hook
private static CreateSiteWizard? _instance;
[JSInvokable]
public static async Task SendAudioToServer(List<byte> audioData)
{
if (_instance is null) return;
var result = await _instance.WhisperService.TranscribeAsync(audioData.ToArray());
if (!string.IsNullOrWhiteSpace(result))
{
_instance.Steps[_instance.CurrentStep].Answer = result;
_instance.StateHasChanged();
}
}
}

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,93 @@
@using BLAIzor.Models
@using BLAIzor.Services
@inject ContentEditorService contentEditorService
@inject CacheService cache
@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
@ -60,7 +61,7 @@ else if (ExtractedMenuItems.Any())
<MenuItemContentEditor Subject=@subject MenuItem="item" WordFile=@document SessionId="SessionId" OnContentUpdated="UpdateMenuItem" /> <MenuItemContentEditor Subject=@subject MenuItem="item" WordFile=@document SessionId="SessionId" OnContentUpdated="UpdateMenuItem" />
} }
<div class="card-footer"> <div class="card-footer">
<button class="btn btn-default btn-sm mt-2" @onclick="() => AddMenuItem()">Add a new menu item after this one</button>
<button class="btn btn-danger btn-sm mt-2" @onclick="() => RemoveMenuItem(item)">Remove</button> <button class="btn btn-danger btn-sm mt-2" @onclick="() => RemoveMenuItem(item)">Remove</button>
</div> </div>
} }
@ -101,7 +102,7 @@ else if (ExtractedMenuItems.Any())
</div> </div>
<button class="btn btn-default btn-sm mt-2" @onclick="() => AddMenuItem()">Add a new menu item after this one</button>
<button class="btn btn-success mt-3" @onclick="() => SaveMenuItems(true)">Save All</button> <button class="btn btn-success mt-3" @onclick="() => SaveMenuItems(true)">Save All</button>
} }
else if (!string.IsNullOrEmpty(ErrorMessage)) else if (!string.IsNullOrEmpty(ErrorMessage))
@ -153,17 +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)
Console.Write($"Found point: {selectedPoint.result.payload.content}"); {
//get vectors to compare
}
model.Content = contentItem.Content;
model.ContentDescription = contentItem.Description;
} }
} }
@ -200,15 +207,18 @@ 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(); var valami = response2.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim('-').Trim())
.ToList() ?? new List<string>();
ExtractedMenuItems = valami.Select(name => new MenuItemModel(name)).ToList();
} }
// Send the content to ChatGPT // Send the content to ChatGPT
} }
@ -246,6 +256,7 @@ else if (!string.IsNullOrEmpty(ErrorMessage))
if (item != null) if (item != null)
{ {
//item.MenuItem. //item.MenuItem.
item.MenuItem.Name = updatedItem.MenuItem.Name;
item.Content = updatedItem.Content; item.Content = updatedItem.Content;
} }
} }
@ -253,7 +264,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;
@ -287,10 +298,12 @@ else if (!string.IsNullOrEmpty(ErrorMessage))
foreach (var menuItemToReorder in menuItems) foreach (var menuItemToReorder in menuItems)
{ {
menuItemToReorder.SortOrder = menuItems.IndexOf(menuItemToReorder); menuItemToReorder.SortOrder = menuItems.IndexOf(menuItemToReorder);
Console.WriteLine($"{menuItemToReorder.Name}, {menuItemToReorder.SortOrder}");
} }
foreach (var extractedMenuItemToReorder in ExtractedMenuItems) foreach (var extractedMenuItemToReorder in ExtractedMenuItems)
{ {
extractedMenuItemToReorder.MenuItem.SortOrder = ExtractedMenuItems.IndexOf(extractedMenuItemToReorder); extractedMenuItemToReorder.MenuItem.SortOrder = ExtractedMenuItems.IndexOf(extractedMenuItemToReorder);
Console.WriteLine($"{extractedMenuItemToReorder.MenuItem.Name}, {extractedMenuItemToReorder.MenuItem.SortOrder}");
} }
JSRuntime.InvokeVoidAsync("eval", $"document.querySelector('.my-class').classList.remove('my-class')"); JSRuntime.InvokeVoidAsync("eval", $"document.querySelector('.my-class').classList.remove('my-class')");
})); }));

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
@ -62,7 +63,7 @@ else if (extractedMenuItems.Any())
<MenuItemContentEditor Subject=@subject MenuItem="item" OnContentUpdated="UpdateMenuItem" SessionId="SessionId" /> <MenuItemContentEditor Subject=@subject MenuItem="item" OnContentUpdated="UpdateMenuItem" SessionId="SessionId" />
<div class="card-footer"> <div class="card-footer">
<button class="btn btn-danger btn-sm mt-2" @onclick="() => RemoveMenuItem(item)">Remove</button> <button class="btn btn-danger btn-sm mt-2" @onclick="() => RemoveMenuItem(item)">Remove</button>
<button class="btn btn-default btn-sm mt-2" @onclick="() => AddMenuItem()">Add a new menu item after this one</button>
</div> </div>
} }
} }
@ -77,7 +78,7 @@ else if (extractedMenuItems.Any())
</div> </div>
</div> </div>
<button class="btn btn-default btn-sm mt-2" @onclick="() => AddMenuItem()">Add a new menu item after this one</button>
<button class="btn btn-success mt-3" @onclick="() => SaveMenuItems(true)">Save All</button> <button class="btn btn-success mt-3" @onclick="() => SaveMenuItems(true)">Save All</button>
} }
@ -122,23 +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)
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);
} }
@ -161,8 +166,11 @@ 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(); var valami = response.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim('-').Trim())
.ToList() ?? new List<string>();
extractedMenuItems = valami.Select(name => new MenuItemModel(name)).ToList();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -182,6 +190,7 @@ else if (!string.IsNullOrEmpty(errorMessage))
var newItem = new MenuItemModel("New menu item", ""); var newItem = new MenuItemModel("New menu item", "");
//TODO Fix //TODO Fix
extractedMenuItems.Add(newItem); extractedMenuItems.Add(newItem);
isLoading = false;
} }
@ -206,7 +215,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,195 @@
@* @page "/site/{SiteId:int}/generate" *@
@using BLAIzor.Models
@using BLAIzor.Services
@inject HttpClient Http
@inject ContentEditorAIService contentEditorAIService
@inject ContentEditorService contentEditorService
@inject NavigationManager Navigation
<div class="container-fluid">
@if (!string.IsNullOrEmpty(errorMessage))
{
<p class="text-red-600">Error: @errorMessage</p>
}
else if (isLoading || GeneratedItems.Any())
{
<RadzenLayout Style="grid-template-areas: 'rz-sidebar rz-header' 'rz-sidebar rz-body'">
<RadzenHeader>
<RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" Gap="0">
<RadzenSidebarToggle Click="@(() => sidebarExpanded = !sidebarExpanded)" />
<RadzenLabel Text=@(totalItemsToGenerate.ToString() + " page(s)") />
</RadzenStack>
</RadzenHeader>
<RadzenSidebar @bind-Expanded="@sidebarExpanded">
<RadzenPanelMenu>
@foreach (var item in GeneratedItems)
{
<RadzenPanelMenuItem Text="@item.Title" Icon="account_box" Click="@(() => SelectContent(item.Title))" />
}
</RadzenPanelMenu>
<button class="btn btn-primary mt-4" @onclick="SaveContent">Save All</button>
</RadzenSidebar>
<RadzenBody Style="padding: 0px !important">
@if (isLoading)
{
<p class="text-muted">Generating... please wait.</p>
//progress bar
<RadzenProgressBar Value="@generationProgress" ShowValue="true" Style="width: 100%;" />
}
else
{
if (SelectedItem != null)
{
<textarea class="w-100 p-2 rounded text-white bg-panel-gradient" style="min-height:500px" @bind="SelectedItem.Content"></textarea>
}
}
</RadzenBody>
</RadzenLayout>
}
else
{
<button class="btn btn-success" @onclick="GeneratePageList">Start Generation</button>
}
</div>
@code {
[Parameter]
public int SiteId { get; set; }
[Parameter] public string SessionId { get; set; } = string.Empty;
private SiteInfo site;
private bool isLoading = false;
private string errorMessage = string.Empty;
private List<ContentItem> GeneratedItems = new();
private ContentGroup defaultGroup;
private ContentItem SelectedItem;
private double generationProgress = 0.0;
private int totalItemsToGenerate = 0;
bool sidebarExpanded = true;
protected override async Task OnInitializedAsync()
{
site = await contentEditorService.GetSiteInfoByIdAsync(SiteId);
var existingGroup = await contentEditorService.GetContentGroupsBySiteInfoIdAsync(SiteId);
if (!existingGroup.Any(x => x.Name == "Pages"))
{
defaultGroup = new ContentGroup
{
SiteInfoId = SiteId,
Name = "Pages",
Slug = "pages",
Type = "Page",
VectorSize = 1536,
EmbeddingModel = "openai",
Version = 0
};
// Save ContentGroup to DB (if not already exists)
var response = await contentEditorService.CreateContentGroupAsync(defaultGroup);
defaultGroup = response;
}
else
{
defaultGroup = existingGroup.FirstOrDefault(x => x.Name == "Pages");
}
}
private void SelectContent(string title)
{
SelectedItem = GeneratedItems.Where(x => x.Title == title).FirstOrDefault();
StateHasChanged();
}
private async Task GeneratePageList()
{
isLoading = true;
errorMessage = string.Empty;
GeneratedItems.Clear();
try
{
var pageListPrompt = $"Based on the following site description, list the main website pages in {site.DefaultLanguage} as a comma-separated list. " +
"Only return the page titles." +
$"Site Description: {site.SiteDescription}";
var pageListText = await contentEditorAIService.GetMenuSuggestionsAsync(SessionId, pageListPrompt);
var pageTitles = pageListText
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.Trim())
.Where(t => !string.IsNullOrWhiteSpace(t))
.Distinct()
.ToList();
totalItemsToGenerate = pageTitles.Count;
foreach (var title in pageTitles)
{
var itemPrompt = $"Write a full, well-structured website page text content in {site.DefaultLanguage} for a page titled {title}. " +
$"The tone should be {site.Persona}, and match the following site description: '{site.SiteDescription}' " +
"Include headers, clear sections, and persuasive copy.";
var content = await contentEditorAIService.GetGeneratedContentAsync(SessionId, itemPrompt);
GeneratedItems.Add(new ContentItem
{
Title = title,
Description = $"A {defaultGroup.Type} called {title} of a website called {site.SiteName}.",
Language = site.DefaultLanguage,
Content = content,
ContentGroupId = defaultGroup.Id,
Tags = title,
IsPublished = false,
Version = 0
});
generationProgress += 100.0 / totalItemsToGenerate;
// Refresh UI after each item is generated
StateHasChanged();
}
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
finally
{
SelectedItem = GeneratedItems.FirstOrDefault();
isLoading = false;
StateHasChanged();
}
}
private async Task SaveContent()
{
isLoading = true;
errorMessage = string.Empty;
try
{
foreach (var item in GeneratedItems)
{
//await Http.PostAsJsonAsync("/api/contentitem", item);
await contentEditorService.CreateContentItemAsync(item, site.VectorCollectionName);
}
Navigation.NavigateTo($"/site/{SiteId}/overview");
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
finally
{
isLoading = false;
}
}
}

View File

@ -0,0 +1,511 @@
@page "/site/{SiteId:int}/generate"
@using BLAIzor.Interfaces
@using BLAIzor.Models
@using BLAIzor.Services
@using System.Text.RegularExpressions
@using SixLabors.ImageSharp
@using SixLabors.ImageSharp.Processing
@inject HttpClient Http
@inject ContentEditorAIService contentEditorAIService
@inject ContentEditorService contentEditorService
@inject NavigationManager Navigation
@inject ReplicateService ReplicateService
@inject IHttpClientFactory HttpClientFactory
@inject IBrightDataService BrightDataService
<div class="container-fluid flex">
<!-- Sidebar -->
@if (GeneratedItems.Any())
{
<aside class="w-64 border-r p-4 flex flex-col">
<div class="text-lg font-semibold mb-4">Pages (@totalItemsToGenerate)</div>
<nav class="flex-1 overflow-auto space-y-2">
@foreach (var item in GeneratedItems)
{
<button class="btn @(item == SelectedItem ? "btn-primary font-semibold" : "bg-panel-gradient")"
@onclick="@(() => SelectContent(item.Title))">
@item.Title
</button>
}
</nav>
@if (isLoading || isGeneratingImages)
{
<button disabled class="btn py-2 px-4 rounded"
@onclick="SaveContent">
💾 Please wait...
</button>
}
else
{
<button class="btn py-2 px-4 rounded"
@onclick="SaveContent">
💾 Save All
</button>
}
</aside>
}
<!-- Main Content -->
<div class="row">
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="text-red-600 font-medium mb-4">⚠️ @errorMessage</div>
}
@if (isLoading)
{
<div class="text-gray-700 mb-4" style="width:fit-content !important; margin: 0 auto !important">
@if (!string.IsNullOrEmpty(message))
{
<p>@message</p>
}
@if (GeneratedItems.Any())
{
<RadzenProgressBarCircular Style="margin: 0 auto !important" ProgressBarStyle="ProgressBarStyle.Primary" Value="@(Math.Round(generationProgress, 0))" ShowValue="true" Size="ProgressBarCircularSize.Large" />
}
else
{
<RadzenProgressBarCircular Style="margin: 0 auto !important" ProgressBarStyle="ProgressBarStyle.Light" Value="100" ShowValue="false" Mode="ProgressBarMode.Indeterminate" Size="ProgressBarCircularSize.Large" />
}
</div>
@* <RadzenProgressBar Value="@generationProgress" ShowValue="true" Style="width: 100%;" /> *@
}
else if (SelectedItem != null)
{
@if (isGeneratingImages)
{
<div class="col-12">
<div class="text-gray-700 mb-4" style="width:fit-content !important; margin: 0 auto !important">
<p>Generating pages... please wait.</p>
@* <RadzenProgressBarCircular Style="margin: 0 auto !important" ProgressBarStyle="ProgressBarStyle.Success" Value="@(Math.Round(generationProgress, 0))" ShowValue="true" Size="ProgressBarCircularSize.Large" /> *@
<RadzenProgressBar Value="@(Math.Round(generationProgress, 0))" ShowValue="true" Style="width: 100%;" />
</div>
</div>
}
<div class="col-12">
<p>
Please review the generated content and the images. You are free to modify the text, but please don't remove photo tags, they are handled automatically.
Please approve the images you like. You can also regenerate, and select photos from your device.
</p>
</div>
<!-- Content editor -->
<div class="col-sm-12 col-md-7">
<h2 class="text-xl font-bold mb-2">@SelectedItem.Title</h2>
<textarea class="p-4 rounded text-white bg-panel-gradient" style="height:500px; width: 100%; border: 0px;"
@bind="SelectedItem.Content"></textarea>
</div>
<!-- Photo slots -->
<div class="col-sm-12 col-md-5">
<h2 class="text-xl font-bold mb-2">Suggested Photos</h2>
<div class="row" style="height: 500px; overflow: scroll">
@if (SelectedItem.PhotoSlots.Any())
{
@foreach (var slot in SelectedItem.PhotoSlots)
{
<div class="col-6 rounded p-1">
<div class="bg-panel-gradient text-white m-1">
@* <p class="text-sm mb-2">@slot.Description</p> *@
@if (!string.IsNullOrEmpty(slot.ImageUrl))
{
<div style="width: 100%; position: relative;">
<p>Photo_slot_@(SelectedItem.PhotoSlots.IndexOf(slot) + 1)</p>
<div style="position: absolute; height: 100px; top: 0px; right: 0px;">
<button class="btn btn-sm btn-primary flex-1"
@onclick="() => ReplaceUrls(slot)">
✓ Approve
</button>
</div>
<img src="@slot.ImageUrl" alt="@slot.Description" class="img-fluid" />
</div>
}
else
{
<div style="width: 100%; height: 270px;">
<RadzenProgressBarCircular Style="width:100%; margin: 0 auto;" ProgressBarStyle="ProgressBarStyle.Light" ShowValue="true" Mode="ProgressBarMode.Indeterminate" Size="ProgressBarCircularSize.Large">
<Template>Wait</Template>
</RadzenProgressBarCircular>
</div>
}
<div class="flex gap-2">
<button class="btn btn-primary flex-1"
@onclick="() => RegeneratePhoto(slot, false)">
🎨 Regenerate
</button>
<button class="btn flex-1"
@onclick="() => SelectPhoto(slot)">
📂 Select
</button>
</div>
</div>
</div>
}
}
else
{
<p class="text-gray-400 text-sm">No photo suggestions yet.</p>
}
</div>
</div>
}
else if (!GeneratedItems.Any())
{
<div class="flex flex-col items-center justify-center h-full text-center text-gray-500">
<p class="mb-4">Click the button below to generate your site's pages based on its description.</p>
<button class="btn text-white py-2 px-6 rounded"
@onclick="GeneratePageList">
🚀 Start Generation
</button>
</div>
}
</div>
</div>
@code {
[Parameter] public int SiteId { get; set; }
[Parameter] public string SessionId { get; set; } = string.Empty;
private SiteInfo site;
private bool isLoading = false;
private string errorMessage = string.Empty;
private string message = "Generating pages... please wait.";
private List<GeneratedContentItem> GeneratedItems = new();
private ContentGroup defaultGroup;
private GeneratedContentItem SelectedItem;
private double generationProgress = 0.0;
private int totalItemsToGenerate = 0;
private bool isGeneratingImages = false;
protected override async Task OnInitializedAsync()
{
site = await contentEditorService.GetSiteInfoByIdAsync(SiteId);
var existingGroup = await contentEditorService.GetContentGroupsBySiteInfoIdAsync(SiteId);
if (!existingGroup.Any(x => x.Name == "Pages"))
{
defaultGroup = new ContentGroup
{
SiteInfoId = SiteId,
Name = "Pages",
Slug = "pages",
Type = "Page",
VectorSize = 1536,
EmbeddingModel = "openai",
Version = 0
};
defaultGroup = await contentEditorService.CreateContentGroupAsync(defaultGroup);
}
else
{
defaultGroup = existingGroup.First(x => x.Name == "Pages");
var content = await contentEditorService.GetContentItemsByGroupIdAsync(defaultGroup.Id);
GeneratedItems = content.Select(c => new GeneratedContentItem
{
Id = c.Id,
Title = c.Title,
Description = c.Description,
Language = c.Language,
Content = c.Content,
ContentGroupId = c.ContentGroupId,
Tags = c.Tags,
IsPublished = c.IsPublished,
Version = c.Version
}).ToList();
}
}
private void SelectContent(string title)
{
SelectedItem = GeneratedItems.FirstOrDefault(x => x.Title == title);
StateHasChanged();
}
private async Task GeneratePageList()
{
isLoading = true;
errorMessage = string.Empty;
GeneratedItems.Clear();
string facebookContent = string.Empty;
if (!string.IsNullOrWhiteSpace(site.FacebookUrl))
{
message = "Reading Facebook posts... please wait.";
var valami = await BrightDataService.ScrapeFacebookPostsAsync(site.FacebookUrl, 20);
if (valami == null || !valami.Any())
{
errorMessage = "No Facebook posts found. Please check the URL.";
isLoading = false;
}
else
{
var grabFacebookPrompt = $"Based on the following Facebook posts, extract the main topics and themes that could be used to generate website pages. " +
$"Focus on the content relevant to the site description: {site.SiteDescription}. " +
$"Content found on Facebook in JSON format: {valami}";
facebookContent = await contentEditorAIService.GetFacebookContentAsync(SessionId, grabFacebookPrompt);
}
}
try
{
message = "Generating pages... please wait.";
// 1⃣ Ask AI for the page titles first
var pageListPrompt = $"Based on the following site description, list the main website pages in {site.DefaultLanguage} as a comma-separated list. " +
"Only return the page titles." +
$"Site Description: {site.SiteDescription}." +
$"Content found on facebook: {facebookContent}";
var pageListText = await contentEditorAIService.GetMenuSuggestionsAsync(SessionId, pageListPrompt);
var pageTitles = pageListText
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.Trim())
.Where(t => !string.IsNullOrWhiteSpace(t))
.Distinct()
.ToList();
totalItemsToGenerate = pageTitles.Count;
foreach (var title in pageTitles)
{
message = $"Generating content for page: {title}... please wait.";
// 2⃣ Generate content WITH inline photo placeholders
var itemPrompt =
$"Write a full, well-structured website page text content in {site.DefaultLanguage} for a page titled '{title}'. " +
$"The tone should be {site.Persona}, and match the following site description: '{site.SiteDescription}'. " +
"Include headers, clear sections, and persuasive copy. " +
"Insert image placeholders in the text like [PHOTO_SLOT_1: short image description] at natural points where an illustration would be helpful. " +
"Number them sequentially starting from 1.";
var content = await contentEditorAIService.GetGeneratedContentAsync(SessionId, itemPrompt);
// 3⃣ Extract PhotoSlots from placeholders
var matches = Regex.Matches(content, @"\[PHOTO_SLOT_(\d+):\s*(.+?)\]");
var photoSlots = matches
.Select(m => new PhotoSlot { Description = m.Groups[2].Value.Trim() })
.ToList();
// 4⃣ Build GeneratedContentItem
var generatedItem = new GeneratedContentItem
{
Title = title,
Description = $"A {defaultGroup.Type} called {title} of a website called {site.SiteName}.",
Language = site.DefaultLanguage,
Content = content,
ContentGroupId = defaultGroup.Id,
Tags = title,
IsPublished = false,
Version = 0,
PhotoSlots = photoSlots
};
GeneratedItems.Add(generatedItem);
generationProgress += 100.0 / totalItemsToGenerate;
StateHasChanged();
}
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
finally
{
message = "Generating images... please wait.";
SelectedItem = GeneratedItems.FirstOrDefault();
isLoading = false;
generationProgress = 0;
isGeneratingImages = true;
StateHasChanged();
foreach (var page in GeneratedItems)
{
totalItemsToGenerate += page.PhotoSlots.Count;
}
foreach (var page in GeneratedItems)
{
foreach (var image in page.PhotoSlots)
{
await GeneratePhoto(image, false);
generationProgress += 100.0 / totalItemsToGenerate;
StateHasChanged();
}
}
isGeneratingImages = false;
StateHasChanged();
}
}
private async Task SaveContent()
{
isLoading = true;
errorMessage = string.Empty;
try
{
int i = 0;
foreach (var item in GeneratedItems)
{
//get all photo urls from content and replace them with uploaded version
// if (!string.IsNullOrWhiteSpace(GeneratedLogoUrl) && GeneratedLogoUrl.StartsWith("http"))
// {
// var savedPath = await DownloadAndSaveImage(GeneratedLogoUrl);
// if (!string.IsNullOrEmpty(savedPath))
// {
// logoUrl = savedPath;
// }
// }
item.Content = Regex.Replace(SelectedItem.Content, @"\[PHOTO_SLOT_\d+:[^\]]*\]", "");
var contentItem = await contentEditorService.CreateContentItemAsync(item, site.VectorCollectionName);
var newMenuItem = new MenuItem
{
SiteInfoId = SiteId,
ContentItemId = contentItem.Id,
ContentGroupId = defaultGroup.Id,
ShowInMainMenu = true,
Slug = contentItem.Title.ToLower(),
SortOrder = i,
Name = contentItem.Title
};
await contentEditorService.AddMenuItemAsync(newMenuItem);
i++;
}
Navigation.NavigateTo($"/preview/{SiteId}");
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
finally
{
isLoading = false;
}
}
private async Task RegeneratePhoto(PhotoSlot slot, bool removeBackground)
{
slot.ImageUrl = "";
StateHasChanged();
await GeneratePhoto(slot, removeBackground);
}
private async Task GeneratePhoto(PhotoSlot slot, bool removeBackground)
{
if (SelectedItem == null)
{
errorMessage = "No content item selected.";
return;
}
var userPrompt =
$"Generate an image prompt for me for AN ILLUSTRATION. Use the colors: {site.DefaultColor}. " +
$"The photo should have a {site.Persona} feel, and must be in {site.DesignStyle} style " +
$"My general photo idea: {slot.Description}. " +
"DO NOT generate a UI design or UX design prompt, the image will be part of the website content.";
var imagePrompt = await contentEditorAIService.GetPhotoPromptAsync(SessionId, userPrompt);
try
{
var imageUrl = await ReplicateService.GenerateImageAsync(imagePrompt, removeBackground);
slot.ImageUrl = imageUrl;
}
catch (Exception ex)
{
errorMessage = $"Image generation failed: {ex.Message}";
}
}
private async Task ReplaceUrls(PhotoSlot slot)
{
// Find the index of this slot in the SelectedItem's PhotoSlots list
var slotIndex = SelectedItem.PhotoSlots.IndexOf(slot);
if (slotIndex >= 0)
{
var imageUrl = await DownloadAndSaveImage(slot.ImageUrl);
// Replace placeholder with actual image URL
SelectedItem.Content = Regex.Replace(
SelectedItem.Content,
$@"\[PHOTO_SLOT_{slotIndex + 1}:[^\]]*\]",
$"[Photo URL: {imageUrl}]"
);
slot.ImageUrl = imageUrl;
}
}
private async Task<string?> DownloadAndSaveImage(string imageUrl)
{
try
{
var uploadPath = Path.Combine("wwwroot", "uploads", site.UserId, "images");
Directory.CreateDirectory(uploadPath);
var fileName = $"logo_{Guid.NewGuid().ToString().Substring(0, 8)}.jpg";
var filePath = Path.Combine(uploadPath, fileName);
var httpClient = HttpClientFactory.CreateClient();
var imageBytes = await httpClient.GetByteArrayAsync(imageUrl);
await File.WriteAllBytesAsync(filePath, imageBytes);
// Generate thumbnail
var thumbnailFolder = Path.Combine(uploadPath, "thumbnails");
Directory.CreateDirectory(thumbnailFolder);
var thumbnailPath = Path.Combine(thumbnailFolder, fileName);
using var image = Image.Load(imageBytes);
image.Mutate(x => x.Resize(new ResizeOptions
{
Size = new Size(300, 0),
Mode = ResizeMode.Max
}));
await image.SaveAsync(thumbnailPath);
return $"/uploads/{site.UserId}/images/{fileName}";
}
catch (Exception ex)
{
Console.WriteLine($"Error saving logo: {ex.Message}");
return null;
}
}
private void SelectPhoto(PhotoSlot slot)
{
// TODO: implement image picker dialog
// For now, just simulate selecting an existing image
slot.ImageUrl = "/images/placeholder.png";
}
}

View File

@ -0,0 +1,85 @@
@using Microsoft.AspNetCore.Components.Forms
@* <InputTextArea @bind-Value="RawHtml" class="form-control" rows="10" /> *@
<RadzenHtmlEditor @bind-Value=@RawHtml style="height: 70vh; color:#000; background-color: transparent !important;" Input=@OnInput Change=@OnChange Paste=@OnPaste UploadComplete=@OnUploadComplete Execute=@OnExecute UploadUrl="upload/image">
<RadzenHtmlEditorUndo />
<RadzenHtmlEditorRedo />
<RadzenHtmlEditorSource />
</RadzenHtmlEditor>
<div class="mt-3 d-flex justify-content-end gap-2">
<button class="btn btn-primary" @onclick="OnSaveClicked">Save</button>
<button class="btn btn-secondary" @onclick="OnCancelClicked">Cancel</button>
</div>
@if (ShowPreview)
{
<div class="mt-4">
<h6>Live Preview:</h6>
<div class="border p-3 bg-light" style="min-height: 100px;" @key="RawHtml">
@((MarkupString)RawHtml)
</div>
</div>
}
@code {
[Parameter]
public string Html { get; set; } = string.Empty;
[Parameter]
public Func<string, Task> HtmlChanged { get; set; }
private string RawHtml = string.Empty;
private bool ShowPreview = false;
protected override void OnInitialized()
{
RawHtml = Html;
}
private async Task OnSaveClicked()
{
// await HtmlChanged.InvokeAsync(RawHtml);
if (HtmlChanged != null)
await HtmlChanged.Invoke(RawHtml);
}
private void OnCancelClicked()
{
RawHtml = Html;
}
async Task OnPaste(HtmlEditorPasteEventArgs args)
{
// console.Log($"Paste: {args.Html}");
// MenuItem.Content = args.Html;
// await OnContentUpdated.InvokeAsync(MenuItem);
}
async Task OnChange(string html)
{
// console.Log($"Change: {html}");
// MenuItem.Content = html;
// await OnContentUpdated.InvokeAsync(MenuItem);
}
async Task OnInput(string html)
{
// console.Log($"Input: {html}");
// MenuItem.Content = html;
// await OnContentUpdated.InvokeAsync(MenuItem);
}
void OnExecute(HtmlEditorExecuteEventArgs args)
{
//console.Log($"Execute: {args.CommandName}");
}
void OnUploadComplete(UploadCompleteEventArgs args)
{
//console.Log($"Upload complete: {args.RawResponse}");
}
}

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 group</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,154 @@

@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<SiteInfo, 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);
if (OnSiteInfoSaveClicked != null)
await OnSiteInfoSaveClicked.Invoke(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);
}
}

File diff suppressed because one or more lines are too long

View File

@ -2,11 +2,12 @@
@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
<div class="menu-item-editor"> <div class="menu-item-editor">
@{ @{
if (!string.IsNullOrEmpty(WordFile)) if (!string.IsNullOrEmpty(WordFile))
{ {
@ -17,14 +18,20 @@
} }
<button class="btn btn-primary mb-2" @onclick="GenerateContent" disabled="@IsLoading">Generate by AI</button> <button class="btn btn-primary mb-2" @onclick="GenerateContent" disabled="@IsLoading">Generate by AI</button>
@* <textarea class="form-control border-0 text-white" @bind="GeneratedContent" rows="5"></textarea> *@ @* <textarea class="form-control border-0 text-white" @bind="GeneratedContent" rows="5"></textarea> *@
<RadzenHtmlEditor @bind-Value=@GeneratedContent style="height: 450px; color:#000; background-color: rgba(255,255,255,0.4)" Input=@OnInput Change=@OnChange Paste=@OnPaste UploadComplete=@OnUploadComplete Execute=@OnExecute UploadUrl="upload/image"> <div class="row">
<RadzenHtmlEditorUndo />
<RadzenHtmlEditorRedo />
<RadzenHtmlEditorSource />
</RadzenHtmlEditor>
<InputFile class="btn btn-default" type="file" multiple OnChange=HandleFileUpload accept=".mp3,.mp4,.jpg,.png" />
<EventConsole @ref=@console /> <RadzenTextArea @bind-Value=@MenuItem.ContentDescription></RadzenTextArea>
</div>
<div class="row">
<RadzenHtmlEditor @bind-Value=@GeneratedContent style="height: 450px; color:#000; background-color: rgba(255,255,255,0.4)" Input=@OnInput Change=@OnChange Paste=@OnPaste UploadComplete=@OnUploadComplete Execute=@OnExecute UploadUrl="upload/image">
<RadzenHtmlEditorUndo />
<RadzenHtmlEditorRedo />
<RadzenHtmlEditorSource />
</RadzenHtmlEditor>
</div>
<InputFile class="btn btn-default" type="file" multiple OnChange=HandleFileUpload accept=".mp3,.mp4,.jpg,.png" />
@* <EventConsole @ref=@console /> *@
@if (IsLoading) @if (IsLoading)
{ {
<p>Loading content...</p> <p>Loading content...</p>
@ -40,15 +47,17 @@
private bool IsLoading = false; private bool IsLoading = false;
private string GeneratedContent = string.Empty; private string GeneratedContent = string.Empty;
// private string Description = string.Empty;
private string? userId; private string? userId;
private string? userName; private string? userName;
private AuthenticationState? authState; private AuthenticationState? authState;
EventConsole console; //EventConsole console;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
GeneratedContent = MenuItem.Content; GeneratedContent = MenuItem.Content;
// Description = MenuItem.ContentDescription;
authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (authState.User.Identity?.IsAuthenticated == true) if (authState.User.Identity?.IsAuthenticated == true)
{ {
@ -64,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);
} }
@ -85,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);
} }
@ -103,33 +112,33 @@
async Task OnPaste(HtmlEditorPasteEventArgs args) async Task OnPaste(HtmlEditorPasteEventArgs args)
{ {
console.Log($"Paste: {args.Html}"); // console.Log($"Paste: {args.Html}");
MenuItem.Content = args.Html; MenuItem.Content = args.Html;
await OnContentUpdated.InvokeAsync(MenuItem); await OnContentUpdated.InvokeAsync(MenuItem);
} }
async Task OnChange(string html) async Task OnChange(string html)
{ {
console.Log($"Change: {html}"); // console.Log($"Change: {html}");
MenuItem.Content = html; MenuItem.Content = html;
await OnContentUpdated.InvokeAsync(MenuItem); await OnContentUpdated.InvokeAsync(MenuItem);
} }
async Task OnInput(string html) async Task OnInput(string html)
{ {
console.Log($"Input: {html}"); // console.Log($"Input: {html}");
MenuItem.Content = html; MenuItem.Content = html;
await OnContentUpdated.InvokeAsync(MenuItem); await OnContentUpdated.InvokeAsync(MenuItem);
} }
void OnExecute(HtmlEditorExecuteEventArgs args) void OnExecute(HtmlEditorExecuteEventArgs args)
{ {
console.Log($"Execute: {args.CommandName}"); //console.Log($"Execute: {args.CommandName}");
} }
void OnUploadComplete(UploadCompleteEventArgs args) void OnUploadComplete(UploadCompleteEventArgs args)
{ {
console.Log($"Upload complete: {args.RawResponse}"); //console.Log($"Upload complete: {args.RawResponse}");
} }
private async Task HandleFileUpload(InputFileChangeEventArgs e) private async Task HandleFileUpload(InputFileChangeEventArgs e)
@ -167,7 +176,7 @@
} }
catch (Exception ex) catch (Exception ex)
{ {
console.Log($"Error uploading files: {ex.Message}"); // console.Log($"Error uploading files: {ex.Message}");
} }
finally finally
{ {

View File

@ -0,0 +1,249 @@
@using BLAIzor.Models
@using BLAIzor.Services
@using Newtonsoft.Json
@using BLAIzor.Components.Partials
@using System.Collections.ObjectModel
@inject ContentEditorService ContentEditorService
@inject ContentEditorAIService ContentEditorAIService
@inject HtmlSnippetProcessor HtmlSnippetProcessor
@inject QDrantService QDrantService
@inject IJSRuntime JSRuntime
@* <div>
<label for="subject">Enter the subject of your website:</label>
<input id="subject" @bind="subject" class="form-control" placeholder="e.g., Web Design Company" />
<button class="btn btn-primary mt-2" @onclick="GenerateMenuItems">Suggest New by AI</button>
</div> *@
@if (isLoading)
{
<p>Loading suggestions...</p>
}
else if (extractedMenuItems.Any())
{
<h4>Menu Items Editor</h4>
<div class="row">
<div class="rz-p-sm-12">
<RadzenStack Orientation="Orientation.Horizontal" JustifyContent="JustifyContent.Right">
<RadzenButton Click="ToggleReorder">
@(allowReorder ? "Done Reordering" : "Reorder")
</RadzenButton>
</RadzenStack>
@if (allowReorder)
{
<RadzenDataGrid @ref="dataGrid" TItem="MenuItem" RowSelect=@OnRowSelect AllowFiltering="true" AllowColumnResize="true" AllowSorting="true" PageSize="20" AllowPaging="true"
Data="@menuItems" ColumnWidth="300px" SelectionMode="DataGridSelectionMode.Single" RowRender="@RowRender">
<Columns>
<RadzenDataGridColumn Property="@nameof(MenuItem.SortOrder)" Title="Sort Order" Width="40px" />
<RadzenDataGridColumn Property="@nameof(MenuItem.Name)" Title="Menu Name" Frozen="true" Width="200px" />
<RadzenDataGridColumn Property="@nameof(MenuItem.ShowInMainMenu)" Title="Visible" Width="40px" />
</Columns>
</RadzenDataGrid>
}
else
{
<RadzenAccordion class="admin-accordion" Multiple="false">
<Items>
@foreach (var item in extractedMenuItems)
{
<RadzenAccordionItem Text="@item.MenuItem.Name" Icon="menu">
<input @bind="item.MenuItem.Name" class="form-control mb-2" placeholder="Menu item name" />
<label>Parent Menu (optional):</label>
<select class="form-control mb-2" @bind="item.MenuItem.ParentId">
<option value="">-- No Parent --</option>
@foreach (var parent in extractedMenuItems)
{
if (parent != item)
{
<option value="@parent.MenuItem.Id">@parent.MenuItem.Name</option>
}
}
</select>
<label>Attach Content Item:</label>
<select class="form-control mb-2" @bind="item.MenuItem.ContentItemId">
<option value="">-- Select --</option>
@foreach (var contentItem in allContentItems)
{
<option value="@contentItem.Id">@contentItem.Title</option>
}
</select>
<textarea class="form-control mt-2" @bind="item.Content" placeholder="Optional inline content"></textarea>
<div class="card-footer">
<button class="btn btn-danger btn-sm mt-2" @onclick="() => RemoveMenuItem(item)">Remove</button>
</div>
</RadzenAccordionItem>
}
</Items>
</RadzenAccordion>
}
</div>
</div>
<button class="btn btn-default btn-sm mt-3" @onclick="() => AddMenuItem()">Add Menu Item</button>
<button class="btn btn-success mt-3" @onclick="() => SaveMenuItems(true)">Save All</button>
}
else if (!string.IsNullOrEmpty(errorMessage))
{
<p class="text-danger">@errorMessage</p>
}
@code {
[Parameter] public int SiteId { get; set; }
[Parameter] public string SessionId { get; set; }
private string subject = string.Empty;
ObservableCollection<MenuItem> menuItems = new();
IList<MenuItem> selectedMenuItems;
private List<MenuItemModel> extractedMenuItems = new();
private List<ContentItem> allContentItems = new();
private bool MenuItemsSaved = false;
private bool isLoading = false;
private string? errorMessage;
private bool hasCollection = false;
private bool allowReorder = false;
private RadzenDataGrid<MenuItem> dataGrid;
MenuItem draggedItem;
MenuItemModel SelectedMenuItemModel = new("");
protected override async Task OnParametersSetAsync()
{
var site = await ContentEditorService.GetSiteInfoByIdAsync(SiteId);
subject = site.SiteDescription ?? string.Empty;
menuItems = new ObservableCollection<MenuItem>((await ContentEditorService.GetMenuItemsBySiteIdAsync(SiteId)).OrderBy(x => x.SortOrder));
allContentItems = await ContentEditorService.GetAllContentItemsBySiteIdAsync(SiteId);
var collectionResult = await QDrantService.GetCollectionBySiteIdAsync(SiteId);
hasCollection = !string.IsNullOrEmpty(collectionResult);
if (menuItems.Count > 0)
{
foreach (var menuItem in menuItems)
{
var model = new MenuItemModel("") { MenuItem = menuItem };
if (menuItem.ContentItemId != null)
{
var contentItem = await ContentEditorService.GetContentItemByIdAsync(menuItem.ContentItemId.Value);
if (contentItem != null)
{
model.Content = contentItem.Content;
model.ContentDescription = contentItem.Description;
}
}
extractedMenuItems.Add(model);
}
MenuItemsSaved = true;
}
}
// private async Task GenerateMenuItems()
// {
// if (string.IsNullOrWhiteSpace(subject))
// {
// errorMessage = "Please enter a subject.";
// return;
// }
// isLoading = true;
// try
// {
// var prompt = $"Suggest a list of menu items for a website about: {subject}. Please do not attach any explanation.";
// var response = await ContentEditorAIService.GetMenuSuggestionsAsync(SessionId, prompt);
// extractedMenuItems = response.Select(name => new MenuItemModel(name)).ToList();
// }
// catch
// {
// errorMessage = "An error occurred while generating menu items.";
// }
// finally { isLoading = false; }
// }
private async Task AddMenuItem()
{
MenuItemModel newModel = new MenuItemModel("New Menu Item");
MenuItem newItem = new MenuItem();
newItem.SiteInfoId = SiteId;
newItem.Name = "New Menu Item";
newItem.SortOrder = extractedMenuItems.Count+1;
var result = await ContentEditorService.AddMenuItemAsync(newItem);
newModel.MenuItem = result;
extractedMenuItems.Add(newModel);
if(result == null)
{
extractedMenuItems.Remove(newModel);
}
}
private async Task RemoveMenuItem(MenuItemModel item){
await ContentEditorService.DeleteMenuItemAsync(item.MenuItem.Id);
extractedMenuItems.Remove(item);
}
private async Task SaveMenuItems(bool updateVectorDatabase)
{
//var result = await ContentEditorAIService.ProcessMenuItems(SiteId, hasCollection, extractedMenuItems, subject, MenuItemsSaved, updateVectorDatabase);
try
{
foreach (var menuItemToUpdate in extractedMenuItems)
{
var result2 = await ContentEditorService.UpdateMenuItemAsync(menuItemToUpdate.MenuItem);
}
}
catch (Exception ex)
{
errorMessage = "Some error occured: " + ex;
}
finally
{
MenuItemsSaved = true;
hasCollection = true;
errorMessage = "Menu saved";
}
}
void RowRender(RowRenderEventArgs<MenuItem> args)
{
if (!allowReorder) return;
args.Attributes.Add("title", "Drag row to reorder");
args.Attributes.Add("style", "cursor:grab");
args.Attributes.Add("draggable", "true");
args.Attributes.Add("ondragover", "event.preventDefault();event.target.closest('.rz-data-row').classList.add('my-class')");
args.Attributes.Add("ondragleave", "event.target.closest('.rz-data-row').classList.remove('my-class')");
args.Attributes.Add("ondragstart", EventCallback.Factory.Create<DragEventArgs>(this, () => draggedItem = args.Data));
args.Attributes.Add("ondrop", EventCallback.Factory.Create<DragEventArgs>(this, () =>
{
var draggedIndex = menuItems.IndexOf(draggedItem);
var droppedIndex = menuItems.IndexOf(args.Data);
var draggedModel = extractedMenuItems[draggedIndex];
menuItems.RemoveAt(draggedIndex);
extractedMenuItems.RemoveAt(draggedIndex);
menuItems.Insert(droppedIndex, draggedItem);
extractedMenuItems.Insert(droppedIndex, draggedModel);
for (int i = 0; i < menuItems.Count; i++)
{
menuItems[i].SortOrder = i;
extractedMenuItems[i].MenuItem.SortOrder = i;
}
JSRuntime.InvokeVoidAsync("eval", "document.querySelector('.my-class').classList.remove('my-class')");
}));
}
void ToggleReorder()
{
if (allowReorder) _ = SaveMenuItems(false);
allowReorder = !allowReorder;
}
void OnRowSelect(MenuItem item)
{
SelectedMenuItemModel = extractedMenuItems.FirstOrDefault(x => x.MenuItem.Id == item.Id) ?? new("");
}
}

View File

@ -9,8 +9,9 @@
@* <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;">
@{ @{
var brandFileName = _scopedContentService.SelectedDocument; var brandFileName = _scopedContentService.SelectedDocument;
var brandName = _scopedContentService.SelectedBrandName; var brandName = _scopedContentService.SelectedBrandName;
@ -23,14 +24,14 @@
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav me-auto mb-2 mb-lg-0">
@{ @{
if(MenuList != null) if (MenuList != null)
{ {
foreach(var menu in MenuList) foreach (var menu in MenuList)
{ {
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" aria-current="page" @onclick="()=>MenuClickedAsync(menu)">@menu</a> <a class="nav-link active" aria-current="page" @onclick="()=>MenuClickedAsync(menu)">@menu</a>
</li> </li>
} }
} }
} }
@ -44,7 +45,7 @@
<option value="">Lang</option> <option value="">Lang</option>
@foreach (var Language in Languages) @foreach (var Language in Languages)
{ {
<option value="@Language">@Language</option> <option value="@Language">@Language</option>
} }
</select> </select>
</div> </div>
@ -98,12 +99,12 @@
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
if(!string.IsNullOrEmpty(MenuString)) if (!string.IsNullOrEmpty(MenuString))
{ {
MenuList = MenuString.Split(","); MenuList = MenuString.Split(",");
} }
await base.OnParametersSetAsync(); await base.OnParametersSetAsync();
} }
public void OnLanguageSelected(ChangeEventArgs e) public void OnLanguageSelected(ChangeEventArgs e)
{ {

View File

@ -0,0 +1,167 @@
@using BLAIzor.Models
@using BLAIzor.Services
@inject ScopedContentService _scopedContentService;
@inject NavigationManager _navigationManager;
@inject ContentEditorService _contentEditorService
@inject IHttpContextAccessor HttpContextAccessor
@inject IJSRuntime JS
@inject ISimpleLogger _logger
<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="@SelectedLanguage">@SelectedLanguage</option>
@foreach (var Language in Languages)
{
<option value="@Language">@Language</option>
}
</select>
</div>
</div>
</nav>
@code {
[Parameter]
public int SiteId { get; set; }
[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()
{
}
protected override async Task OnParametersSetAsync()
{
if (Menu != null)
{
//
}
var lang = await JS.InvokeAsync<string>("getUserLanguage");
SiteInfo site = await _contentEditorService.GetSiteInfoByIdAsync(SiteId);
if (site.DefaultLanguage != null)
{
await _logger.InfoAsync($"NavMenu component: Setting default language to {site.DefaultLanguage} for site {site.Id}");
_scopedContentService.WebsiteDefaultLanguage = site.DefaultLanguage;
_scopedContentService.SelectedLanguage = site.DefaultLanguage;
SelectedLanguage = site.DefaultLanguage;
}
else
{
// 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";
await _logger.InfoAsync($"Setting default language to {SelectedLanguage} based on user language {lang}");
_scopedContentService.SelectedLanguage = SelectedLanguage;
}
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

@ -57,14 +57,14 @@
<p>Your sample html template for a specific block, like article, card, hero or such. Here you can use sample data in the snippet, to see if your css is working well</p> <p>Your sample html template for a specific block, like article, card, hero or such. Here you can use sample data in the snippet, to see if your css is working well</p>
<div class="form-group"> <div class="form-group">
<label>Sample</label><RadzenButton Click="@(() => ShowSnippetPreview(Snippet))">Preview snippet</RadzenButton> <label>Sample</label><RadzenButton Click="@(() => ShowSnippetPreview(Snippet))">Preview snippet</RadzenButton>
<RadzenHtmlEditor @bind-Value=@Snippet.SampleHtml style="height: 450px; color:#000; background-color: rgba(255,255,255,0) !important" Input=@OnInput Change=@OnChange Paste=@OnPaste UploadComplete=@OnUploadComplete Execute=@OnExecute UploadUrl="upload/image"> @* <RadzenHtmlEditor @bind-Value=@Snippet.SampleHtml style="height: 450px; color:#000; background-color: rgba(255,255,255,0) !important" Input=@OnInput Change=@OnChange Paste=@OnPaste UploadComplete=@OnUploadComplete Execute=@OnExecute UploadUrl="upload/image">
<RadzenHtmlEditorUndo /> <RadzenHtmlEditorUndo />
<RadzenHtmlEditorRedo /> <RadzenHtmlEditorRedo />
<RadzenHtmlEditorSource /> <RadzenHtmlEditorSource />
</RadzenHtmlEditor> </RadzenHtmlEditor> *@
</div> </div>
</div> </div>
<EventConsole @ref=@console /> @* <EventConsole @ref=@console /> *@
@if (IsLoading) @if (IsLoading)
{ {
<p>Loading content...</p> <p>Loading content...</p>
@ -83,7 +83,7 @@
private string? userName; private string? userName;
private AuthenticationState? authState; private AuthenticationState? authState;
EventConsole console; // EventConsole console;
public async void ShowSnippetPreview(HtmlSnippet snippet) public async void ShowSnippetPreview(HtmlSnippet snippet)
{ {
@ -117,30 +117,30 @@
async Task OnPaste(HtmlEditorPasteEventArgs args) async Task OnPaste(HtmlEditorPasteEventArgs args)
{ {
console.Log($"Paste: {args.Html}"); // console.Log($"Paste: {args.Html}");
// await OnContentUpdated.InvokeAsync(MenuItem); // await OnContentUpdated.InvokeAsync(MenuItem);
} }
async Task OnChange(string html) async Task OnChange(string html)
{ {
console.Log($"Change: {html}"); // console.Log($"Change: {html}");
} }
async Task OnInput(string html) async Task OnInput(string html)
{ {
console.Log($"Input: {html}"); // console.Log($"Input: {html}");
} }
void OnExecute(HtmlEditorExecuteEventArgs args) void OnExecute(HtmlEditorExecuteEventArgs args)
{ {
console.Log($"Execute: {args.CommandName}"); // console.Log($"Execute: {args.CommandName}");
} }
void OnUploadComplete(UploadCompleteEventArgs args) void OnUploadComplete(UploadCompleteEventArgs args)
{ {
console.Log($"Upload complete: {args.RawResponse}"); // console.Log($"Upload complete: {args.RawResponse}");
} }
} }

View File

@ -1,11 +1,12 @@
<style> @using BLAIzor.Models
<style>
.parent-element-to-video { .parent-element-to-video {
overflow: hidden; overflow: hidden;
width: 120vw; width: 120vw;
height: auto; height: auto;
} }
video { #myVideo {
position: fixed; position: fixed;
right: 0; right: 0;
top: 0; top: 0;
@ -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;
}
}

269
Helpers/TextHelper.cs Normal file
View File

@ -0,0 +1,269 @@
using System;
using System.Text.RegularExpressions;
using System.Text;
using System.Collections.Generic;
public static class TextHelper
{
// Special character replacement map
private static readonly Dictionary<string, string> HungarianSpecialCharacterMap = new()
{
{ "/", " per " },
{ "@", " kukac " },
{ "#", " kettőskereszt " },
{ "&", " és " },
//{ ",", " vessző " },
{ " = ", " egyenlő " }, // Example, you can add more
//{ " - ", " mínusz " } // Example, you can add more
};
private static readonly Dictionary<string, string> EnglishSpecialCharacterMap = new()
{
{ "/", " slash " },
{ "@", " at " },
{ "#", " hashtag " },
{ "&", " and " },
//{ ",", " vessző " },
{ " = ", " equals " }, // Example, you can add more
//{ " - ", " mínusz " } // Example, you can add more
};
public static string ReplaceNumbersAndSpecialCharacters(string text, string language)
{
// Save parts that should be skipped (emails, URLs, dates)
var protectedParts = new Dictionary<string, string>();
// Protect dates like 2024.05.06
text = Regex.Replace(text, @"\b\d{4}\.\d{2}\.\d{2}\b", match =>
{
string key = $"__DATE__{protectedParts.Count}__";
protectedParts[key] = match.Value;
return key;
});
// Remove anything between [] including the brackets themselves
text = Regex.Replace(text, @"\[[^\]]*\]", "");
// First replace floats (keep this BEFORE integers)
text = Regex.Replace(text, @"\b\d+\.\d+\b", match =>
{
var parts = match.Value.Split('.');
var integerPart = int.Parse(parts[0]);
var decimalPart = int.Parse(parts[1]);
if(language == "Hungarian")
{
return $"{NumberToHungarian(integerPart)} egész {NumberToHungarian(decimalPart)} {(parts[1].Length == 1 ? "tized" : parts[1].Length == 2 ? "század" : "ezred")}";
}
else
{
return $"{NumberToEnglish(integerPart)} point {NumberToEnglish(decimalPart)}";
}
});
// Then replace integers
text = Regex.Replace(text, @"\b\d+\b", match =>
{
int number = int.Parse(match.Value);
if(language == "Hungarian")
{
return NumberToHungarian(number);
}
else
{
return NumberToEnglish(number);
}
});
// Replace special characters from dictionary
if(language == "Hungarian")
{
foreach (var kvp in HungarianSpecialCharacterMap)
{
text = text.Replace(kvp.Key, kvp.Value);
}
}
else
{
foreach (var kvp in EnglishSpecialCharacterMap)
{
text = text.Replace(kvp.Key, kvp.Value);
}
}
// Replace dots surrounded by spaces (optional)
//text = Regex.Replace(text, @" (?=\.)|(?<=\.) ", " pont ");
// Restore protected parts
foreach (var kvp in protectedParts)
{
text = text.Replace(kvp.Key, kvp.Value);
}
return text;
}
public static string NumberToHungarian(int number)
{
if (number == 0) return "nulla";
string[] units = { "", "egy", "két", "három", "négy", "öt", "hat", "hét", "nyolc", "kilenc" };
string[] tens = { "", "tíz", "húsz", "harminc", "negyven", "ötven", "hatvan", "hetven", "nyolcvan", "kilencven" };
string[] tensAlternate = { "", "tizen", "huszon", "harminc", "negyven", "ötven", "hatvan", "hetven", "nyolcvan", "kilencven" };
StringBuilder result = new StringBuilder();
if (number >= 1000)
{
int thousands = number / 1000;
if (thousands == 1)
result.Append("ezer");
else
{
result.Append(NumberToHungarian(thousands));
result.Append("ezer");
}
number %= 1000;
}
if (number >= 100)
{
int hundreds = number / 100;
if (hundreds == 1)
result.Append("száz");
else
{
result.Append(NumberToHungarian(hundreds));
result.Append("száz");
}
number %= 100;
}
if (number >= 10)
{
int tensPart = number / 10;
result.Append(tensAlternate[tensPart]);
number %= 10;
}
if (number > 0)
{
// "két" instead of "kettő" in compound numbers
if (number == 2 && result.Length > 0)
result.Append("két");
else
result.Append(units[number]);
}
return result.ToString();
}
public static string NumberToEnglish(int number)
{
if (number == 0) return "zero";
string[] units = { "", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" };
string[] tens = { "", "ten", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninty" };
StringBuilder result = new StringBuilder();
if (number >= 1000)
{
int thousands = number / 1000;
if (thousands == 1)
result.Append("thousand");
else
{
result.Append(NumberToHungarian(thousands));
result.Append("thousand");
}
number %= 1000;
}
if (number >= 100)
{
int hundreds = number / 100;
if (hundreds == 1)
result.Append("hundred");
else
{
result.Append(NumberToHungarian(hundreds));
result.Append("hundred");
}
number %= 100;
}
if (number >= 10)
{
//int tensPart = number / 10;
//result.Append(tens[tensPart]);
//number %= 10;
switch (number)
{
case 10:
result.Append("ten");
break;
case 11:
result.Append("eleven");
break;
case 12:
result.Append("twelve");
break;
case 13:
result.Append("thirteen");
break;
case 14:
result.Append("fourteen");
break;
case 15:
result.Append("fifteen");
break;
case 16:
result.Append("sixteen");
break;
case 17:
result.Append("seventeen");
break;
case 18:
result.Append("eighteen");
break;
case 19:
result.Append("nineteen");
break;
}
}
return result.ToString();
}
public static string FixJsonWithoutAI(string aiResponse)
{
if (aiResponse.StartsWith("```"))
{
//Console.WriteLine("FIXING ``` in AI Response.");
aiResponse = aiResponse.Substring(3);
if (aiResponse.StartsWith("json"))
{
aiResponse = aiResponse.Substring(4);
}
if (aiResponse.StartsWith("html"))
{
aiResponse = aiResponse.Substring(4);
}
aiResponse = aiResponse.Substring(0, aiResponse.Length - 3);
}
return aiResponse;
}
public static string RemoveTabs(string text)
{
if (string.IsNullOrEmpty(text)) return text;
return text.Replace("\t", ""); // Simple replace — remove all tab characters
}
}

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,7 @@
namespace BLAIzor.Interfaces
{
public interface IBrightDataService
{
Task<string?> ScrapeFacebookPostsAsync(string pageUrl, int numPosts = 10);
}
}

View File

@ -0,0 +1,637 @@
// <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("20250602055410_addVoiceInformation")]
partial class addVoiceInformation
{
/// <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.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<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("PointId")
.HasColumnType("int");
b.Property<Guid?>("QdrantPointId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("ShowInMainMenu")
.HasColumnType("bit");
b.Property<int>("SiteInfoId")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<string>("StoredHtml")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
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>("BrandLogoUrl")
.HasColumnType("nvarchar(max)");
b.Property<string>("DefaultColor")
.HasColumnType("nvarchar(max)");
b.Property<string>("DefaultUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("DomainUrl")
.IsRequired()
.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.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.SiteInfo", "SiteInfo")
.WithMany("MenuItems")
.HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
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.DesignTemplate", b =>
{
b.Navigation("CssTemplate")
.IsRequired();
b.Navigation("Sites");
});
modelBuilder.Entity("BLAIzor.Models.SiteInfo", b =>
{
b.Navigation("FormDefinitions");
b.Navigation("MenuItems");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,55 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BLAIzor.Migrations
{
/// <inheritdoc />
public partial class addVoiceInformation : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Entity",
table: "SiteInfos",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Persona",
table: "SiteInfos",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "voiceId",
table: "SiteInfos",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.UpdateData(
table: "SiteInfos",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "Entity", "Persona", "voiceId" },
values: new object[] { null, null, null });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Entity",
table: "SiteInfos");
migrationBuilder.DropColumn(
name: "Persona",
table: "SiteInfos");
migrationBuilder.DropColumn(
name: "voiceId",
table: "SiteInfos");
}
}
}

View File

@ -0,0 +1,645 @@
// <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("20250615203806_EmbeddingServiceAndDefaultLanguage")]
partial class EmbeddingServiceAndDefaultLanguage
{
/// <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.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<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("PointId")
.HasColumnType("int");
b.Property<Guid?>("QdrantPointId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("ShowInMainMenu")
.HasColumnType("bit");
b.Property<int>("SiteInfoId")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<string>("StoredHtml")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
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.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.SiteInfo", "SiteInfo")
.WithMany("MenuItems")
.HasForeignKey("SiteInfoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
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.DesignTemplate", b =>
{
b.Navigation("CssTemplate")
.IsRequired();
b.Navigation("Sites");
});
modelBuilder.Entity("BLAIzor.Models.SiteInfo", b =>
{
b.Navigation("FormDefinitions");
b.Navigation("MenuItems");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,83 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BLAIzor.Migrations
{
/// <inheritdoc />
public partial class EmbeddingServiceAndDefaultLanguage : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "voiceId",
table: "SiteInfos",
newName: "VoiceId");
migrationBuilder.AddColumn<string>(
name: "BackgroundVideo",
table: "SiteInfos",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "DefaultLanguage",
table: "SiteInfos",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "EmbeddingService",
table: "SiteInfos",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AlterColumn<string>(
name: "StoredHtml",
table: "MenuItems",
type: "nvarchar(max)",
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(max)");
migrationBuilder.UpdateData(
table: "SiteInfos",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "BackgroundVideo", "DefaultLanguage", "EmbeddingService" },
values: new object[] { null, null, null });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "BackgroundVideo",
table: "SiteInfos");
migrationBuilder.DropColumn(
name: "DefaultLanguage",
table: "SiteInfos");
migrationBuilder.DropColumn(
name: "EmbeddingService",
table: "SiteInfos");
migrationBuilder.RenameColumn(
name: "VoiceId",
table: "SiteInfos",
newName: "voiceId");
migrationBuilder.AlterColumn<string>(
name: "StoredHtml",
table: "MenuItems",
type: "nvarchar(max)",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "nvarchar(max)",
oldNullable: 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("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

@ -0,0 +1,884 @@
// <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("20250717145336_domainurl_nullable")]
partial class domainurl_nullable
{
/// <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")
.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,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BLAIzor.Migrations
{
/// <inheritdoc />
public partial class domainurl_nullable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "DomainUrl",
table: "SiteInfos",
type: "nvarchar(max)",
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(max)");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "DomainUrl",
table: "SiteInfos",
type: "nvarchar(max)",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "nvarchar(max)",
oldNullable: true);
}
}
}

View File

@ -0,0 +1,893 @@
// <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("20250814214748_social_links")]
partial class social_links
{
/// <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")
.HasColumnType("nvarchar(max)");
b.Property<string>("EmbeddingService")
.HasColumnType("nvarchar(max)");
b.Property<string>("Entity")
.HasColumnType("nvarchar(max)");
b.Property<string>("FacebookUrl")
.HasColumnType("nvarchar(max)");
b.Property<string>("InstagramUrl")
.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>("TwitterUrl")
.HasColumnType("nvarchar(max)");
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,55 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BLAIzor.Migrations
{
/// <inheritdoc />
public partial class social_links : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "FacebookUrl",
table: "SiteInfos",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "InstagramUrl",
table: "SiteInfos",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "TwitterUrl",
table: "SiteInfos",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.UpdateData(
table: "SiteInfos",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "FacebookUrl", "InstagramUrl", "TwitterUrl" },
values: new object[] { null, null, null });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "FacebookUrl",
table: "SiteInfos");
migrationBuilder.DropColumn(
name: "InstagramUrl",
table: "SiteInfos");
migrationBuilder.DropColumn(
name: "TwitterUrl",
table: "SiteInfos");
}
}
}

View File

@ -0,0 +1,896 @@
// <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("20250816221528_siteinfo_designstyle")]
partial class siteinfo_designstyle
{
/// <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>("DesignStyle")
.HasColumnType("nvarchar(max)");
b.Property<string>("DomainUrl")
.HasColumnType("nvarchar(max)");
b.Property<string>("EmbeddingService")
.HasColumnType("nvarchar(max)");
b.Property<string>("Entity")
.HasColumnType("nvarchar(max)");
b.Property<string>("FacebookUrl")
.HasColumnType("nvarchar(max)");
b.Property<string>("InstagramUrl")
.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>("TwitterUrl")
.HasColumnType("nvarchar(max)");
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 siteinfo_designstyle : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "DesignStyle",
table: "SiteInfos",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.UpdateData(
table: "SiteInfos",
keyColumn: "Id",
keyValue: 1,
column: "DesignStyle",
value: null);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DesignStyle",
table: "SiteInfos");
}
}
}

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,31 +327,42 @@ 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");
b.Property<string>("StoredHtml") b.Property<string>("StoredHtml")
.IsRequired()
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
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");
@ -211,23 +376,46 @@ namespace BLAIzor.Migrations
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("BackgroundVideo")
.HasColumnType("nvarchar(max)");
b.Property<string>("BrandLogoUrl") b.Property<string>("BrandLogoUrl")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<string>("DefaultColor") b.Property<string>("DefaultColor")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<string>("DefaultLanguage")
.HasColumnType("nvarchar(max)");
b.Property<string>("DefaultUrl") b.Property<string>("DefaultUrl")
.IsRequired() .IsRequired()
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<string>("DesignStyle")
.HasColumnType("nvarchar(max)");
b.Property<string>("DomainUrl") b.Property<string>("DomainUrl")
.IsRequired() .HasColumnType("nvarchar(max)");
b.Property<string>("EmbeddingService")
.HasColumnType("nvarchar(max)");
b.Property<string>("Entity")
.HasColumnType("nvarchar(max)");
b.Property<string>("FacebookUrl")
.HasColumnType("nvarchar(max)");
b.Property<string>("InstagramUrl")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<bool>("IsPublished") b.Property<bool>("IsPublished")
.HasColumnType("bit"); .HasColumnType("bit");
b.Property<string>("Persona")
.HasColumnType("nvarchar(max)");
b.Property<bool>("STTActive") b.Property<bool>("STTActive")
.HasColumnType("bit"); .HasColumnType("bit");
@ -243,10 +431,19 @@ namespace BLAIzor.Migrations
b.Property<int?>("TemplateId") b.Property<int?>("TemplateId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("TwitterUrl")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId") b.Property<string>("UserId")
.IsRequired() .IsRequired()
.HasColumnType("nvarchar(450)"); .HasColumnType("nvarchar(450)");
b.Property<string>("VectorCollectionName")
.HasColumnType("nvarchar(max)");
b.Property<string>("VoiceId")
.HasColumnType("nvarchar(max)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("TemplateId"); b.HasIndex("TemplateId");
@ -492,6 +689,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")
@ -527,12 +757,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");
}); });
@ -605,6 +856,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")
@ -613,8 +874,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");

358
Models/AiPrompts.cs Normal file
View File

@ -0,0 +1,358 @@
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 use the same text multiple times**" +
"- 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 put in blocks \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 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" +
"- DO **NOT** merge different content blocks.\n" +
"- DO **NOT** remove javascript or <script> tags" +
"- DO **NOT** wrap the full output in a single `div`.\n" +
"- DO **NOT** skip any provided content.\n" +
"- DO **NOT** add assumed text (e.g. prices, links).\n" +
"- DO **NOT** add `<img>` if no image URL exists.\n" +
"- DO **NOT** modify image URLs. not even adding 'https://example.com or such before the relaitve path'\n" +
"- DO **NOT** 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; }
}
}

View File

@ -14,6 +14,10 @@ namespace BLAIzor.Models
public Message[] Messages { get; set; } = Array.Empty<Message>(); public Message[] Messages { get; set; } = Array.Empty<Message>();
[JsonProperty("stream")] [JsonProperty("stream")]
public bool Stream { get; set; } = true; public bool Stream { get; set; } = true;
//[JsonProperty("reasoning_effort")]
//public string ReasoningEffort { get; set; } = "minimal";
//[JsonProperty("verbosity")]
//public string Verbosity { get; set; } = "high";
} }
} }

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

@ -0,0 +1,7 @@
namespace BLAIzor.Models
{
public class GeneratedContentItem : ContentItem
{
public List<PhotoSlot> PhotoSlots { get; set; } = new();
}
}

View File

@ -18,7 +18,10 @@
public string Tags { get; set; } public string Tags { get; set; }
public string Type { get; set; } // e.g. "article", "form", "gallery" public string Type { get; set; } // e.g. "article", "form", "gallery"
public string? Variant { get; set; } // 👈 NEW: e.g. "image-left", "image-right", "no-image" public string? Variant { get; set; } // 👈 NEW: e.g. "image-left", "image-right", "no-image"
public string SampleHtml { get; set; } public string SampleHtml { get; set; } //for design purposes
public List<string> Slots { get; set; } // e.g. ["title", "subtitle", "image", "cta"]
public float[] Vectors { get; set; }
} }
} }

28
Models/LayoutBlock.cs Normal file
View File

@ -0,0 +1,28 @@
namespace BLAIzor.Models
{
public class LayoutBlock
{
public string Type { get; set; } // e.g. "hero", "article", "gallery", "testimonial"
public string? Variant { get; set; } // e.g. "image-left", "image-right", "no-image"
public List<string> RequiredSlots { get; set; } // e.g. ["title", "text", "image", "cta"]
public Dictionary<string, string> ContentMap { get; set; }
// Maps slot names to content IDs or keys, e.g. { "title": "main_heading", "image": "img_1" }
public int? PreferredSnippetId { get; set; }
// Optional: used if a perfect match snippet is found or AI prefers one
//public string? BootstrapStructure { get; set; }
// Optional: e.g. "row-cols-2", "container-fluid py-5"
public string RawContent { get; set; } = string.Empty;
public int Order { get; set; }
// The position of the block in the layout
public string? Notes { get; set; }
// Optional field for AI hints, comments, or debug info
}
}

8
Models/LayoutPlan.cs Normal file
View File

@ -0,0 +1,8 @@
namespace BLAIzor.Models
{
public class LayoutPlan
{
public string Title { get; set; }
public List<LayoutBlock> Blocks { get; set; } = new();
}
}

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

@ -7,6 +7,7 @@ namespace BLAIzor.Models
{ {
public MenuItem MenuItem { get; set; } public MenuItem MenuItem { get; set; }
public string Content { get; set; } = string.Empty; public string Content { get; set; } = string.Empty;
public string ContentDescription { get; set; } = string.Empty;
public MenuItemModel(string name) public MenuItemModel(string name)
{ {

9
Models/PhotoSlot.cs Normal file
View File

@ -0,0 +1,9 @@
namespace BLAIzor.Models
{
public class PhotoSlot
{
public string Description { get; set; } // AI-generated suggestion
public string ImageUrl { get; set; } // AI-generated or user-selected image
public int SortOrder { get; set; } // order in which it appears in the UI
}
}

View File

@ -19,14 +19,27 @@ namespace BLAIzor.Models
public string UserId { get; set; } public string UserId { get; set; }
[Url(ErrorMessage = "The DomainUrl field must be a valid URL.")] [Url(ErrorMessage = "The DomainUrl field must be a valid URL.")]
public string DomainUrl { get; set; } // For custom domains public string? DomainUrl { get; set; } // For custom domains
[Url(ErrorMessage = "The DefaultUrl field must be a valid URL.")] [Url(ErrorMessage = "The DefaultUrl field must be a valid URL.")]
public string DefaultUrl { get; set; } // For generated subdomains public string DefaultUrl { get; set; } // For generated subdomains
public bool IsPublished { get; set; } // For generated subdomains public bool IsPublished { get; set; }
public int? TemplateId { get; set; } public int? TemplateId { get; set; }
public bool TTSActive { get; set; } = false; public bool TTSActive { get; set; } = false;
public bool STTActive { get; set; } = false; public bool STTActive { get; set; } = false;
public string? VoiceId { get; set; }
public string? Persona { get; set; }
public string? Entity { get; set; }
public string? DesignStyle { get; set; } // e.g. "Modern", "Classic", "Photorealistic"
public string? DefaultLanguage { get; set; }
public string? BackgroundVideo { get; set; }
public string? VectorCollectionName { get; set; }
public string? EmbeddingService { get; set; }
public string? FacebookUrl { get; set; }
public string? TwitterUrl { get; set; }
public string? InstagramUrl { 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,17 +1,37 @@
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; }
public string Name { get; set; } public string Name { get; set; }
public string Description { get; set; } public string Description { get; set; }
public string Content { get; set; } public string Content { 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

@ -1,6 +1,8 @@
using BLAIzor.Components; using BLAIzor.Components;
using BLAIzor.Components.Partials;
using BLAIzor.Data; using BLAIzor.Data;
using BLAIzor.Helpers; using BLAIzor.Helpers;
using BLAIzor.Interfaces;
using BLAIzor.Services; using BLAIzor.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
@ -12,6 +14,8 @@ using Radzen;
using Sidio.Sitemap.AspNetCore; using Sidio.Sitemap.AspNetCore;
using Sidio.Sitemap.Blazor; using Sidio.Sitemap.Blazor;
using Sidio.Sitemap.Core.Services; using Sidio.Sitemap.Core.Services;
using System;
using System.Net.Http.Headers;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration; var configuration = builder.Configuration;
@ -35,6 +39,12 @@ builder.Services.Configure<KestrelServerOptions>(options =>
options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; options.Limits.MaxRequestBodySize = 50 * 1024 * 1024;
}); });
var env = builder.Environment; // This is IWebHostEnvironment
if (env.IsProduction())
{
// do production-specific setup
}
// Add services to the container. // Add services to the container.
builder.Services.AddRazorComponents() builder.Services.AddRazorComponents()
@ -42,12 +52,24 @@ builder.Services.AddRazorComponents()
builder.Services.AddRadzenComponents(); builder.Services.AddRadzenComponents();
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
// Required for scoped injection (used in pages/services)
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
// Required for factory-based logging or background usage
builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString), ServiceLifetime.Scoped);
builder.Services.AddHttpClient(); builder.Services.AddHttpClient();
builder.Services.AddScoped<ISimpleLogger, SimpleLogger>();
builder.Services.AddScoped<AIService>(); builder.Services.AddScoped<AIService>();
builder.Services.AddSingleton<ContentService>(); //builder.Services.AddSingleton<ContentService>();
builder.Services.AddScoped<ScopedContentService>(); builder.Services.AddScoped<ScopedContentService>();
builder.Services.AddScoped<QDrantService>(); builder.Services.AddScoped<QDrantService>();
builder.Services.AddScoped<OpenAIEmbeddingService>(); builder.Services.AddScoped<OpenAIEmbeddingService>();
builder.Services.AddScoped<LocalEmbeddingService>();
builder.Services.AddScoped<HtmlSnippetProcessor>(); builder.Services.AddScoped<HtmlSnippetProcessor>();
builder.Services builder.Services
.AddHttpContextAccessor() .AddHttpContextAccessor()
@ -57,14 +79,32 @@ builder.Services.AddScoped<CustomAuthenticationStateProvider>();
builder.Services.Configure<SmtpSettings>(builder.Configuration.GetSection("Smtp")); builder.Services.Configure<SmtpSettings>(builder.Configuration.GetSection("Smtp"));
builder.Services.AddTransient<IEmailSender, EmailService>(); builder.Services.AddTransient<IEmailSender, EmailService>();
builder.Services.AddScoped<ContentEditorService>(); builder.Services.AddScoped<ContentEditorService>();
builder.Services.AddScoped<ContentEditorAIService>();
builder.Services.AddScoped<CssTemplateService>(); builder.Services.AddScoped<CssTemplateService>();
builder.Services.AddScoped<DesignTemplateService>(); builder.Services.AddScoped<DesignTemplateService>();
builder.Services.AddScoped<OpenAIApiService>(); builder.Services.AddScoped<OpenAIApiService>();
builder.Services.AddScoped<DeepSeekApiService>(); builder.Services.AddScoped<DeepSeekApiService>();
builder.Services.AddScoped<OpenAiRealtimeService>(); builder.Services.AddScoped<OpenAiRealtimeService>();
builder.Services.AddScoped<CerebrasAPIService>(); builder.Services.AddScoped<CerebrasAPIService>();
builder.Services.AddScoped<KimiApiService>();
builder.Services.AddScoped<CssInjectorService>(); builder.Services.AddScoped<CssInjectorService>();
builder.Services.AddScoped<LocalVectorSearchService>();
builder.Services.AddScoped<WebsiteContentLoaderService>();
builder.Services.AddScoped<CacheService>();
builder.Services.AddSingleton<CreateSiteWizard>();
builder.Services.AddScoped<WhisperTranscriptionService>();
builder.Services.AddScoped<IBrightDataService, BrightDataService>();
builder.Services.AddHttpClient<ReplicateService>(client =>
{
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "r8_MUApXYIE5mRjxqy20tsGLehWBJkCzNj0Cwvrh");
});
builder.Services.AddHostedService<TempFileCleanupService>(); builder.Services.AddHostedService<TempFileCleanupService>();
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 it is too large Load Diff

View File

@ -0,0 +1,127 @@
using BLAIzor.Interfaces;
using System.Net.Http;
using System.Text;
using System.Text.Json;
namespace BLAIzor.Services
{
public class BrightDataService : IBrightDataService
{
private readonly ISimpleLogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
private string _apiToken;
public static IConfiguration? _configuration;
public BrightDataService(ISimpleLogger logger, IHttpClientFactory httpClientFactory, IConfiguration configuration)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
_configuration = configuration;
}
private string GetScraperSettings() =>
_configuration?.GetSection("ScraperSettings")?.GetValue<string>("Provider") ?? string.Empty;
public string GetApiKey()
{
if (_configuration == null)
{
return string.Empty;
}
if (_configuration.GetSection("ScraperSettings") == null)
{
return string.Empty;
}
return _configuration.GetSection("ScraperSettings").GetValue<string>("ApiKey")!;
}
public async Task<string?> ScrapeFacebookPostsAsync(string pageUrl, int numPosts = 10)
{
if (string.IsNullOrWhiteSpace(pageUrl))
return null;
_apiToken = GetApiKey();
try
{
var client = _httpClientFactory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiToken}");
var url = "https://api.brightdata.com/datasets/v3/trigger?dataset_id=gd_lkaxegm826bjpoo9m5&include_errors=true&limit_multiple_results=20";
var payload = new[]
{
new
{
url = pageUrl,
num_of_posts = numPosts,
posts_to_not_include = Array.Empty<string>(),
start_date = "",
end_date = ""
}
};
var json = JsonSerializer.Serialize(payload);
var response = await client.PostAsync(url, new StringContent(json, Encoding.UTF8, "application/json"));
response.EnsureSuccessStatusCode();
var scrapeId = await response.Content.ReadAsStringAsync();
//"snapshot_id
if (string.IsNullOrWhiteSpace(scrapeId))
{
await _logger.ErrorAsync("Failed to initiate scraping for Facebook posts.");
return null;
}
else
{
//have to keep checking:
//result: "{\"snapshot_id\":\"s_mec12qv422avgbv9jl\"}"
//let's extract the scrapeId from the response
scrapeId = scrapeId.Trim('"').Split(':')[1].Trim('"');
//remove all other characters
scrapeId = scrapeId.Replace("{", "").Replace("}", "").Replace("\"", "");
var checkUrl = $"https://api.brightdata.com/datasets/v3/progress/{scrapeId}";
var statusResponse = await client.GetAsync(checkUrl);
var responseString = await statusResponse.Content.ReadAsStringAsync();
int attempt = 0;
//make a cycle
while (responseString.Contains("status") && !responseString.Contains("ready"))
{
if (attempt >= 60)
{
await _logger.ErrorAsync($"Failed to get scraping status for Facebook page: {pageUrl} after multiple attempts.");
return null;
}
// Wait for a while before retrying
await Task.Delay(5000); // Wait for 5 seconds
statusResponse = await client.GetAsync(checkUrl);
responseString = await statusResponse.Content.ReadAsStringAsync();
attempt++;
}
// Now fetch the snapshot
var snapshotUrl = $"https://api.brightdata.com/datasets/v3/snapshot/{scrapeId}?format=json";
var snapshotResponse = await client.GetAsync(snapshotUrl);
var snapshotString = await snapshotResponse.Content.ReadAsStringAsync();
return snapshotString;
}
}
catch (Exception ex)
{
Console.WriteLine($"Error scraping Facebook: {ex.Message}");
return null;
}
}
}
}

43
Services/CacheService.cs Normal file
View File

@ -0,0 +1,43 @@
using BLAIzor.Models;
namespace BLAIzor.Services
{
public class CacheService
{
private readonly WebsiteContentLoaderService _websiteContentLoaderService;
private readonly ISimpleLogger _logger;
private readonly ScopedContentService _scopedContentService;
private readonly ContentEditorService _contentEditorService;
private readonly QDrantService _qDrantService;
public CacheService(
WebsiteContentLoaderService websiteContentLoaderService,
ISimpleLogger logger,
ScopedContentService scopedContentService,
ContentEditorService contentEditorService,
QDrantService qDrantService
)
{
_websiteContentLoaderService = websiteContentLoaderService;
_logger = logger;
_scopedContentService = scopedContentService;
_contentEditorService = contentEditorService;
_qDrantService = qDrantService;
}
public async Task<WebsiteContentModel> UpdateContentCache(string sessionId, int siteId)
{
await _logger.InfoAsync($"UpdateCache: method called", $"sessionId: {sessionId}, siteId: {siteId}");
SiteInfo site = await _contentEditorService.GetSiteInfoByIdAsync(siteId);
WebsiteContentModel siteModel = null;
siteModel = await _websiteContentLoaderService.LoadAllAsync(
site,
_qDrantService.GetPointsFromQdrantAsyncByPointIds
);
_scopedContentService.WebsiteContentModel = siteModel;
return siteModel;
}
}
}

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,308 @@
using BLAIzor.Data;
using BLAIzor.Models;
using Microsoft.DotNet.Scaffolding.Shared;
using Microsoft.EntityFrameworkCore;
using Microsoft.Identity.Client;
namespace BLAIzor.Services
{
public class ContentEditorAIService
{
//private readonly AIService _aiService;
private readonly OpenAIApiService _openAIApiService;
private readonly OpenAiRealtimeService _openAIRealtimeService;
private readonly DeepSeekApiService _deepSeekApiService;
private readonly CerebrasAPIService _cerebrasAPIService;
private readonly KimiApiService _kimiApiService;
//private readonly ApplicationDbContext _context;
private readonly QDrantService _qDrantService;
private readonly HtmlSnippetProcessor _htmlSnippetProcessor;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ScopedContentService _scopedContentService;
private readonly ContentEditorService _contentEditorService;
private readonly ISimpleLogger _logger;
public static IConfiguration? _configuration;
public bool UseWebsocket = false;
private string AiProvider = "";
public ContentEditorAIService(
OpenAIApiService openAIApiService,
DeepSeekApiService deepSeekApiService,
OpenAiRealtimeService openAIRealtimeService,
CerebrasAPIService cerebrasAPIService,
KimiApiService kimiApiService,
/*ApplicationDbContext context,*/
QDrantService qDrantService,
HtmlSnippetProcessor htmlSnippetProcessor,
IServiceScopeFactory serviceScopeFactory,
ScopedContentService scopedContentService,
ISimpleLogger logger,
IConfiguration? configuration,
ContentEditorService contentEditorService)
{
//_aiService = aiService;
_openAIApiService = openAIApiService;
_deepSeekApiService = deepSeekApiService;
_openAIRealtimeService = openAIRealtimeService;
_cerebrasAPIService = cerebrasAPIService;
_kimiApiService = kimiApiService;
//_context = context;
_qDrantService = qDrantService;
_htmlSnippetProcessor = htmlSnippetProcessor;
_serviceScopeFactory = serviceScopeFactory;
_scopedContentService = scopedContentService;
_logger = logger;
_configuration = configuration;
_contentEditorService = contentEditorService;
}
private string GetAiEmbeddingSettings() =>
_configuration?.GetSection("AiSettings")?.GetValue<string>("EmbeddingService") ?? string.Empty;
private string GetAiSettings() =>
_configuration?.GetSection("AiSettings")?.GetValue<string>("Provider") ?? string.Empty;
// Existing methods
public async Task<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>();
return result;
}
public async Task<string> GetFacebookContentAsync(string sessionId, string prompt)
{
string systemMessage = "You are a helpful assistant that helps the user in creating a website content from facebook posts.";
var result = await _openAIApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, prompt);
await _logger.InfoAsync("Facebook content: " + result);
//return result.Split('\n', StringSplitOptions.RemoveEmptyEntries)
// .Select(line => line.Trim('-').Trim())
// .ToList() ?? new List<string>();
return result;
}
public async Task<string> GetGeneratedContentAsync(string sessionId, string prompt)
{
string systemMessage = "You are a helpful assistant that helps the user plan the content of a website. Do not generate html, just plain text.";
//var result = await _openAIApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, prompt);
string result = "";
AiProvider = GetAiSettings();
if (AiProvider == "cerebras")
{
result = await _cerebrasAPIService.GetSimpleCerebrasResponse(sessionId, systemMessage, prompt);
}
else if (AiProvider == "chatgpt")
{
result = await _openAIApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, prompt);
}
else if (AiProvider == "deepseek")
{
//await _deepSeekApiService.GetChatGPTStreamedResponse(systemMessage, userMessage);
result = await _deepSeekApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, prompt);
}
if (AiProvider == "kimi")
{
result = await _kimiApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, prompt);
}
return result;
}
public async Task<string> GetPhotoPromptAsync(string sessionId, string prompt)
{
string systemMessage = "You are a helpful assistant that writes image prompts. Respond only with the image prompt, no explanation, or information added.";
//var result = await _openAIApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, prompt);
string result = "";
AiProvider = GetAiSettings();
if (AiProvider == "cerebras")
{
result = await _cerebrasAPIService.GetSimpleCerebrasResponse(sessionId, systemMessage, prompt);
}
else if (AiProvider == "chatgpt")
{
result = await _openAIApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, prompt);
}
else if (AiProvider == "deepseek")
{
//await _deepSeekApiService.GetChatGPTStreamedResponse(systemMessage, userMessage);
result = await _deepSeekApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, prompt);
}
if (AiProvider == "kimi")
{
result = await _kimiApiService.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

@ -1,6 +1,7 @@
 
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics;
using System.Threading.Tasks; using System.Threading.Tasks;
using BLAIzor.Models; using BLAIzor.Models;
using DocumentFormat.OpenXml.Office2010.Excel; using DocumentFormat.OpenXml.Office2010.Excel;
@ -16,16 +17,18 @@ namespace BLAIzor.Services
public class HtmlSnippetProcessor public class HtmlSnippetProcessor
{ {
private readonly OpenAIEmbeddingService _embeddingService; private readonly OpenAIEmbeddingService _openAIEmbeddingService;
private readonly LocalEmbeddingService _localEmbeddingService;
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private readonly QDrantService _drantService; private readonly QDrantService _drantService;
public static IConfiguration? _configuration; public static IConfiguration? _configuration;
private string _qdrantApiKey; private string _qdrantApiKey;
public HtmlSnippetProcessor(QDrantService drantService, IConfiguration? configuration) public HtmlSnippetProcessor(QDrantService drantService, OpenAIEmbeddingService openAIEmbeddingService, LocalEmbeddingService localEmbeddingService, IConfiguration? configuration)
{ {
_drantService = drantService; _drantService = drantService;
_embeddingService = new OpenAIEmbeddingService(); _openAIEmbeddingService = openAIEmbeddingService;
_localEmbeddingService = localEmbeddingService;
_httpClient = new HttpClient(); _httpClient = new HttpClient();
_configuration = configuration; _configuration = configuration;
} }
@ -35,6 +38,9 @@ namespace BLAIzor.Services
return _configuration?.GetSection("QDrant")?.GetValue<string>("ApiKey") ?? string.Empty; return _configuration?.GetSection("QDrant")?.GetValue<string>("ApiKey") ?? string.Empty;
} }
private string GetAiEmbeddingSettings() =>
_configuration?.GetSection("AiSettings")?.GetValue<string>("EmbeddingService") ?? string.Empty;
//public async Task ProcessAndStoreSnippetsAsync() //public async Task ProcessAndStoreSnippetsAsync()
//{ //{
// _qdrantApiKey = GetApiKey(); // _qdrantApiKey = GetApiKey();
@ -94,14 +100,27 @@ namespace BLAIzor.Services
{ {
try try
{ {
// Combine details to generate the embedding // Combine details to generate the embedding
var combinedText = $"{snippet.Name}: {snippet.Description}. " + var combinedText = $"{snippet.Name}: {snippet.Description}. " +
$"Type: {snippet.Type}. " + $"Type: {snippet.Type}. " +
$"Variant: {snippet.Variant ?? "default"}. " + $"Variant: {snippet.Variant ?? "default"}. " +
$"Tags: {snippet.Tags}. " + $"Tags: {snippet.Tags}. " +
$"HTML: {snippet.Html}"; $"HTML: {snippet.Html}";
var embedding = await _embeddingService.GenerateEmbeddingAsync(combinedText);
float[] embedding = [];
var embeddingServiceProvider = GetAiEmbeddingSettings();
//if (embeddingServiceProvider == "local")
//{
// embedding = await _localEmbeddingService.GenerateEmbeddingAsync(combinedText);
//}
//else
//{
// embedding = await _openAIEmbeddingService.GenerateEmbeddingAsync(combinedText);
//}
embedding = await _openAIEmbeddingService.GenerateEmbeddingAsync(combinedText);
// Add data for batch insertion // Add data for batch insertion
ids.Add(snippet.Id); ids.Add(snippet.Id);
@ -110,7 +129,7 @@ namespace BLAIzor.Services
{ {
["type"] = snippet.Type, ["type"] = snippet.Type,
["name"] = snippet.Name, ["name"] = snippet.Name,
["variant"] = string.IsNullOrWhiteSpace(snippet.Variant)? "" : snippet.Variant, ["variant"] = string.IsNullOrWhiteSpace(snippet.Variant) ? "" : snippet.Variant,
["tags"] = snippet.Tags, ["tags"] = snippet.Tags,
["description"] = snippet.Description, ["description"] = snippet.Description,
["html"] = snippet.Html, ["html"] = snippet.Html,
@ -133,95 +152,114 @@ namespace BLAIzor.Services
} }
} }
public async Task ProcessAndStoreWebContentsAsync(List<WebPageContent> pageContentList, int siteId) //public async Task ProcessAndStoreWebContentsAsync(List<WebPageContent> pageContentList, int siteId)
//{
// _qdrantApiKey = GetApiKey();
// var ids = new List<int>();
// var vectors = new List<float[]>();
// var payloads = new List<MapField<string, Value>>();
// foreach (var content in pageContentList)
// {
// try
// {
// // Combine details to generate the embedding
// var combinedText = $"{content.Name}: Description: {content.Description}, complete text: {content.Content}";
// float[] embedding = [];
// var embeddingServiceProvider = GetAiEmbeddingSettings();
// if (embeddingServiceProvider == "local")
// {
// embedding = await _localEmbeddingService.GenerateEmbeddingAsync(combinedText);
// }
// else
// {
// embedding = await _openAIEmbeddingService.GenerateEmbeddingAsync(combinedText);
// }
// // Add data for batch insertion
// ids.Add(content.Id);
// vectors.Add(embedding);
// payloads.Add(new MapField<string, Value>
// {
// ["uid"] = content.UId,
// ["type"] = content.Type,
// ["siteId"] = content.SiteId,
// //["menuItemId"] = content.MenuItemId,
// ["name"] = content.Name,
// ["description"] = content.Description,
// ["content"] = content.Content,
// ["lastUpdated"] = content.LastUpdated.ToString()
// });
// }
// catch (Exception ex)
// {
// Console.WriteLine($"Error processing content {content.Name}: {ex.Message}");
// }
// }
// if (ids.Count > 0)
// {
// await _drantService.QDrantInsertManyAsync(ids, vectors, payloads, "Site" + siteId);
// }
// else
// {
// Console.WriteLine("No points were processed successfully.");
// }
//}
public async Task ProcessAndStoreWebContentAsync(PointId id, WebPageContent pageContent, int siteId)
{ {
_qdrantApiKey = GetApiKey(); _qdrantApiKey = GetApiKey();
var ids = new List<int>();
var vectors = new List<float[]>();
var payloads = new List<MapField<string, Value>>();
foreach (var content in pageContentList)
{
try
{
// Combine details to generate the embedding
var combinedText = $"{content.Name}: Description: {content.Description}, complete text: {content.Content}";
var embedding = await _embeddingService.GenerateEmbeddingAsync(combinedText);
// Add data for batch insertion
ids.Add(content.Id);
vectors.Add(embedding);
payloads.Add(new MapField<string, Value>
{
["uid"] = content.UId,
["type"] = content.Type,
["siteId"] = content.SiteId,
//["menuItemId"] = content.MenuItemId,
["name"] = content.Name,
["description"] = content.Description,
["content"] = content.Content,
["lastUpdated"] = content.LastUpdated.ToString()
});
}
catch (Exception ex)
{
Console.WriteLine($"Error processing content {content.Name}: {ex.Message}");
}
}
if (ids.Count > 0)
{
await _drantService.QDrantInsertManyAsync(ids, vectors, payloads, "Site"+siteId);
}
else
{
Console.WriteLine("No points were processed successfully.");
}
}
public async Task ProcessAndStoreWebContentAsync(int id, WebPageContent pageContent, int siteId)
{
_qdrantApiKey = GetApiKey();
float[] vectors = []; float[] vectors = [];
var payload = new MapField<string, Value>(); var payload = new MapField<string, Value>();
try
try {
// Combine details to generate the embedding
var combinedText = $"{pageContent.Name}: {pageContent.Description} - {pageContent.Content}";
float[] embedding = [];
var embeddingServiceProvider = GetAiEmbeddingSettings();
if (embeddingServiceProvider == "local")
{ {
// Combine details to generate the embedding embedding = await _localEmbeddingService.GenerateEmbeddingAsync(combinedText);
var combinedText = $"{pageContent.Name}: {pageContent.Description} - {pageContent.Content}";
var embedding = await _embeddingService.GenerateEmbeddingAsync(combinedText);
// Add data for batch insertion
vectors = embedding;
payload = new MapField<string, Value>
{
["uid"] = pageContent.UId,
["type"] = pageContent.Type,
["siteId"] = pageContent.SiteId,
//["menuItemId"] = pageContent.MenuItemId,
["name"] = pageContent.Name,
["description"] = pageContent.Description,
["content"] = pageContent.Content,
["lastUpdated"] = pageContent.LastUpdated.ToString()
};
} }
catch (Exception ex) else
{ {
Console.WriteLine($"Error processing content {pageContent.Name}: {ex.Message}"); embedding = await _openAIEmbeddingService.GenerateEmbeddingAsync(combinedText);
} }
// Add data for batch insertion
await _drantService.QDrantInsertPointAsync(id, vectors, payload, "Site"+siteId);
vectors = embedding;
payload = new MapField<string, Value>
{
["uid"] = pageContent.UId,
["type"] = pageContent.Type,
["siteId"] = pageContent.SiteId,
//["menuItemId"] = pageContent.MenuItemId,
["name"] = pageContent.Name,
["description"] = pageContent.Description,
["content"] = pageContent.Content,
["lastUpdated"] = pageContent.LastUpdated.ToString()
};
}
catch (Exception ex)
{
Console.WriteLine($"Error processing content {pageContent.Name}: {ex.Message}");
}
await _drantService.QDrantInsertPointAsync(id, vectors, payload, "Site" + siteId);
} }
private List<HtmlSnippet> GetHtmlSnippets() private List<HtmlSnippet> GetHtmlSnippets()

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